Commit 26d47cc6 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add complete HR module: employees, trainers, qualifications, availability

Full vertical slice with migrations, enums, models, services, Livewire CRUD,
views, permissions, routes, and sidebar navigation for the HR domain.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4a181920
<?php
namespace App\Domain\HR\Enums;
enum AvailabilityType: string
{
case Available = 'available';
case Unavailable = 'unavailable';
case Preferred = 'preferred';
public function label(): string
{
return match ($this) {
self::Available => 'متاح',
self::Unavailable => 'غير متاح',
self::Preferred => 'مفضّل',
};
}
}
<?php
namespace App\Domain\HR\Enums;
enum CompensationModel: string
{
case Salary = 'salary';
case Hourly = 'hourly';
case PerSession = 'per_session';
case PerGroup = 'per_group';
case PerPlayer = 'per_player';
case RevenueShare = 'revenue_share';
case Contract = 'contract';
case Hybrid = 'hybrid';
public function label(): string
{
return match ($this) {
self::Salary => 'راتب ثابت',
self::Hourly => 'بالساعة',
self::PerSession => 'بالحصة',
self::PerGroup => 'بالمجموعة',
self::PerPlayer => 'باللاعب',
self::RevenueShare => 'نسبة من الإيرادات',
self::Contract => 'عقد محدد',
self::Hybrid => 'مختلط',
};
}
}
<?php
namespace App\Domain\HR\Enums;
enum EmployeeStatus: string
{
case Active = 'active';
case OnLeave = 'on_leave';
case Suspended = 'suspended';
case Terminated = 'terminated';
case Resigned = 'resigned';
public function label(): string
{
return match ($this) {
self::Active => 'نشط',
self::OnLeave => 'في إجازة',
self::Suspended => 'موقوف',
self::Terminated => 'منتهي الخدمة',
self::Resigned => 'مستقيل',
};
}
}
<?php
namespace App\Domain\HR\Enums;
enum EmploymentType: string
{
case FullTime = 'full_time';
case PartTime = 'part_time';
case Contract = 'contract';
case Intern = 'intern';
case Volunteer = 'volunteer';
public function label(): string
{
return match ($this) {
self::FullTime => 'دوام كامل',
self::PartTime => 'دوام جزئي',
self::Contract => 'عقد',
self::Intern => 'متدرب',
self::Volunteer => 'متطوع',
};
}
}
<?php
namespace App\Domain\HR\Enums;
enum SalaryFrequency: string
{
case Monthly = 'monthly';
case Biweekly = 'biweekly';
case Weekly = 'weekly';
case Daily = 'daily';
case Hourly = 'hourly';
public function label(): string
{
return match ($this) {
self::Monthly => 'شهري',
self::Biweekly => 'نصف شهري',
self::Weekly => 'أسبوعي',
self::Daily => 'يومي',
self::Hourly => 'بالساعة',
};
}
}
<?php
namespace App\Domain\HR\Enums;
enum TrainerStatus: string
{
case Active = 'active';
case Inactive = 'inactive';
case OnLeave = 'on_leave';
case Suspended = 'suspended';
public function label(): string
{
return match ($this) {
self::Active => 'نشط',
self::Inactive => 'غير نشط',
self::OnLeave => 'في إجازة',
self::Suspended => 'موقوف',
};
}
}
<?php
namespace App\Domain\HR\Models;
use App\Domain\HR\Enums\EmployeeStatus;
use App\Domain\HR\Enums\EmploymentType;
use App\Domain\HR\Enums\SalaryFrequency;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
class Employee extends Model
{
use BelongsToAcademy, HasUuid, SoftDeletes;
protected $fillable = [
'academy_id',
'person_id',
'user_id',
'employee_number',
'department',
'position',
'employment_type',
'start_date',
'end_date',
'salary_amount',
'salary_frequency',
'working_hours_per_week',
'branch_id',
'manager_id',
'status',
'termination_reason',
'notes',
'metadata',
'created_by',
'updated_by',
];
protected $casts = [
'employment_type' => EmploymentType::class,
'status' => EmployeeStatus::class,
'salary_frequency' => SalaryFrequency::class,
'salary_amount' => 'integer',
'working_hours_per_week' => 'decimal:1',
'start_date' => 'date',
'end_date' => 'date',
'metadata' => 'array',
];
// --- Relationships ---
public function person(): BelongsTo
{
return $this->belongsTo(\App\Domain\Identity\Models\Person::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class);
}
public function branch(): BelongsTo
{
return $this->belongsTo(\App\Domain\Identity\Models\Branch::class);
}
public function manager(): BelongsTo
{
return $this->belongsTo(self::class, 'manager_id');
}
public function directReports(): HasMany
{
return $this->hasMany(self::class, 'manager_id');
}
public function trainer(): HasOne
{
return $this->hasOne(Trainer::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'created_by');
}
public function updater(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'updated_by');
}
// --- Scopes ---
public function scopeActive(Builder $query): Builder
{
return $query->where('status', EmployeeStatus::Active->value);
}
public function scopeForBranch(Builder $query, ?int $branchId): Builder
{
return $query->where(function (Builder $q) use ($branchId) {
$q->whereNull('branch_id')
->orWhere('branch_id', $branchId);
});
}
}
<?php
namespace App\Domain\HR\Models;
use App\Domain\HR\Enums\CompensationModel;
use App\Domain\HR\Enums\TrainerStatus;
use App\Domain\Shared\Traits\BelongsToAcademy;
use App\Domain\Shared\Traits\HasUuid;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Trainer extends Model
{
use BelongsToAcademy, HasUuid;
protected $fillable = [
'academy_id',
'employee_id',
'trainer_number',
'bio',
'bio_ar',
'specializations',
'sports',
'max_daily_sessions',
'max_weekly_hours',
'compensation_model',
'hourly_rate',
'session_rate',
'group_rate',
'player_rate',
'revenue_share_percent',
'rating',
'total_sessions_conducted',
'status',
];
protected $casts = [
'compensation_model' => CompensationModel::class,
'status' => TrainerStatus::class,
'specializations' => 'array',
'sports' => 'array',
'hourly_rate' => 'integer',
'session_rate' => 'integer',
'group_rate' => 'integer',
'player_rate' => 'integer',
'max_daily_sessions' => 'integer',
'max_weekly_hours' => 'decimal:1',
'revenue_share_percent' => 'decimal:2',
'rating' => 'decimal:2',
'total_sessions_conducted' => 'integer',
];
// --- Relationships ---
public function employee(): BelongsTo
{
return $this->belongsTo(Employee::class);
}
public function qualifications(): HasMany
{
return $this->hasMany(TrainerQualification::class);
}
public function availabilities(): HasMany
{
return $this->hasMany(TrainerAvailability::class);
}
public function assignments(): HasMany
{
return $this->hasMany(\App\Domain\Scheduling\Models\Assignment::class, 'user_id', 'employee_id')
->whereHas('employee', fn ($q) => $q->whereColumn('employees.user_id', 'assignments.user_id'));
}
// --- Scopes ---
public function scopeActive(Builder $query): Builder
{
return $query->where('status', TrainerStatus::Active->value);
}
}
<?php
namespace App\Domain\HR\Models;
use App\Domain\HR\Enums\AvailabilityType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TrainerAvailability extends Model
{
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = null;
protected $table = 'trainer_availability';
protected $fillable = [
'trainer_id',
'type',
'day_of_week',
'specific_date',
'start_time',
'end_time',
'reason',
];
protected $casts = [
'type' => AvailabilityType::class,
'specific_date' => 'date',
'day_of_week' => 'integer',
];
// --- Relationships ---
public function trainer(): BelongsTo
{
return $this->belongsTo(Trainer::class);
}
}
<?php
namespace App\Domain\HR\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TrainerQualification extends Model
{
public $timestamps = false;
const CREATED_AT = 'created_at';
const UPDATED_AT = null;
protected $fillable = [
'trainer_id',
'name',
'issuer',
'issue_date',
'expiry_date',
'document_path',
'is_verified',
'verified_by',
'verified_at',
];
protected $casts = [
'issue_date' => 'date',
'expiry_date' => 'date',
'is_verified' => 'boolean',
'verified_at' => 'datetime',
];
// --- Relationships ---
public function trainer(): BelongsTo
{
return $this->belongsTo(Trainer::class);
}
public function verifier(): BelongsTo
{
return $this->belongsTo(\App\Models\User::class, 'verified_by');
}
}
<?php
namespace App\Domain\HR\Services;
use App\Domain\HR\Models\Employee;
use App\Models\User;
use App\Domain\Shared\Exceptions\DomainException;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use Illuminate\Support\Facades\DB;
class EmployeeService
{
private const VALID_TRANSITIONS = [
'active' => ['on_leave', 'suspended', 'terminated', 'resigned'],
'on_leave' => ['active', 'terminated', 'resigned'],
'suspended' => ['active', 'terminated', 'resigned'],
'terminated' => [],
'resigned' => [],
];
public function create(array $data, User $actor): Employee
{
return DB::transaction(function () use ($data, $actor) {
$data['employee_number'] = $this->generateEmployeeNumber($data['academy_id'], $data['branch_id'] ?? null);
$data['created_by'] = $actor->id;
return Employee::create($data);
});
}
public function update(Employee $employee, array $data, User $actor): Employee
{
return DB::transaction(function () use ($employee, $data, $actor) {
$data['updated_by'] = $actor->id;
$employee->update($data);
return $employee->fresh();
});
}
public function changeStatus(Employee $employee, string $newStatus, ?string $reason, User $actor): Employee
{
$currentStatus = $employee->status->value;
$allowed = self::VALID_TRANSITIONS[$currentStatus] ?? [];
if (! in_array($newStatus, $allowed)) {
throw new InvalidStatusTransitionException(
"لا يمكن تغيير حالة الموظف من '{$currentStatus}' إلى '{$newStatus}'"
);
}
return DB::transaction(function () use ($employee, $newStatus, $reason, $actor) {
$updateData = [
'status' => $newStatus,
'updated_by' => $actor->id,
];
if (in_array($newStatus, ['terminated', 'resigned'])) {
$updateData['end_date'] = now()->toDateString();
$updateData['termination_reason'] = $reason;
}
$employee->update($updateData);
return $employee->fresh();
});
}
private function generateEmployeeNumber(int $academyId, ?int $branchId): string
{
$lastNumber = Employee::withoutGlobalScopes()
->where('academy_id', $academyId)
->max('id') ?? 0;
$seq = str_pad($lastNumber + 1, 4, '0', STR_PAD_LEFT);
return "EMP-{$seq}";
}
}
<?php
namespace App\Domain\HR\Services;
use App\Domain\HR\Models\Employee;
use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Models\TrainerAvailability;
use App\Domain\HR\Models\TrainerQualification;
use App\Models\User;
use App\Domain\Shared\Exceptions\InvalidStatusTransitionException;
use Illuminate\Support\Facades\DB;
class TrainerService
{
private const VALID_TRANSITIONS = [
'active' => ['inactive', 'on_leave', 'suspended'],
'inactive' => ['active'],
'on_leave' => ['active', 'suspended'],
'suspended' => ['active', 'inactive'],
];
public function create(Employee $employee, array $data, User $actor): Trainer
{
return DB::transaction(function () use ($employee, $data) {
$data['academy_id'] = $employee->academy_id;
$data['employee_id'] = $employee->id;
$data['trainer_number'] = $this->generateTrainerNumber($employee->academy_id);
return Trainer::create($data);
});
}
public function update(Trainer $trainer, array $data, User $actor): Trainer
{
return DB::transaction(function () use ($trainer, $data) {
$trainer->update($data);
return $trainer->fresh();
});
}
public function changeStatus(Trainer $trainer, string $newStatus, User $actor): Trainer
{
$currentStatus = $trainer->status->value;
$allowed = self::VALID_TRANSITIONS[$currentStatus] ?? [];
if (! in_array($newStatus, $allowed)) {
throw new InvalidStatusTransitionException(
"لا يمكن تغيير حالة المدرب من '{$currentStatus}' إلى '{$newStatus}'"
);
}
return DB::transaction(function () use ($trainer, $newStatus) {
$trainer->update(['status' => $newStatus]);
return $trainer->fresh();
});
}
public function addQualification(Trainer $trainer, array $data): TrainerQualification
{
$data['trainer_id'] = $trainer->id;
$data['created_at'] = now();
return TrainerQualification::create($data);
}
public function removeQualification(TrainerQualification $qualification): void
{
$qualification->delete();
}
public function verifyQualification(TrainerQualification $qualification, User $verifier): void
{
$qualification->update([
'is_verified' => true,
'verified_by' => $verifier->id,
'verified_at' => now(),
]);
}
public function setAvailability(Trainer $trainer, array $slots): void
{
DB::transaction(function () use ($trainer, $slots) {
$trainer->availabilities()->delete();
$now = now();
$records = array_map(fn (array $slot) => [
'trainer_id' => $trainer->id,
'type' => $slot['type'],
'day_of_week' => $slot['day_of_week'] ?? null,
'specific_date' => $slot['specific_date'] ?? null,
'start_time' => $slot['start_time'],
'end_time' => $slot['end_time'],
'reason' => $slot['reason'] ?? null,
'created_at' => $now,
], $slots);
if (! empty($records)) {
TrainerAvailability::insert($records);
}
});
}
private function generateTrainerNumber(int $academyId): string
{
$lastNumber = Trainer::withoutGlobalScopes()
->where('academy_id', $academyId)
->max('id') ?? 0;
$seq = str_pad($lastNumber + 1, 4, '0', STR_PAD_LEFT);
return "TRN-{$seq}";
}
}
<?php
namespace App\Livewire\HR;
use App\Domain\HR\Enums\EmployeeStatus;
use App\Domain\HR\Enums\EmploymentType;
use App\Domain\HR\Enums\SalaryFrequency;
use App\Domain\HR\Models\Employee;
use App\Domain\HR\Services\EmployeeService;
use App\Domain\Identity\Models\Branch;
use App\Domain\Identity\Models\Person;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('components.layouts.app')]
#[Title('بيانات الموظف')]
class EmployeeForm extends Component
{
public ?Employee $employee = null;
public bool $editing = false;
// Person selection
public string $personSearch = '';
public ?int $personId = null;
public string $selectedPersonName = '';
// Form fields
public string $department = '';
public string $position = '';
public string $employmentType = 'full_time';
public string $startDate = '';
public ?string $endDate = null;
public ?string $salaryAmount = null;
public string $salaryFrequency = '';
public ?string $workingHoursPerWeek = null;
public string $branchId = '';
public string $managerId = '';
public string $notes = '';
public function mount(?Employee $employee = null): void
{
if ($employee && $employee->exists) {
$this->authorize('employees.update');
$this->employee = $employee;
$this->editing = true;
$this->personId = $employee->person_id;
$this->selectedPersonName = $employee->person->name_ar ?? '';
$this->department = $employee->department ?? '';
$this->position = $employee->position ?? '';
$this->employmentType = $employee->employment_type->value;
$this->startDate = $employee->start_date->format('Y-m-d');
$this->endDate = $employee->end_date?->format('Y-m-d');
$this->salaryAmount = $employee->salary_amount ? (string) ($employee->salary_amount / 100) : null;
$this->salaryFrequency = $employee->salary_frequency?->value ?? '';
$this->workingHoursPerWeek = $employee->working_hours_per_week ? (string) $employee->working_hours_per_week : null;
$this->branchId = (string) ($employee->branch_id ?? '');
$this->managerId = (string) ($employee->manager_id ?? '');
$this->notes = $employee->notes ?? '';
} else {
$this->authorize('employees.create');
$this->startDate = now()->format('Y-m-d');
}
}
public function rules(): array
{
return [
'personId' => 'required|exists:people,id',
'department' => 'nullable|string|max:30',
'position' => 'nullable|string|max:50',
'employmentType' => 'required|in:' . implode(',', array_column(EmploymentType::cases(), 'value')),
'startDate' => 'required|date',
'endDate' => 'nullable|date|after_or_equal:startDate',
'salaryAmount' => 'nullable|numeric|min:0',
'salaryFrequency' => 'nullable|in:' . implode(',', array_column(SalaryFrequency::cases(), 'value')),
'workingHoursPerWeek' => 'nullable|numeric|min:0|max:168',
'branchId' => 'nullable|exists:branches,id',
'managerId' => 'nullable|exists:employees,id',
'notes' => 'nullable|string|max:2000',
];
}
public function messages(): array
{
return [
'personId.required' => 'يجب اختيار شخص',
'personId.exists' => 'الشخص المختار غير موجود',
'employmentType.required' => 'نوع التوظيف مطلوب',
'employmentType.in' => 'نوع التوظيف غير صالح',
'startDate.required' => 'تاريخ البداية مطلوب',
'startDate.date' => 'تاريخ البداية غير صالح',
'endDate.after_or_equal' => 'تاريخ الانتهاء يجب أن يكون بعد تاريخ البداية',
'salaryAmount.numeric' => 'الراتب يجب أن يكون رقماً',
'salaryFrequency.in' => 'دورة الراتب غير صالحة',
'workingHoursPerWeek.numeric' => 'ساعات العمل يجب أن تكون رقماً',
];
}
public function save(EmployeeService $service): void
{
$this->validate();
$data = [
'person_id' => $this->personId,
'department' => $this->department ?: null,
'position' => $this->position ?: null,
'employment_type' => $this->employmentType,
'start_date' => $this->startDate,
'end_date' => $this->endDate ?: null,
'salary_amount' => $this->salaryAmount ? (int) round((float) $this->salaryAmount * 100) : null,
'salary_frequency' => $this->salaryFrequency ?: null,
'working_hours_per_week' => $this->workingHoursPerWeek ?: null,
'branch_id' => $this->branchId ?: null,
'manager_id' => $this->managerId ?: null,
'notes' => $this->notes ?: null,
];
try {
if ($this->editing) {
$service->update($this->employee, $data, auth()->user());
session()->flash('success', __('تم تحديث بيانات الموظف بنجاح'));
} else {
$data['academy_id'] = app('current_academy')->id;
$service->create($data, auth()->user());
session()->flash('success', __('تم إضافة الموظف بنجاح'));
}
$this->redirect(route('employees.list'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function selectPerson(int $id): void
{
$person = Person::find($id);
if ($person) {
$this->personId = $person->id;
$this->selectedPersonName = $person->name_ar;
$this->personSearch = '';
}
}
public function render()
{
$searchResults = [];
if (strlen($this->personSearch) >= 2) {
$searchResults = Person::where(function ($q) {
$q->where('name_ar', 'ilike', "%{$this->personSearch}%")
->orWhere('name', 'ilike', "%{$this->personSearch}%")
->orWhere('phone', 'ilike', "%{$this->personSearch}%")
->orWhere('national_id', 'ilike', "%{$this->personSearch}%");
})->limit(10)->get();
}
return view('livewire.hr.employee-form', [
'searchResults' => $searchResults,
'employmentTypes' => EmploymentType::cases(),
'salaryFrequencies' => SalaryFrequency::cases(),
'branches' => Branch::orderBy('name_ar')->get(),
'managers' => Employee::with('person')->active()->get(),
]);
}
}
<?php
namespace App\Livewire\HR;
use App\Domain\HR\Enums\EmployeeStatus;
use App\Domain\HR\Enums\EmploymentType;
use App\Domain\HR\Models\Employee;
use App\Domain\HR\Services\EmployeeService;
use App\Domain\Identity\Services\PermissionService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('components.layouts.app')]
#[Title('الموظفين')]
class EmployeeList extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
#[Url]
public string $employmentType = '';
#[Url]
public string $branchId = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function updatedEmploymentType(): void
{
$this->resetPage();
}
public function updatedBranchId(): void
{
$this->resetPage();
}
public function delete(string $uuid, EmployeeService $service): void
{
$this->authorize('employees.delete');
$employee = Employee::where('uuid', $uuid)->firstOrFail();
$employee->delete();
session()->flash('success', __('تم حذف الموظف بنجاح'));
}
public function render()
{
$query = Employee::with(['person', 'branch'])
->latest();
$query = PermissionService::applyScope($query, auth()->user(), 'employees.list');
if ($this->search) {
$search = $this->search;
$query->where(function ($q) use ($search) {
$q->where('employee_number', 'ilike', "%{$search}%")
->orWhereHas('person', function ($pq) use ($search) {
$pq->where('name_ar', 'ilike', "%{$search}%")
->orWhere('name', 'ilike', "%{$search}%")
->orWhere('national_id', 'ilike', "%{$search}%");
});
});
}
if ($this->status) {
$query->where('status', $this->status);
}
if ($this->employmentType) {
$query->where('employment_type', $this->employmentType);
}
if ($this->branchId) {
$query->where('branch_id', $this->branchId);
}
return view('livewire.hr.employee-list', [
'employees' => $query->paginate(20),
'statuses' => EmployeeStatus::cases(),
'employmentTypes' => EmploymentType::cases(),
'branches' => \App\Domain\Identity\Models\Branch::orderBy('name_ar')->get(),
]);
}
}
<?php
namespace App\Livewire\HR;
use App\Domain\HR\Enums\CompensationModel;
use App\Domain\HR\Models\Employee;
use App\Domain\HR\Models\Trainer;
use App\Domain\HR\Services\TrainerService;
use App\Domain\Shared\Exceptions\DomainException;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('components.layouts.app')]
#[Title('بيانات المدرب')]
class TrainerForm extends Component
{
public ?Trainer $trainer = null;
public bool $editing = false;
// Employee selection
public string $employeeSearch = '';
public ?int $employeeId = null;
public string $selectedEmployeeName = '';
// Form fields
public string $bio = '';
public string $bioAr = '';
public array $specializations = [];
public array $sports = [];
public ?string $maxDailySessions = null;
public ?string $maxWeeklyHours = null;
public string $compensationModel = 'salary';
public ?string $hourlyRate = null;
public ?string $sessionRate = null;
public ?string $groupRate = null;
public ?string $playerRate = null;
public ?string $revenueSharePercent = null;
// Qualifications
public array $qualifications = [];
public string $newQualName = '';
public string $newQualIssuer = '';
public ?string $newQualIssueDate = null;
public ?string $newQualExpiryDate = null;
public function mount(?Trainer $trainer = null): void
{
if ($trainer && $trainer->exists) {
$this->authorize('trainers.update');
$this->trainer = $trainer->load(['employee.person', 'qualifications']);
$this->editing = true;
$this->employeeId = $trainer->employee_id;
$this->selectedEmployeeName = $trainer->employee->person->name_ar ?? '';
$this->bio = $trainer->bio ?? '';
$this->bioAr = $trainer->bio_ar ?? '';
$this->specializations = $trainer->specializations ?? [];
$this->sports = $trainer->sports ?? [];
$this->maxDailySessions = $trainer->max_daily_sessions ? (string) $trainer->max_daily_sessions : null;
$this->maxWeeklyHours = $trainer->max_weekly_hours ? (string) $trainer->max_weekly_hours : null;
$this->compensationModel = $trainer->compensation_model->value;
$this->hourlyRate = $trainer->hourly_rate ? (string) ($trainer->hourly_rate / 100) : null;
$this->sessionRate = $trainer->session_rate ? (string) ($trainer->session_rate / 100) : null;
$this->groupRate = $trainer->group_rate ? (string) ($trainer->group_rate / 100) : null;
$this->playerRate = $trainer->player_rate ? (string) ($trainer->player_rate / 100) : null;
$this->revenueSharePercent = $trainer->revenue_share_percent ? (string) $trainer->revenue_share_percent : null;
$this->qualifications = $trainer->qualifications->map(fn ($q) => [
'id' => $q->id,
'name' => $q->name,
'issuer' => $q->issuer,
'issue_date' => $q->issue_date?->format('Y-m-d'),
'expiry_date' => $q->expiry_date?->format('Y-m-d'),
'is_verified' => $q->is_verified,
])->toArray();
} else {
$this->authorize('trainers.create');
}
}
public function rules(): array
{
return [
'employeeId' => 'required|exists:employees,id',
'bio' => 'nullable|string|max:2000',
'bioAr' => 'nullable|string|max:2000',
'compensationModel' => 'required|in:' . implode(',', array_column(CompensationModel::cases(), 'value')),
'maxDailySessions' => 'nullable|integer|min:1|max:20',
'maxWeeklyHours' => 'nullable|numeric|min:1|max:168',
'hourlyRate' => 'nullable|numeric|min:0',
'sessionRate' => 'nullable|numeric|min:0',
'groupRate' => 'nullable|numeric|min:0',
'playerRate' => 'nullable|numeric|min:0',
'revenueSharePercent' => 'nullable|numeric|min:0|max:100',
];
}
public function messages(): array
{
return [
'employeeId.required' => 'يجب اختيار موظف',
'employeeId.exists' => 'الموظف المختار غير موجود',
'compensationModel.required' => 'نموذج التعويض مطلوب',
'compensationModel.in' => 'نموذج التعويض غير صالح',
'maxDailySessions.integer' => 'الحد الأقصى للحصص يجب أن يكون رقماً صحيحاً',
'maxWeeklyHours.numeric' => 'ساعات العمل الأسبوعية يجب أن تكون رقماً',
'revenueSharePercent.max' => 'نسبة الإيرادات لا يمكن أن تتجاوز 100%',
];
}
public function save(TrainerService $service): void
{
$this->validate();
$data = [
'bio' => $this->bio ?: null,
'bio_ar' => $this->bioAr ?: null,
'specializations' => $this->specializations,
'sports' => $this->sports,
'max_daily_sessions' => $this->maxDailySessions ? (int) $this->maxDailySessions : null,
'max_weekly_hours' => $this->maxWeeklyHours ?: null,
'compensation_model' => $this->compensationModel,
'hourly_rate' => $this->hourlyRate ? (int) round((float) $this->hourlyRate * 100) : null,
'session_rate' => $this->sessionRate ? (int) round((float) $this->sessionRate * 100) : null,
'group_rate' => $this->groupRate ? (int) round((float) $this->groupRate * 100) : null,
'player_rate' => $this->playerRate ? (int) round((float) $this->playerRate * 100) : null,
'revenue_share_percent' => $this->revenueSharePercent ?: null,
];
try {
if ($this->editing) {
$service->update($this->trainer, $data, auth()->user());
session()->flash('success', __('تم تحديث بيانات المدرب بنجاح'));
} else {
$employee = Employee::findOrFail($this->employeeId);
$service->create($employee, $data, auth()->user());
session()->flash('success', __('تم إضافة المدرب بنجاح'));
}
$this->redirect(route('trainers.list'), navigate: true);
} catch (DomainException $e) {
session()->flash('error', $e->getMessage());
}
}
public function addQualification(): void
{
if (! $this->newQualName) {
return;
}
$this->qualifications[] = [
'id' => null,
'name' => $this->newQualName,
'issuer' => $this->newQualIssuer,
'issue_date' => $this->newQualIssueDate,
'expiry_date' => $this->newQualExpiryDate,
'is_verified' => false,
];
$this->newQualName = '';
$this->newQualIssuer = '';
$this->newQualIssueDate = null;
$this->newQualExpiryDate = null;
}
public function removeQualification(int $index): void
{
unset($this->qualifications[$index]);
$this->qualifications = array_values($this->qualifications);
}
public function selectEmployee(int $id): void
{
$employee = Employee::with('person')->find($id);
if ($employee) {
$this->employeeId = $employee->id;
$this->selectedEmployeeName = $employee->person->name_ar;
$this->employeeSearch = '';
}
}
public function render()
{
$searchResults = [];
if (strlen($this->employeeSearch) >= 2) {
$searchResults = Employee::with('person')
->active()
->doesntHave('trainer')
->whereHas('person', function ($q) {
$q->where('name_ar', 'ilike', "%{$this->employeeSearch}%")
->orWhere('name', 'ilike', "%{$this->employeeSearch}%");
})
->limit(10)->get();
}
return view('livewire.hr.trainer-form', [
'searchResults' => $searchResults,
'compensationModels' => CompensationModel::cases(),
'activities' => \App\Domain\Training\Models\Activity::where('is_active', true)->orderBy('name_ar')->get(),
]);
}
}
<?php
namespace App\Livewire\HR;
use App\Domain\HR\Enums\CompensationModel;
use App\Domain\HR\Enums\TrainerStatus;
use App\Domain\HR\Models\Trainer;
use App\Domain\Identity\Services\PermissionService;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('components.layouts.app')]
#[Title('المدربين')]
class TrainerList extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
#[Url]
public string $compensationModel = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function updatedCompensationModel(): void
{
$this->resetPage();
}
public function render()
{
$query = Trainer::with(['employee.person', 'employee.branch'])
->latest();
$query = PermissionService::applyScope($query, auth()->user(), 'trainers.list');
if ($this->search) {
$search = $this->search;
$query->where(function ($q) use ($search) {
$q->where('trainer_number', 'ilike', "%{$search}%")
->orWhereHas('employee.person', function ($pq) use ($search) {
$pq->where('name_ar', 'ilike', "%{$search}%")
->orWhere('name', 'ilike', "%{$search}%");
});
});
}
if ($this->status) {
$query->where('status', $this->status);
}
if ($this->compensationModel) {
$query->where('compensation_model', $this->compensationModel);
}
return view('livewire.hr.trainer-list', [
'trainers' => $query->paginate(20),
'statuses' => TrainerStatus::cases(),
'compensationModels' => CompensationModel::cases(),
]);
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('employees', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->foreignId('person_id')->constrained('people');
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('employee_number', 20);
$table->string('department', 30)->nullable();
$table->string('position', 50)->nullable();
$table->string('employment_type', 20);
$table->date('start_date');
$table->date('end_date')->nullable();
$table->bigInteger('salary_amount')->nullable();
$table->string('salary_frequency', 20)->nullable();
$table->decimal('working_hours_per_week', 4, 1)->nullable();
$table->foreignId('branch_id')->nullable()->constrained('branches')->nullOnDelete();
$table->unsignedBigInteger('manager_id')->nullable();
$table->string('status', 20)->default('active');
$table->text('termination_reason')->nullable();
$table->text('notes')->nullable();
$table->jsonb('metadata')->default('{}');
$table->foreignId('created_by')->constrained('users');
$table->foreignId('updated_by')->nullable()->constrained('users');
$table->timestamps();
$table->softDeletes();
$table->foreign('manager_id')->references('id')->on('employees')->nullOnDelete();
$table->unique(['academy_id', 'person_id']);
$table->unique(['academy_id', 'employee_number']);
$table->index(['academy_id', 'status']);
$table->index('branch_id');
});
DB::statement("ALTER TABLE employees ADD CONSTRAINT employees_employment_type_check CHECK (employment_type IN ('full_time', 'part_time', 'contract', 'intern', 'volunteer'))");
DB::statement("ALTER TABLE employees ADD CONSTRAINT employees_salary_frequency_check CHECK (salary_frequency IS NULL OR salary_frequency IN ('monthly', 'biweekly', 'weekly', 'daily', 'hourly'))");
DB::statement("ALTER TABLE employees ADD CONSTRAINT employees_status_check CHECK (status IN ('active', 'on_leave', 'suspended', 'terminated', 'resigned'))");
}
public function down(): void
{
Schema::dropIfExists('employees');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('trainers', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('academy_id')->constrained('academies');
$table->foreignId('employee_id')->unique()->constrained('employees');
$table->string('trainer_number', 20);
$table->text('bio')->nullable();
$table->text('bio_ar')->nullable();
$table->jsonb('specializations')->default('[]');
$table->jsonb('sports')->default('[]');
$table->integer('max_daily_sessions')->nullable();
$table->decimal('max_weekly_hours', 4, 1)->nullable();
$table->string('compensation_model', 20);
$table->bigInteger('hourly_rate')->nullable();
$table->bigInteger('session_rate')->nullable();
$table->bigInteger('group_rate')->nullable();
$table->bigInteger('player_rate')->nullable();
$table->decimal('revenue_share_percent', 5, 2)->nullable();
$table->decimal('rating', 3, 2)->nullable();
$table->integer('total_sessions_conducted')->default(0);
$table->string('status', 20)->default('active');
$table->timestamps();
$table->unique(['academy_id', 'trainer_number']);
$table->index(['academy_id', 'status']);
});
DB::statement("ALTER TABLE trainers ADD CONSTRAINT trainers_compensation_model_check CHECK (compensation_model IN ('salary', 'hourly', 'per_session', 'per_group', 'per_player', 'revenue_share', 'contract', 'hybrid'))");
DB::statement("ALTER TABLE trainers ADD CONSTRAINT trainers_status_check CHECK (status IN ('active', 'inactive', 'on_leave', 'suspended'))");
}
public function down(): void
{
Schema::dropIfExists('trainers');
}
};
<?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::create('trainer_qualifications', function (Blueprint $table) {
$table->id();
$table->foreignId('trainer_id')->constrained('trainers')->cascadeOnDelete();
$table->string('name', 100);
$table->string('issuer', 100)->nullable();
$table->date('issue_date')->nullable();
$table->date('expiry_date')->nullable();
$table->string('document_path', 500)->nullable();
$table->boolean('is_verified')->default(false);
$table->foreignId('verified_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('verified_at')->nullable();
$table->timestamp('created_at')->nullable();
$table->index('trainer_id');
$table->index('expiry_date');
});
}
public function down(): void
{
Schema::dropIfExists('trainer_qualifications');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('trainer_availability', function (Blueprint $table) {
$table->id();
$table->foreignId('trainer_id')->constrained('trainers')->cascadeOnDelete();
$table->string('type', 20);
$table->smallInteger('day_of_week')->nullable();
$table->date('specific_date')->nullable();
$table->time('start_time');
$table->time('end_time');
$table->string('reason', 200)->nullable();
$table->timestamp('created_at')->nullable();
$table->index('trainer_id');
$table->index(['trainer_id', 'day_of_week']);
});
DB::statement("ALTER TABLE trainer_availability ADD CONSTRAINT trainer_availability_type_check CHECK (type IN ('available', 'unavailable', 'preferred'))");
}
public function down(): void
{
Schema::dropIfExists('trainer_availability');
}
};
...@@ -9,7 +9,72 @@ class PermissionSeeder extends Seeder ...@@ -9,7 +9,72 @@ class PermissionSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
$permissions = [ $permissions = $this->getPermissionsList();
$now = now();
foreach ($permissions as $permission) {
$parts = explode('.', $permission);
DB::table('permissions')->updateOrInsert(
['name' => $permission],
[
'name' => $permission,
'module' => $parts[0],
'action' => $parts[1] ?? $permission,
'created_at' => $now,
]
);
}
$this->command->info('Seeded ' . count($permissions) . ' permissions.');
$rolePermissions = $this->getRolePermissions();
foreach ($rolePermissions as $roleSlug => $permMap) {
$role = DB::table('roles')->where('slug', $roleSlug)->first();
if (!$role) {
continue;
}
// For wildcard roles, assign ALL permissions with specified scope
if ($permMap === '*') {
$permIds = DB::table('permissions')->pluck('id', 'name')->toArray();
$inserts = [];
foreach ($permIds as $name => $pid) {
$inserts[] = [
'role_id' => $role->id,
'permission_id' => $pid,
'scope' => 'all',
'created_at' => $now,
];
}
} else {
$allPermIds = DB::table('permissions')->pluck('id', 'name')->toArray();
$inserts = [];
foreach ($permMap as $permName => $scope) {
if (!isset($allPermIds[$permName])) {
continue;
}
$inserts[] = [
'role_id' => $role->id,
'permission_id' => $allPermIds[$permName],
'scope' => $scope,
'created_at' => $now,
];
}
}
DB::table('permission_role')->where('role_id', $role->id)->delete();
foreach (array_chunk($inserts, 100) as $chunk) {
DB::table('permission_role')->insert($chunk);
}
$this->command->info(" Role '{$roleSlug}': " . count($inserts) . ' permissions assigned.');
}
}
private function getPermissionsList(): array
{
return [
// Dashboard // Dashboard
'dashboard.view', 'dashboard.export', 'dashboard.view', 'dashboard.export',
...@@ -24,8 +89,9 @@ public function run(): void ...@@ -24,8 +89,9 @@ public function run(): void
// Participants // Participants
'participants.list', 'participants.create', 'participants.update', 'participants.delete', 'participants.list', 'participants.create', 'participants.update', 'participants.delete',
'participants.show', 'participants.change_status', 'participants.export', 'participants.show', 'participants.view', 'participants.change_status', 'participants.export',
'participants.view_financial', 'participants.view_attendance', 'participants.view_financial', 'participants.view_attendance',
'participants.import', 'participants.bulk_status_change',
'guardians.list', 'guardians.create', 'guardians.update', 'guardians.list', 'guardians.create', 'guardians.update',
// Training // Training
...@@ -35,7 +101,7 @@ public function run(): void ...@@ -35,7 +101,7 @@ public function run(): void
'groups.manage_enrollments', 'groups.manage_enrollments',
'sessions.list', 'sessions.show', 'sessions.cancel', 'sessions.reschedule', 'sessions.list', 'sessions.show', 'sessions.cancel', 'sessions.reschedule',
'sessions.start', 'sessions.complete', 'sessions.generate', 'sessions.start', 'sessions.complete', 'sessions.generate',
'schedules.list', 'schedules.manage', 'schedules.list', 'schedules.view', 'schedules.manage',
'enrollments.list', 'enrollments.create', 'enrollments.cancel', 'enrollments.list', 'enrollments.create', 'enrollments.cancel',
'enrollments.override', 'enrollments.transfer', 'enrollments.override', 'enrollments.transfer',
'holidays.list', 'holidays.create', 'holidays.update', 'holidays.delete', 'holidays.list', 'holidays.create', 'holidays.update', 'holidays.delete',
...@@ -48,13 +114,13 @@ public function run(): void ...@@ -48,13 +114,13 @@ public function run(): void
// Financial // Financial
'invoices.list', 'invoices.create', 'invoices.update', 'invoices.cancel', 'invoices.list', 'invoices.create', 'invoices.update', 'invoices.cancel',
'invoices.show', 'invoices.send', 'invoices.export', 'invoices.show', 'invoices.view', 'invoices.send', 'invoices.export',
'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',
...@@ -66,13 +132,15 @@ public function run(): void ...@@ -66,13 +132,15 @@ public function run(): void
'coupons.list', 'coupons.generate', 'coupons.deactivate', 'coupons.list', 'coupons.generate', 'coupons.deactivate',
// POS // POS
'pos.access', 'pos.override_price', 'pos.override_stock', 'pos.void_transaction', 'pos.access', 'pos.sell', 'pos.list',
'pos.override_price', 'pos.override_stock', 'pos.void_transaction',
'pos_sessions.open', 'pos_sessions.close', 'pos_sessions.list', 'pos_sessions.open', 'pos_sessions.close', 'pos_sessions.list',
// Inventory // Inventory
'products.list', 'products.create', 'products.update', 'products.delete', 'products.export', 'products.list', 'products.create', 'products.update', 'products.delete', 'products.export',
'categories.list', 'categories.manage', 'categories.list', 'categories.manage',
'warehouses.list', 'warehouses.create', 'warehouses.update', 'warehouses.list', 'warehouses.create', 'warehouses.update',
'inventory.list', 'inventory.create', 'inventory.update',
'inventory.view_levels', 'inventory.adjust', 'inventory.transfer', 'inventory.view_levels', 'inventory.adjust', 'inventory.transfer',
'purchase_orders.list', 'purchase_orders.create', 'purchase_orders.approve', 'purchase_orders.receive', 'purchase_orders.list', 'purchase_orders.create', 'purchase_orders.approve', 'purchase_orders.receive',
'stock_counts.list', 'stock_counts.create', 'stock_counts.approve', 'stock_counts.list', 'stock_counts.create', 'stock_counts.approve',
...@@ -84,15 +152,24 @@ public function run(): void ...@@ -84,15 +152,24 @@ public function run(): void
'facilities.manage_layouts', 'facilities.manage_hours', 'facilities.manage_layouts', 'facilities.manage_hours',
'reservations.list', 'reservations.create', 'reservations.cancel', 'reservations.confirm', 'reservations.list', 'reservations.create', 'reservations.cancel', 'reservations.confirm',
// HR - Employees
'employees.list', 'employees.create', 'employees.update', 'employees.delete',
'employees.show', 'employees.change_status', 'employees.export',
// HR - Trainers
'trainers.list', 'trainers.create', 'trainers.update', 'trainers.delete',
'trainers.show', 'trainers.manage_qualifications', 'trainers.manage_availability',
'trainers.view_compensation',
// Assignments // Assignments
'assignments.list', 'assignments.create', 'assignments.update', 'assignments.cancel', 'assignments.list', 'assignments.create', 'assignments.update', 'assignments.cancel',
// Notifications // Notifications
'notifications.send', 'notifications.manage_templates', 'notifications.send', 'notifications.manage', 'notifications.manage_templates',
'notification_preferences.manage', 'notification_preferences.manage',
// Reports // Reports
'reports.financial', 'reports.attendance', 'reports.enrollment', 'reports.view', 'reports.financial', 'reports.attendance', 'reports.enrollment',
'reports.inventory', 'reports.trainer', 'reports.facility', 'reports.inventory', 'reports.trainer', 'reports.facility',
'reports.export_pdf', 'reports.export_excel', 'reports.export_pdf', 'reports.export_excel',
...@@ -104,153 +181,181 @@ public function run(): void ...@@ -104,153 +181,181 @@ public function run(): void
// Evaluations // Evaluations
'evaluations.list', 'evaluations.create', 'evaluations.update', 'evaluations.manage', 'evaluations.list', 'evaluations.create', 'evaluations.update', 'evaluations.manage',
'evaluations.approve', 'evaluations.share',
// Audit // Audit
'audit.list', 'audit.export', 'audit.list', 'audit.view', 'audit.export',
// POS (route-aligned aliases)
'pos.sell', 'pos.list',
// Notifications (route-aligned)
'notifications.manage',
// Cash Sessions (route-aligned)
'cash_sessions.manage',
// Wallets (route-aligned)
'wallets.view',
// Inventory (route-aligned)
'inventory.list', 'inventory.create', 'inventory.update',
// Reports (route-aligned)
'reports.view',
// Super Admin // Super Admin
'super_admin.access', 'super_admin.access',
]; ];
}
// Insert permissions (idempotent) /**
$now = now(); * Role → Permission mappings with per-permission scopes.
foreach ($permissions as $permission) { * '*' means all permissions with 'all' scope.
$parts = explode('.', $permission); * Otherwise: ['permission.name' => 'scope', ...]
DB::table('permissions')->updateOrInsert( */
['name' => $permission], private function getRolePermissions(): array
[ {
'name' => $permission, return [
'module' => $parts[0], 'super_admin' => '*',
'action' => $parts[1] ?? $permission, 'academy_owner' => '*',
'created_at' => $now, 'academy_admin' => $this->academyAdminPermissions(),
] 'branch_manager' => $this->branchManagerPermissions(),
); 'head_trainer' => $this->headTrainerPermissions(),
'trainer' => $this->trainerPermissions(),
'receptionist' => $this->receptionistPermissions(),
'accountant' => $this->accountantPermissions(),
'data_entry' => $this->dataEntryPermissions(),
'parent' => $this->parentPermissions(),
];
} }
$this->command->info('Seeded ' . count($permissions) . ' permissions.'); private function academyAdminPermissions(): array
{
$excluded = ['roles.delete', 'super_admin.access'];
$all = $this->getPermissionsList();
$map = [];
foreach ($all as $perm) {
if (!in_array($perm, $excluded)) {
$map[$perm] = 'all';
}
}
return $map;
}
// Role → Permission mapping private function branchManagerPermissions(): array
$rolePermissions = $this->getRolePermissions($permissions); {
$allPerms = $this->getPermissionsList();
$allowedModules = [
'participants', 'guardians', 'activities', 'programs', 'groups',
'sessions', 'schedules', 'enrollments', 'attendance', 'excuses',
'invoices', 'payments', 'wallets', 'cash_sessions', 'pricing',
'pos', 'pos_sessions', 'inventory', 'products', 'warehouses',
'categories', 'kits', 'facilities', 'reservations', 'assignments',
'evaluations', 'notifications', 'reports', 'audit',
'purchase_orders', 'stock_counts', 'suppliers',
'employees', 'trainers',
];
foreach ($rolePermissions as $roleSlug => $perms) { $map = [];
$role = DB::table('roles')->where('slug', $roleSlug)->first(); foreach ($allPerms as $perm) {
if (!$role) { $module = explode('.', $perm)[0];
continue; if (in_array($module, $allowedModules)) {
$map[$perm] = 'branch';
}
} }
if ($perms === '*') { // Extras with specific scopes
$permIds = DB::table('permissions')->pluck('id')->toArray(); $map['dashboard.view'] = 'branch';
} else { $map['dashboard.export'] = 'branch';
$permIds = DB::table('permissions')->whereIn('name', $perms)->pluck('id')->toArray(); $map['holidays.list'] = 'academy';
$map['settings.view'] = 'branch';
$map['users.list'] = 'branch';
$map['users.create'] = 'branch';
$map['users.update'] = 'branch';
return $map;
} }
// Sync: remove old, insert new private function headTrainerPermissions(): array
DB::table('permission_role')->where('role_id', $role->id)->delete(); {
$scope = in_array($roleSlug, ['super_admin', 'academy_owner', 'academy_admin']) ? 'all' : 'academy'; // Start with all trainer permissions
$inserts = array_map(fn ($pid) => [ $map = $this->trainerPermissions();
'role_id' => $role->id,
'permission_id' => $pid, // Additional permissions at branch scope
'scope' => $scope, $branchPerms = [
'created_at' => $now, 'sessions.cancel', 'sessions.reschedule', 'sessions.generate',
], $permIds); 'schedules.list', 'schedules.manage',
foreach (array_chunk($inserts, 100) as $chunk) { 'attendance.edit', 'attendance.export',
DB::table('permission_role')->insert($chunk); 'evaluations.manage', 'evaluations.approve', 'evaluations.share',
'groups.manage_enrollments',
'assignments.list', 'assignments.create',
'excuses.list', 'excuses.approve', 'excuses.reject',
'participants.view_attendance', 'participants.view_financial',
'trainers.list', 'trainers.show', 'trainers.manage_availability',
];
foreach ($branchPerms as $perm) {
$map[$perm] = 'branch';
} }
$this->command->info(" Role '{$roleSlug}': " . count($permIds) . ' permissions assigned.'); return $map;
} }
private function trainerPermissions(): array
{
return [
'dashboard.view' => 'academy',
'sessions.list' => 'own_groups',
'sessions.show' => 'own_groups',
'sessions.start' => 'own_groups',
'sessions.complete' => 'own_groups',
'attendance.list' => 'own_groups',
'attendance.mark' => 'own_groups',
'attendance.view_reports' => 'own_groups',
'groups.list' => 'own_groups',
'groups.show' => 'own_groups',
'evaluations.list' => 'own_groups',
'evaluations.create' => 'own_groups',
'evaluations.update' => 'own_groups',
'participants.list' => 'own_groups',
'participants.show' => 'own_groups',
'participants.view' => 'own_groups',
'participants.view_attendance' => 'own_groups',
'schedules.view' => 'own_groups',
'excuses.submit' => 'own_groups',
];
} }
private function getRolePermissions(array $allPermissions): array private function receptionistPermissions(): array
{ {
return [ return [
'super_admin' => '*', 'dashboard.view' => 'branch',
'academy_owner' => '*', 'participants.list' => 'branch',
'academy_admin' => array_filter($allPermissions, fn ($p) => !in_array($p, [ 'participants.show' => 'branch',
'settings.manage', 'roles.delete', 'participants.view' => 'branch',
])), 'participants.create' => 'branch',
'branch_manager' => array_filter($allPermissions, fn ($p) => 'participants.update' => 'branch',
str_starts_with($p, 'participants.') || 'guardians.list' => 'branch',
str_starts_with($p, 'guardians.') || 'guardians.create' => 'branch',
str_starts_with($p, 'activities.') || 'enrollments.list' => 'branch',
str_starts_with($p, 'programs.') || 'enrollments.create' => 'branch',
str_starts_with($p, 'groups.') || 'activities.list' => 'academy',
str_starts_with($p, 'sessions.') || 'programs.list' => 'academy',
str_starts_with($p, 'schedules.') || 'pos.access' => 'branch',
str_starts_with($p, 'enrollments.') || 'pos.sell' => 'branch',
str_starts_with($p, 'attendance.') || 'pos.list' => 'branch',
str_starts_with($p, 'excuses.') || 'invoices.list' => 'branch',
str_starts_with($p, 'invoices.') || 'invoices.show' => 'branch',
str_starts_with($p, 'payments.') || 'invoices.view' => 'branch',
str_starts_with($p, 'wallets.') || 'invoices.create' => 'branch',
str_starts_with($p, 'cash_sessions.') || 'payments.create' => 'branch',
str_starts_with($p, 'pricing.') || 'wallets.view' => 'branch',
str_starts_with($p, 'pos.') || 'cash_sessions.open' => 'branch',
str_starts_with($p, 'inventory.') || 'cash_sessions.close' => 'branch',
str_starts_with($p, 'products.') || 'cash_sessions.list' => 'branch',
str_starts_with($p, 'warehouses.') || 'attendance.list' => 'branch',
str_starts_with($p, 'facilities.') || 'schedules.view' => 'branch',
str_starts_with($p, 'reservations.') || ];
str_starts_with($p, 'assignments.') || }
str_starts_with($p, 'evaluations.') ||
str_starts_with($p, 'notifications.') || private function accountantPermissions(): array
str_starts_with($p, 'reports.') || {
str_starts_with($p, 'audit.') || $map = [
$p === 'dashboard.view' || $p === 'holidays.list' || 'dashboard.view' => 'academy',
$p === 'settings.view' 'participants.list' => 'academy',
), 'participants.show' => 'academy',
'head_trainer' => [ 'participants.view' => 'academy',
'dashboard.view', 'enrollments.list' => 'academy',
'sessions.list', 'sessions.show', 'sessions.start', 'sessions.complete', 'programs.list' => 'academy',
'sessions.cancel', 'sessions.reschedule', 'sessions.generate', 'pricing.list' => 'academy',
'schedules.list', 'schedules.manage', ];
'attendance.list', 'attendance.mark', 'attendance.edit', 'attendance.view_reports',
'groups.list', 'groups.show', // Full financial suite at academy scope
'evaluations.list', 'evaluations.create', 'evaluations.update', $financialPerms = [
'participants.list', 'participants.show', 'participants.view_attendance',
'excuses.list', 'excuses.approve', 'excuses.reject',
'assignments.list',
],
'trainer' => [
'dashboard.view',
'sessions.list', 'sessions.show', 'sessions.start', 'sessions.complete',
'attendance.list', 'attendance.mark',
'groups.list', 'groups.show',
'evaluations.list', 'evaluations.create',
'participants.list', 'participants.show',
],
'receptionist' => [
'dashboard.view',
'participants.list', 'participants.show', 'participants.create',
'guardians.list', 'guardians.create',
'enrollments.create',
'pos.access', 'pos.sell', 'pos.list',
'invoices.show',
'payments.create',
'cash_sessions.open', 'cash_sessions.close', 'cash_sessions.list',
],
'accountant' => [
'dashboard.view',
'invoices.list', 'invoices.create', 'invoices.update', 'invoices.cancel', 'invoices.list', 'invoices.create', 'invoices.update', 'invoices.cancel',
'invoices.show', 'invoices.send', 'invoices.export', 'invoices.show', 'invoices.view', 'invoices.send', 'invoices.export',
'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',
...@@ -260,18 +365,52 @@ private function getRolePermissions(array $allPermissions): array ...@@ -260,18 +365,52 @@ private function getRolePermissions(array $allPermissions): array
'refunds.initiate', 'refunds.approve', 'refunds.initiate', 'refunds.approve',
'daily_closing.create', 'daily_closing.view', 'daily_closing.create', 'daily_closing.view',
'reports.financial', 'reports.view', 'reports.export_pdf', 'reports.export_excel', 'reports.financial', 'reports.view', 'reports.export_pdf', 'reports.export_excel',
'pricing.list', ];
], foreach ($financialPerms as $perm) {
'data_entry' => [ $map[$perm] = 'academy';
'dashboard.view', }
'participants.list', 'participants.create', 'participants.update',
'guardians.list', 'guardians.create', 'guardians.update', return $map;
'people.list', 'people.create', 'people.update', }
],
'parent' => [ private function dataEntryPermissions(): array
'dashboard.view', {
'attendance.list', return [
], 'dashboard.view' => 'academy',
'participants.list' => 'branch',
'participants.create' => 'branch',
'participants.update' => 'branch',
'participants.show' => 'branch',
'participants.view' => 'branch',
'guardians.list' => 'branch',
'guardians.create' => 'branch',
'guardians.update' => 'branch',
'people.list' => 'branch',
'people.create' => 'branch',
'people.update' => 'branch',
'activities.list' => 'academy',
'programs.list' => 'academy',
'groups.list' => 'branch',
'enrollments.list' => 'branch',
'enrollments.create' => 'branch',
];
}
private function parentPermissions(): array
{
return [
'dashboard.view' => 'academy',
'attendance.list' => 'own_children',
'participants.view' => 'own_children',
'participants.show' => 'own_children',
'evaluations.list' => 'own_children',
'invoices.list' => 'own_children',
'invoices.view' => 'own_children',
'payments.list' => 'own_children',
'wallets.view' => 'own_children',
'programs.list' => 'academy',
'schedules.view' => 'own_children',
'excuses.submit' => 'own_children',
]; ];
} }
} }
@php @php
$navigation = [ $navigation = [
['label' => 'لوحة التحكم', 'route' => 'dashboard', 'icon' => 'home', 'permission' => 'dashboard.view'], ['label' => 'لوحة التحكم', 'route' => 'dashboard', 'icon' => 'home', 'permission' => 'dashboard.view'],
['label' => 'بوابة ولي الأمر', 'route' => 'guardian.dashboard', 'icon' => 'user-group', 'permission' => 'dashboard.view', 'role' => 'parent'],
['label' => 'مكتب الاستقبال', 'route' => 'receptionist.dashboard', 'icon' => 'reception', 'permission' => 'participants.list'], ['label' => 'مكتب الاستقبال', 'route' => 'receptionist.dashboard', 'icon' => 'reception', 'permission' => 'participants.list'],
['section' => 'المشاركين', 'items' => [ ['section' => 'المشاركين', 'items' => [
...@@ -27,6 +28,11 @@ ...@@ -27,6 +28,11 @@
['label' => 'لوحة المدرب', 'route' => 'trainer.dashboard', 'icon' => 'user', 'permission' => 'attendance.mark'], ['label' => 'لوحة المدرب', 'route' => 'trainer.dashboard', 'icon' => 'user', 'permission' => 'attendance.mark'],
]], ]],
['section' => 'الموارد البشرية', 'items' => [
['label' => 'الموظفين', 'route' => 'employees.list', 'icon' => 'briefcase', 'permission' => 'employees.list'],
['label' => 'المدربين', 'route' => 'trainers.list', 'icon' => 'academic-cap', 'permission' => 'trainers.list'],
]],
['section' => 'المالية', 'items' => [ ['section' => 'المالية', 'items' => [
['label' => 'النظرة المالية', 'route' => 'financial.overview', 'icon' => 'chart-bar', 'permission' => 'invoices.list'], ['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'],
...@@ -69,7 +75,7 @@ ...@@ -69,7 +75,7 @@
['label' => 'التقارير', 'route' => 'reports.view', 'icon' => 'chart-bar', 'permission' => 'reports.view'], ['label' => 'التقارير', 'route' => 'reports.view', 'icon' => 'chart-bar', 'permission' => 'reports.view'],
['label' => 'المستخدمين', 'route' => 'users.list', 'icon' => 'users', 'permission' => 'users.list'], ['label' => 'المستخدمين', 'route' => 'users.list', 'icon' => 'users', 'permission' => 'users.list'],
['label' => 'الأدوار', 'route' => 'roles.list', 'icon' => 'shield-check', 'permission' => 'roles.list'], ['label' => 'الأدوار', 'route' => 'roles.list', 'icon' => 'shield-check', 'permission' => 'roles.list'],
['label' => 'الفروع', 'route' => 'branches.list', 'icon' => 'building-office', 'permission' => 'settings.manage'], ['label' => 'الفروع', 'route' => 'branches.list', 'icon' => 'building-office', 'permission' => 'branches.list'],
['label' => 'سجل المراجعة', 'route' => 'audit.list', 'icon' => 'eye', 'permission' => 'audit.list'], ['label' => 'سجل المراجعة', 'route' => 'audit.list', 'icon' => 'eye', 'permission' => 'audit.list'],
['label' => 'إعدادات الأكاديمية', 'route' => 'settings.academy', 'icon' => 'cog-6-tooth', 'permission' => 'settings.manage'], ['label' => 'إعدادات الأكاديمية', 'route' => 'settings.academy', 'icon' => 'cog-6-tooth', 'permission' => 'settings.manage'],
['label' => 'الهوية البصرية', 'route' => 'settings.branding', 'icon' => 'swatch', 'permission' => 'settings.manage'], ['label' => 'الهوية البصرية', 'route' => 'settings.branding', 'icon' => 'swatch', 'permission' => 'settings.manage'],
...@@ -117,6 +123,7 @@ ...@@ -117,6 +123,7 @@
'chat' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>', 'chat' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>',
'user' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>', 'user' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>',
'receipt-percent' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2zM10 8.5a.5.5 0 11-1 0 .5.5 0 011 0zm5 5a.5.5 0 11-1 0 .5.5 0 011 0z"/>', 'receipt-percent' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 14l6-6m-5.5.5h.01m4.99 5h.01M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16l3.5-2 3.5 2 3.5-2 3.5 2zM10 8.5a.5.5 0 11-1 0 .5.5 0 011 0zm5 5a.5.5 0 11-1 0 .5.5 0 011 0z"/>',
'briefcase' => '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7H4a2 2 0 00-2 2v10a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2zM16 7V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v2"/>',
]; ];
$permissionService = app(\App\Domain\Identity\Services\PermissionService::class); $permissionService = app(\App\Domain\Identity\Services\PermissionService::class);
...@@ -125,6 +132,18 @@ ...@@ -125,6 +132,18 @@
$userCan = function (string $permission) use ($permissionService, $currentUser): bool { $userCan = function (string $permission) use ($permissionService, $currentUser): bool {
return $permissionService->can($currentUser, $permission); return $permissionService->can($currentUser, $permission);
}; };
$userRole = $currentUser->primaryRole?->slug ?? $currentUser->roles->first()?->slug;
$itemVisible = function (array $item) use ($userCan, $userRole): bool {
if (!Route::has($item['route']) || !$userCan($item['permission'])) {
return false;
}
if (isset($item['role']) && $userRole !== $item['role']) {
return false;
}
return true;
};
@endphp @endphp
<aside dir="rtl" <aside dir="rtl"
...@@ -152,7 +171,7 @@ class="fixed top-0 start-0 h-screen w-64 flex flex-col z-40 overflow-hidden tran ...@@ -152,7 +171,7 @@ class="fixed top-0 start-0 h-screen w-64 flex flex-col z-40 overflow-hidden tran
@foreach($navigation as $item) @foreach($navigation as $item)
{{-- Top-level link (no section) --}} {{-- Top-level link (no section) --}}
@if(isset($item['route'])) @if(isset($item['route']))
@if(Route::has($item['route']) && $userCan($item['permission'])) @if($itemVisible($item))
<a href="{{ route($item['route']) }}" <a href="{{ route($item['route']) }}"
class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-150 hover:bg-white/10 active:bg-white/20 active:scale-[0.97]" class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-150 hover:bg-white/10 active:bg-white/20 active:scale-[0.97]"
style="{{ request()->routeIs($item['route'] . '*') ? 'background-color: var(--brand-sidebar-active, #2563eb); color: #fff;' : 'color: var(--brand-sidebar-text, #e2e8f0);' }}"> style="{{ request()->routeIs($item['route'] . '*') ? 'background-color: var(--brand-sidebar-active, #2563eb); color: #fff;' : 'color: var(--brand-sidebar-text, #e2e8f0);' }}">
...@@ -164,8 +183,8 @@ class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transi ...@@ -164,8 +183,8 @@ class="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transi
{{-- Section with items --}} {{-- Section with items --}}
@elseif(isset($item['section'])) @elseif(isset($item['section']))
@php @php
$visibleItems = collect($item['items'])->filter(function ($child) use ($userCan) { $visibleItems = collect($item['items'])->filter(function ($child) use ($itemVisible) {
return Route::has($child['route']) && $userCan($child['permission']); return $itemVisible($child);
}); });
@endphp @endphp
......
<div>
<div class="max-w-3xl mx-auto">
{{-- Header --}}
<div class="flex items-center gap-3 mb-6">
<a href="{{ route('employees.list') }}" wire:navigate class="text-gray-400 hover:text-gray-600">
<svg 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="M11 17l5-5-5-5M18 12H4"/></svg>
</a>
<h1 class="text-2xl font-bold text-gray-900">{{ $editing ? __('تعديل موظف') : __('إضافة موظف جديد') }}</h1>
</div>
{{-- Flash messages --}}
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">{{ session('error') }}</div>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Person Selection --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('الشخص') }}</h2>
@if($personId)
<div class="flex items-center justify-between p-3 bg-blue-50 border border-blue-200 rounded-lg">
<span class="font-medium text-blue-900">{{ $selectedPersonName }}</span>
@unless($editing)
<button type="button" wire:click="$set('personId', null)" class="text-sm text-blue-600 hover:text-blue-800">{{ __('تغيير') }}</button>
@endunless
</div>
@else
<div class="relative">
<input type="text" wire:model.live.debounce.300ms="personSearch"
placeholder="{{ __('ابحث بالاسم أو الهاتف أو الرقم القومي...') }}"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
@if(count($searchResults) > 0)
<div class="absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
@foreach($searchResults as $person)
<button type="button" wire:click="selectPerson({{ $person->id }})"
class="w-full px-4 py-3 text-start hover:bg-gray-50 border-b border-gray-100 last:border-0">
<p class="font-medium text-gray-900">{{ $person->name_ar }}</p>
<p class="text-xs text-gray-500 mt-0.5">{{ $person->phone ?? '' }} {{ $person->national_id ? '- ' . $person->national_id : '' }}</p>
</button>
@endforeach
</div>
@endif
</div>
@endif
@error('personId') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
{{-- Employment Details --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('بيانات التوظيف') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('نوع التوظيف') }} <span class="text-red-500">*</span></label>
<select wire:model="employmentType" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
@foreach($employmentTypes as $t)
<option value="{{ $t->value }}">{{ $t->label() }}</option>
@endforeach
</select>
@error('employmentType') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الفرع') }}</label>
<select wire:model="branchId" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('-- اختر --') }}</option>
@foreach($branches as $b)
<option value="{{ $b->id }}">{{ $b->name_ar }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('القسم') }}</label>
<input type="text" wire:model="department" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="{{ __('مثال: التدريب') }}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المنصب') }}</label>
<input type="text" wire:model="position" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="{{ __('مثال: مدرب أول') }}">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('تاريخ البداية') }} <span class="text-red-500">*</span></label>
<input type="date" wire:model="startDate" dir="ltr" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
@error('startDate') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('تاريخ الانتهاء') }}</label>
<input type="date" wire:model="endDate" dir="ltr" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المدير المباشر') }}</label>
<select wire:model="managerId" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('-- بدون --') }}</option>
@foreach($managers as $m)
<option value="{{ $m->id }}">{{ $m->person->name_ar ?? $m->employee_number }}</option>
@endforeach
</select>
</div>
</div>
</div>
{{-- Salary --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('الراتب') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('المبلغ (ج.م)') }}</label>
<input type="number" wire:model="salaryAmount" step="0.01" min="0" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="0.00">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('دورة الصرف') }}</label>
<select wire:model="salaryFrequency" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('-- اختر --') }}</option>
@foreach($salaryFrequencies as $f)
<option value="{{ $f->value }}">{{ $f->label() }}</option>
@endforeach
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ساعات العمل/أسبوع') }}</label>
<input type="number" wire:model="workingHoursPerWeek" step="0.5" min="0" max="168" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500"
placeholder="40">
</div>
</div>
</div>
{{-- Notes --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('ملاحظات') }}</label>
<textarea wire:model="notes" rows="3" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="{{ __('ملاحظات إضافية...') }}"></textarea>
</div>
{{-- Submit --}}
<div class="flex items-center justify-end gap-3">
<a href="{{ route('employees.list') }}" wire:navigate class="px-4 py-2.5 text-sm text-gray-600 hover:text-gray-800 rounded-lg hover:bg-gray-100">{{ __('إلغاء') }}</a>
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="px-6 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50">
<span wire:loading.remove wire:target="save">{{ $editing ? __('حفظ التغييرات') : __('إضافة الموظف') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</form>
</div>
</div>
<div>
{{-- Header --}}
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h1 class="text-2xl font-bold text-gray-900">{{ __('الموظفين') }}</h1>
@can('employees.create')
<a href="{{ route('employees.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg 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 4v16m8-8H4"/></svg>
{{ __('إضافة موظف') }}
</a>
@endcan
</div>
{{-- Flash messages --}}
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-800 text-sm">{{ session('success') }}</div>
@endif
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث بالاسم أو الرقم الوظيفي...') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<select wire:model.live="status" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('كل الحالات') }}</option>
@foreach($statuses as $s)
<option value="{{ $s->value }}">{{ $s->label() }}</option>
@endforeach
</select>
<select wire:model.live="employmentType" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('كل أنواع التوظيف') }}</option>
@foreach($employmentTypes as $t)
<option value="{{ $t->value }}">{{ $t->label() }}</option>
@endforeach
</select>
<select wire:model.live="branchId" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('كل الفروع') }}</option>
@foreach($branches as $b)
<option value="{{ $b->id }}">{{ $b->name_ar }}</option>
@endforeach
</select>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto" wire:loading.class="opacity-50 pointer-events-none">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('الموظف') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('الرقم الوظيفي') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('المنصب') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('نوع التوظيف') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('الفرع') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('إجراءات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($employees as $emp)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3 font-medium text-gray-900">{{ $emp->person->name_ar ?? '-' }}</td>
<td class="px-4 py-3 text-gray-600 font-mono text-xs">{{ $emp->employee_number }}</td>
<td class="px-4 py-3 text-gray-600">{{ $emp->position ?? '-' }}</td>
<td class="px-4 py-3">
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-700">
{{ $emp->employment_type->label() }}
</span>
</td>
<td class="px-4 py-3 text-gray-600">{{ $emp->branch?->name_ar ?? '-' }}</td>
<td class="px-4 py-3">
@php
$statusColors = [
'active' => 'bg-green-100 text-green-800',
'on_leave' => 'bg-yellow-100 text-yellow-800',
'suspended' => 'bg-red-100 text-red-800',
'terminated' => 'bg-gray-100 text-gray-800',
'resigned' => 'bg-gray-100 text-gray-600',
];
@endphp
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$emp->status->value] ?? 'bg-gray-100 text-gray-600' }}">
{{ $emp->status->label() }}
</span>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
@can('employees.update')
<a href="{{ route('employees.edit', $emp) }}" wire:navigate
class="text-blue-600 hover:text-blue-800 text-xs font-medium">{{ __('تعديل') }}</a>
@endcan
@can('employees.delete')
<button wire:click="delete('{{ $emp->uuid }}')"
wire:confirm="{{ __('هل أنت متأكد من حذف هذا الموظف؟') }}"
class="text-red-600 hover:text-red-800 text-xs font-medium">{{ __('حذف') }}</button>
@endcan
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-12 text-center text-gray-500">
<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="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
<p class="font-medium">{{ __('لا يوجد موظفين') }}</p>
<p class="text-sm text-gray-400 mt-1">{{ __('ابدأ بإضافة موظف جديد') }}</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($employees->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $employees->links() }}
</div>
@endif
</div>
</div>
<div>
<div class="max-w-3xl mx-auto">
{{-- Header --}}
<div class="flex items-center gap-3 mb-6">
<a href="{{ route('trainers.list') }}" wire:navigate class="text-gray-400 hover:text-gray-600">
<svg 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="M11 17l5-5-5-5M18 12H4"/></svg>
</a>
<h1 class="text-2xl font-bold text-gray-900">{{ $editing ? __('تعديل مدرب') : __('إضافة مدرب جديد') }}</h1>
</div>
{{-- Flash messages --}}
@if(session('error'))
<div class="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-800 text-sm">{{ session('error') }}</div>
@endif
<form wire:submit="save" class="space-y-6">
{{-- Employee Selection --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('الموظف') }}</h2>
@if($employeeId)
<div class="flex items-center justify-between p-3 bg-blue-50 border border-blue-200 rounded-lg">
<span class="font-medium text-blue-900">{{ $selectedEmployeeName }}</span>
@unless($editing)
<button type="button" wire:click="$set('employeeId', null)" class="text-sm text-blue-600 hover:text-blue-800">{{ __('تغيير') }}</button>
@endunless
</div>
@else
<div class="relative">
<input type="text" wire:model.live.debounce.300ms="employeeSearch"
placeholder="{{ __('ابحث عن موظف (يجب أن يكون موظف أولاً)...') }}"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
@if(count($searchResults) > 0)
<div class="absolute z-10 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
@foreach($searchResults as $emp)
<button type="button" wire:click="selectEmployee({{ $emp->id }})"
class="w-full px-4 py-3 text-start hover:bg-gray-50 border-b border-gray-100 last:border-0">
<p class="font-medium text-gray-900">{{ $emp->person->name_ar }}</p>
<p class="text-xs text-gray-500 mt-0.5">{{ $emp->employee_number }} - {{ $emp->position ?? $emp->employment_type->label() }}</p>
</button>
@endforeach
</div>
@endif
</div>
<p class="text-xs text-gray-500 mt-2">{{ __('يجب إضافة الشخص كموظف أولاً من صفحة الموظفين') }}</p>
@endif
@error('employeeId') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
{{-- Compensation Model --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('نموذج التعويض') }}</h2>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('نموذج الدفع') }} <span class="text-red-500">*</span></label>
<select wire:model.live="compensationModel" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
@foreach($compensationModels as $cm)
<option value="{{ $cm->value }}">{{ $cm->label() }}</option>
@endforeach
</select>
@error('compensationModel') <p class="text-sm text-red-600 mt-1">{{ $message }}</p> @enderror
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
@if(in_array($compensationModel, ['hourly', 'hybrid']))
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('سعر الساعة (ج.م)') }}</label>
<input type="number" wire:model="hourlyRate" step="0.01" min="0" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="0.00">
</div>
@endif
@if(in_array($compensationModel, ['per_session', 'hybrid']))
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('سعر الحصة (ج.م)') }}</label>
<input type="number" wire:model="sessionRate" step="0.01" min="0" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="0.00">
</div>
@endif
@if(in_array($compensationModel, ['per_group', 'hybrid']))
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('سعر المجموعة (ج.م)') }}</label>
<input type="number" wire:model="groupRate" step="0.01" min="0" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="0.00">
</div>
@endif
@if(in_array($compensationModel, ['per_player', 'hybrid']))
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('سعر اللاعب (ج.م)') }}</label>
<input type="number" wire:model="playerRate" step="0.01" min="0" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="0.00">
</div>
@endif
@if(in_array($compensationModel, ['revenue_share', 'hybrid']))
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('نسبة الإيرادات (%)') }}</label>
<input type="number" wire:model="revenueSharePercent" step="0.01" min="0" max="100" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="0.00">
</div>
@endif
</div>
</div>
{{-- Bio & Limits --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('معلومات إضافية') }}</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الحد الأقصى للحصص/يوم') }}</label>
<input type="number" wire:model="maxDailySessions" min="1" max="20" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="4">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('الحد الأقصى للساعات/أسبوع') }}</label>
<input type="number" wire:model="maxWeeklyHours" step="0.5" min="1" max="168" dir="ltr"
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="40">
</div>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('السيرة الذاتية (عربي)') }}</label>
<textarea wire:model="bioAr" rows="3" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="{{ __('نبذة عن المدرب...') }}"></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('السيرة الذاتية (إنجليزي)') }}</label>
<textarea wire:model="bio" rows="2" dir="ltr" class="w-full px-3 py-2.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500" placeholder="Bio in English..."></textarea>
</div>
</div>
{{-- Qualifications --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ __('المؤهلات والشهادات') }}</h2>
@if(count($qualifications) > 0)
<div class="space-y-2 mb-4">
@foreach($qualifications as $idx => $qual)
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
<div>
<p class="font-medium text-gray-900 text-sm">{{ $qual['name'] }}</p>
<p class="text-xs text-gray-500">
{{ $qual['issuer'] ?? '' }}
@if($qual['expiry_date']) — {{ __('تنتهي') }}: {{ $qual['expiry_date'] }} @endif
</p>
</div>
<button type="button" wire:click="removeQualification({{ $idx }})"
class="text-red-500 hover:text-red-700 text-xs">{{ __('حذف') }}</button>
</div>
@endforeach
</div>
@endif
<div class="border border-gray-200 rounded-lg p-3">
<p class="text-sm font-medium text-gray-600 mb-2">{{ __('إضافة شهادة') }}</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<input type="text" wire:model="newQualName" placeholder="{{ __('اسم الشهادة *') }}"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<input type="text" wire:model="newQualIssuer" placeholder="{{ __('الجهة المانحة') }}"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<input type="date" wire:model="newQualIssueDate" dir="ltr"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<input type="date" wire:model="newQualExpiryDate" dir="ltr"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
</div>
<button type="button" wire:click="addQualification"
class="mt-3 px-4 py-2 text-sm font-medium text-blue-600 border border-blue-300 rounded-lg hover:bg-blue-50">
{{ __('+ إضافة') }}
</button>
</div>
</div>
{{-- Submit --}}
<div class="flex items-center justify-end gap-3">
<a href="{{ route('trainers.list') }}" wire:navigate class="px-4 py-2.5 text-sm text-gray-600 hover:text-gray-800 rounded-lg hover:bg-gray-100">{{ __('إلغاء') }}</a>
<button type="submit" wire:loading.attr="disabled" wire:target="save"
class="px-6 py-2.5 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50">
<span wire:loading.remove wire:target="save">{{ $editing ? __('حفظ التغييرات') : __('إضافة المدرب') }}</span>
<span wire:loading wire:target="save">{{ __('جارٍ الحفظ...') }}</span>
</button>
</div>
</form>
</div>
</div>
<div>
{{-- Header --}}
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<h1 class="text-2xl font-bold text-gray-900">{{ __('المدربين') }}</h1>
@can('trainers.create')
<a href="{{ route('trainers.create') }}" wire:navigate
class="inline-flex items-center gap-2 px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg 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 4v16m8-8H4"/></svg>
{{ __('إضافة مدرب') }}
</a>
@endcan
</div>
{{-- Flash messages --}}
@if(session('success'))
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-800 text-sm">{{ session('success') }}</div>
@endif
{{-- Filters --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<input type="text" wire:model.live.debounce.300ms="search"
placeholder="{{ __('بحث بالاسم أو رقم المدرب...') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<select wire:model.live="status" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('كل الحالات') }}</option>
@foreach($statuses as $s)
<option value="{{ $s->value }}">{{ $s->label() }}</option>
@endforeach
</select>
<select wire:model.live="compensationModel" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500">
<option value="">{{ __('كل نماذج التعويض') }}</option>
@foreach($compensationModels as $cm)
<option value="{{ $cm->value }}">{{ $cm->label() }}</option>
@endforeach
</select>
</div>
</div>
{{-- Table --}}
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto" wire:loading.class="opacity-50 pointer-events-none">
<table class="w-full text-sm">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('المدرب') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('رقم المدرب') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('نموذج التعويض') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('التقييم') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('الحصص') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('الحالة') }}</th>
<th class="px-4 py-3 text-start font-semibold text-gray-600">{{ __('إجراءات') }}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
@forelse($trainers as $trainer)
<tr class="hover:bg-gray-50">
<td class="px-4 py-3">
<p class="font-medium text-gray-900">{{ $trainer->employee->person->name_ar ?? '-' }}</p>
<p class="text-xs text-gray-500 mt-0.5">{{ $trainer->employee->branch?->name_ar ?? '' }}</p>
</td>
<td class="px-4 py-3 text-gray-600 font-mono text-xs">{{ $trainer->trainer_number }}</td>
<td class="px-4 py-3">
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{{ $trainer->compensation_model->label() }}
</span>
</td>
<td class="px-4 py-3 text-gray-600">
@if($trainer->rating)
<span class="flex items-center gap-1">
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
{{ number_format($trainer->rating, 1) }}
</span>
@else
<span class="text-gray-400">-</span>
@endif
</td>
<td class="px-4 py-3 text-gray-600">{{ $trainer->total_sessions_conducted }}</td>
<td class="px-4 py-3">
@php
$statusColors = [
'active' => 'bg-green-100 text-green-800',
'inactive' => 'bg-gray-100 text-gray-600',
'on_leave' => 'bg-yellow-100 text-yellow-800',
'suspended' => 'bg-red-100 text-red-800',
];
@endphp
<span class="inline-flex px-2 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$trainer->status->value] ?? 'bg-gray-100 text-gray-600' }}">
{{ $trainer->status->label() }}
</span>
</td>
<td class="px-4 py-3">
@can('trainers.update')
<a href="{{ route('trainers.edit', $trainer) }}" wire:navigate
class="text-blue-600 hover:text-blue-800 text-xs font-medium">{{ __('تعديل') }}</a>
@endcan
</td>
</tr>
@empty
<tr>
<td colspan="7" class="px-4 py-12 text-center text-gray-500">
<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="M4.26 10.147a60.436 60.436 0 00-.491 6.347A48.627 48.627 0 0112 20.904a48.627 48.627 0 018.232-4.41 60.46 60.46 0 00-.491-6.347m-15.482 0a50.57 50.57 0 00-2.658-.813A59.905 59.905 0 0112 3.493a59.902 59.902 0 0110.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.697 50.697 0 0112 13.489a50.702 50.702 0 017.74-3.342"/></svg>
<p class="font-medium">{{ __('لا يوجد مدربين') }}</p>
<p class="text-sm text-gray-400 mt-1">{{ __('ابدأ بإضافة مدرب جديد') }}</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($trainers->hasPages())
<div class="px-4 py-3 border-t border-gray-200">
{{ $trainers->links() }}
</div>
@endif
</div>
</div>
...@@ -158,11 +158,11 @@ ...@@ -158,11 +158,11 @@
// Activities // Activities
Route::get('/activities', ActivityList::class)->name('activities.list') Route::get('/activities', ActivityList::class)->name('activities.list')
->middleware('permission:participants.list'); ->middleware('permission:activities.list');
Route::get('/activities/create', ActivityForm::class)->name('activities.create') Route::get('/activities/create', ActivityForm::class)->name('activities.create')
->middleware('permission:participants.create'); ->middleware('permission:activities.create');
Route::get('/activities/{activity}/edit', ActivityForm::class)->name('activities.edit') Route::get('/activities/{activity}/edit', ActivityForm::class)->name('activities.edit')
->middleware('permission:participants.update'); ->middleware('permission:activities.update');
// Training Programs // Training Programs
Route::get('/programs', ProgramList::class)->name('programs.list') Route::get('/programs', ProgramList::class)->name('programs.list')
...@@ -240,6 +240,22 @@ ...@@ -240,6 +240,22 @@
Route::get('/attendance/{session}', TakeAttendance::class)->name('attendance.take') Route::get('/attendance/{session}', TakeAttendance::class)->name('attendance.take')
->middleware('permission:attendance.mark'); ->middleware('permission:attendance.mark');
// HR - Employees
Route::get('/hr/employees', \App\Livewire\HR\EmployeeList::class)->name('employees.list')
->middleware('permission:employees.list');
Route::get('/hr/employees/create', \App\Livewire\HR\EmployeeForm::class)->name('employees.create')
->middleware('permission:employees.create');
Route::get('/hr/employees/{employee}/edit', \App\Livewire\HR\EmployeeForm::class)->name('employees.edit')
->middleware('permission:employees.update');
// HR - Trainers
Route::get('/hr/trainers', \App\Livewire\HR\TrainerList::class)->name('trainers.list')
->middleware('permission:trainers.list');
Route::get('/hr/trainers/create', \App\Livewire\HR\TrainerForm::class)->name('trainers.create')
->middleware('permission:trainers.create');
Route::get('/hr/trainers/{trainer}/edit', \App\Livewire\HR\TrainerForm::class)->name('trainers.edit')
->middleware('permission:trainers.update');
// Assignments // Assignments
Route::get('/assignments', AssignmentList::class)->name('assignments.list') Route::get('/assignments', AssignmentList::class)->name('assignments.list')
->middleware('permission:assignments.list'); ->middleware('permission:assignments.list');
...@@ -411,5 +427,6 @@ ...@@ -411,5 +427,6 @@
->middleware('permission:settings.manage'); ->middleware('permission:settings.manage');
// Guardian Portal // Guardian Portal
Route::get('/guardian', GuardianDashboard::class)->name('guardian.dashboard'); Route::get('/guardian', GuardianDashboard::class)->name('guardian.dashboard')
->middleware('permission:dashboard.view');
}); });
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