# Cycle 4: Identity & People Gaps

## Goal
Add missing `employees`, `trainers`, `branch_user` pivot, `contacts` table, and align existing tables with doc spec.

---

## New Migration: `2024_01_01_000051_create_employees_table.php`

```sql
CREATE TABLE employees (
    id                      BIGSERIAL PRIMARY KEY,
    uuid                    UUID NOT NULL UNIQUE,
    academy_id              BIGINT NOT NULL REFERENCES academies(id),
    person_id               BIGINT NOT NULL REFERENCES people(id),
    user_id                 BIGINT REFERENCES users(id),
    branch_id               BIGINT REFERENCES branches(id),
    employee_number         VARCHAR(20) NOT NULL,
    department              VARCHAR(50),
    position                VARCHAR(100) NOT NULL,
    employment_type         VARCHAR(20) NOT NULL,          -- full_time, part_time, contract, freelance
    start_date              DATE NOT NULL,
    end_date                DATE,
    salary_amount           BIGINT DEFAULT 0,               -- piasters
    salary_frequency        VARCHAR(20) DEFAULT 'monthly',  -- monthly, weekly, hourly, per_session
    working_hours_per_week  INTEGER,
    manager_id              BIGINT REFERENCES users(id),
    status                  VARCHAR(20) NOT NULL DEFAULT 'active',
    status_changed_at       TIMESTAMP,
    termination_reason      TEXT,
    notes                   TEXT,
    metadata                JSONB NOT NULL DEFAULT '{}',
    created_by              BIGINT NOT NULL REFERENCES users(id),
    created_at              TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at              TIMESTAMP NOT NULL DEFAULT NOW(),
    deleted_at              TIMESTAMP
);

ALTER TABLE employees ADD CONSTRAINT employees_employment_type_check
    CHECK (employment_type IN ('full_time', 'part_time', 'contract', 'freelance'));

ALTER TABLE employees ADD CONSTRAINT employees_salary_frequency_check
    CHECK (salary_frequency IN ('monthly', 'weekly', 'hourly', 'per_session'));

ALTER TABLE employees ADD CONSTRAINT employees_status_check
    CHECK (status IN ('active', 'on_leave', 'suspended', 'terminated', 'resigned'));

CREATE UNIQUE INDEX employees_academy_number ON employees(academy_id, employee_number);
CREATE UNIQUE INDEX employees_academy_person ON employees(academy_id, person_id);
```

### Model: `app/Domain/Identity/Models/Employee.php`
```php
class Employee extends Model
{
    use BelongsToAcademy, HasUuid, SoftDeletes, Auditable;

    protected $fillable = [
        'academy_id', 'person_id', 'user_id', 'branch_id', 'employee_number',
        'department', 'position', 'employment_type', 'start_date', 'end_date',
        'salary_amount', 'salary_frequency', 'working_hours_per_week', 'manager_id',
        'status', 'status_changed_at', 'termination_reason', 'notes', 'metadata', 'created_by',
    ];

    protected $casts = [
        'salary_amount' => 'integer',
        'start_date' => 'date',
        'end_date' => 'date',
        'status_changed_at' => 'datetime',
        'metadata' => 'array',
    ];

    // Relationships: person(), user(), branch(), manager(), creator(), trainerRecord()
}
```

### Service: `app/Domain/Identity/Services/EmployeeService.php`
- `create(array $data, User $actor)` — validate person exists, generate employee_number
- `update(Employee $employee, array $data, User $actor)`
- `terminate(Employee $employee, string $reason, User $actor)` — status → terminated, set end_date
- `changeStatus(Employee $employee, string $newStatus, User $actor)` — enforce transitions

---

## New Migration: `2024_01_01_000052_create_trainers_tables.php`

```sql
CREATE TABLE trainers (
    id                      BIGSERIAL PRIMARY KEY,
    uuid                    UUID NOT NULL UNIQUE,
    academy_id              BIGINT NOT NULL REFERENCES academies(id),
    employee_id             BIGINT NOT NULL REFERENCES employees(id),
    specializations         JSONB NOT NULL DEFAULT '[]',    -- activity IDs or names
    compensation_type       VARCHAR(20) NOT NULL DEFAULT 'salary', -- salary, per_session, per_hour, mixed
    per_session_rate        BIGINT DEFAULT 0,               -- piasters
    per_hour_rate           BIGINT DEFAULT 0,               -- piasters
    max_daily_sessions      INTEGER DEFAULT 4,
    max_weekly_hours        INTEGER DEFAULT 40,
    rating                  DECIMAL(3,2) DEFAULT 0.00,      -- 0.00-5.00
    total_sessions_taught   INTEGER NOT NULL DEFAULT 0,
    is_active               BOOLEAN NOT NULL DEFAULT TRUE,
    notes                   TEXT,
    metadata                JSONB NOT NULL DEFAULT '{}',
    created_at              TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at              TIMESTAMP NOT NULL DEFAULT NOW()
);

ALTER TABLE trainers ADD CONSTRAINT trainers_compensation_type_check
    CHECK (compensation_type IN ('salary', 'per_session', 'per_hour', 'mixed'));

CREATE UNIQUE INDEX trainers_academy_employee ON trainers(academy_id, employee_id);

CREATE TABLE trainer_qualifications (
    id                  BIGSERIAL PRIMARY KEY,
    trainer_id          BIGINT NOT NULL REFERENCES trainers(id) ON DELETE CASCADE,
    type                VARCHAR(30) NOT NULL,          -- certification, license, degree, course
    name                VARCHAR(200) NOT NULL,
    name_ar             VARCHAR(200),
    issuing_authority   VARCHAR(200),
    issue_date          DATE,
    expiry_date         DATE,
    document_path       VARCHAR(500),
    is_verified         BOOLEAN NOT NULL DEFAULT FALSE,
    verified_by         BIGINT REFERENCES users(id),
    verified_at         TIMESTAMP,
    notes               TEXT,
    created_at          TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMP NOT NULL DEFAULT NOW()
);

ALTER TABLE trainer_qualifications ADD CONSTRAINT trainer_qualifications_type_check
    CHECK (type IN ('certification', 'license', 'degree', 'course', 'first_aid', 'other'));

CREATE TABLE trainer_availability (
    id                  BIGSERIAL PRIMARY KEY,
    trainer_id          BIGINT NOT NULL REFERENCES trainers(id) ON DELETE CASCADE,
    day_of_week         SMALLINT NOT NULL CHECK (day_of_week BETWEEN 0 AND 6),
    start_time          TIME NOT NULL,
    end_time            TIME NOT NULL,
    is_available        BOOLEAN NOT NULL DEFAULT TRUE,
    preference          VARCHAR(20) DEFAULT 'available', -- available, preferred, unavailable
    effective_from      DATE,
    effective_until     DATE,
    notes               VARCHAR(200),
    created_at          TIMESTAMP NOT NULL DEFAULT NOW()
);

ALTER TABLE trainer_availability ADD CONSTRAINT trainer_availability_preference_check
    CHECK (preference IN ('available', 'preferred', 'unavailable'));

CREATE UNIQUE INDEX trainer_availability_unique
    ON trainer_availability(trainer_id, day_of_week, start_time)
    WHERE effective_until IS NULL OR effective_until >= CURRENT_DATE;
```

### Models
- `Trainer`: BelongsToAcademy, HasUuid, Auditable; relations: employee(), qualifications(), availability(), assignments(), groups()
- `TrainerQualification`: trainer() BelongsTo, verifiedBy() BelongsTo
- `TrainerAvailability`: trainer() BelongsTo; scope: forDay(), available()

### Service: `app/Domain/Identity/Services/TrainerService.php`
- `create(Employee $employee, array $data, User $actor)` — create trainer record linked to employee
- `update(Trainer $trainer, array $data, User $actor)`
- `addQualification(Trainer $trainer, array $data, User $actor)`
- `verifyQualification(TrainerQualification $qual, User $actor)`
- `setAvailability(Trainer $trainer, array $slots)` — replace availability for given days
- `getAvailableTrainers(int $dayOfWeek, string $startTime, string $endTime)` — for scheduling
- `checkConflicts(Trainer $trainer, Carbon $date, string $start, string $end)` — existing sessions overlap
- `incrementSessionCount(Trainer $trainer)` — after session completed

---

## New Migration: `2024_01_01_000053_create_branch_user_pivot.php`

```sql
CREATE TABLE branch_user (
    id          BIGSERIAL PRIMARY KEY,
    branch_id   BIGINT NOT NULL REFERENCES branches(id) ON DELETE CASCADE,
    user_id     BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    is_primary  BOOLEAN NOT NULL DEFAULT FALSE,
    created_at  TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE UNIQUE INDEX branch_user_unique ON branch_user(branch_id, user_id);
CREATE INDEX branch_user_user_idx ON branch_user(user_id);
```

### Usage
- User model: `branches() BelongsToMany` with `->withPivot('is_primary')`
- Branch model: `users() BelongsToMany`
- PermissionService::applyScope() uses this to determine branch-level access
- When scope = 'branch': `WHERE branch_id IN (user's branch_ids)`

---

## New Migration: `2024_01_01_000054_create_contacts_table.php`

```sql
CREATE TABLE contacts (
    id              BIGSERIAL PRIMARY KEY,
    contactable_type VARCHAR(100) NOT NULL,
    contactable_id  BIGINT NOT NULL,
    type            VARCHAR(20) NOT NULL,
    label           VARCHAR(50),
    value           VARCHAR(200) NOT NULL,
    is_primary      BOOLEAN NOT NULL DEFAULT FALSE,
    is_verified     BOOLEAN NOT NULL DEFAULT FALSE,
    verified_at     TIMESTAMP,
    sort_order      INTEGER NOT NULL DEFAULT 0,
    created_at      TIMESTAMP NOT NULL DEFAULT NOW()
);

ALTER TABLE contacts ADD CONSTRAINT contacts_type_check
    CHECK (type IN ('phone', 'email', 'whatsapp', 'address', 'emergency', 'social'));

CREATE INDEX contacts_contactable ON contacts(contactable_type, contactable_id);
CREATE INDEX contacts_type_value ON contacts(type, value);
```

### Model: `app/Domain/Identity/Models/Contact.php`
- No academy_id (polymorphic, gets tenant from parent)
- MorphTo: contactable()
- Scopes: ofType($type), primary(), verified()

### Trait: `HasContacts` (added to Person, Guardian, Academy)
```php
public function contacts(): MorphMany
{
    return $this->morphMany(Contact::class, 'contactable');
}

public function primaryPhone(): ?string
{
    return $this->contacts()->where('type', 'phone')->where('is_primary', true)->value('value');
}
```

---

## Migration: `2024_01_01_000055_add_missing_identity_columns.php`

### On `academies`:
```php
$table->string('country_code', 2)->default('EG');
$table->string('tax_number', 50)->nullable();
$table->text('address')->nullable();
$table->string('subscription_plan', 20)->default('trial');
$table->timestamp('subscription_expires_at')->nullable();
```
CHECK: `subscription_plan IN ('trial', 'basic', 'professional', 'enterprise')`

### On `branches`:
```php
$table->string('status', 20)->default('active');
```
CHECK: `status IN ('active', 'inactive', 'closed')`
(Consider keeping `is_active` but adding 'closed' state via status)

### On `users`:
```php
$table->string('status_locked', 20)->nullable(); // for 'locked' state
$table->timestamp('locked_at')->nullable();
$table->string('lock_reason')->nullable();
$table->timestamp('phone_verified_at')->nullable();
```

### On `roles`:
```php
$table->uuid('uuid')->unique();
$table->boolean('is_default')->default(false);
```
Make `academy_id` nullable (for system-level roles).

### On `guardian_participant`:
```php
$table->boolean('can_authorize_medical')->default(false);
```

---

## Livewire Components to Create

### Employee Management
- `app/Livewire/Employees/EmployeeList.php` — WithPagination, search, status/department/branch filters
- `app/Livewire/Employees/EmployeeForm.php` — create/edit with person lookup, position, department, salary
- `app/Livewire/Employees/EmployeeShow.php` — profile with qualifications, assignments, attendance

### Trainer Management
- `app/Livewire/Trainers/TrainerList.php` — filters: activity, availability, rating
- `app/Livewire/Trainers/TrainerForm.php` — linked to employee, specializations, compensation
- `app/Livewire/Trainers/TrainerShow.php` — profile with qualifications, schedule, groups, sessions taught

### Views (Arabic-first, RTL)
- `resources/views/livewire/employees/employee-list.blade.php`
- `resources/views/livewire/employees/employee-form.blade.php`
- `resources/views/livewire/employees/employee-show.blade.php`
- `resources/views/livewire/trainers/trainer-list.blade.php`
- `resources/views/livewire/trainers/trainer-form.blade.php`
- `resources/views/livewire/trainers/trainer-show.blade.php`

### Routes
```php
Route::middleware(['auth', 'permission:employees.list'])->group(function () {
    Route::get('/employees', EmployeeList::class)->name('employees.index');
    Route::get('/employees/create', EmployeeForm::class)->name('employees.create');
    Route::get('/employees/{employee}/edit', EmployeeForm::class)->name('employees.edit');
    Route::get('/employees/{employee}', EmployeeShow::class)->name('employees.show');
});

Route::middleware(['auth', 'permission:trainers.list'])->group(function () {
    Route::get('/trainers', TrainerList::class)->name('trainers.index');
    Route::get('/trainers/create', TrainerForm::class)->name('trainers.create');
    Route::get('/trainers/{trainer}/edit', TrainerForm::class)->name('trainers.edit');
    Route::get('/trainers/{trainer}', TrainerShow::class)->name('trainers.show');
});
```

### Sidebar Addition
Under "الموارد البشرية" (HR) section:
- الموظفون (Employees)
- المدربون (Trainers)

### Permissions to Seed
```
employees.list, employees.create, employees.update, employees.delete, employees.show
trainers.list, trainers.create, trainers.update, trainers.delete, trainers.show
trainers.manage_qualifications, trainers.manage_availability
```

---

## Impact on Existing Code

1. **training_groups.head_trainer_id** — Currently FK to users. After this cycle, consider linking to trainers table instead (or maintain users FK and validate the user has a trainer record).

2. **training_schedules.trainer_id / assistant_trainer_id** — Same consideration.

3. **AssignmentService** — Update to validate assignee has a trainer record when assigning to training roles.

4. **SessionGeneratorService** — Can now check trainer availability before generating sessions.

5. **AttendanceGenerationService** — Already handles subject_type polymorphic; trainers attendance will work if we generate records for Trainer model.

---

## Verification

- [ ] `php artisan migrate` creates all new tables
- [ ] Create employee → person linked correctly
- [ ] Create trainer → employee linked → qualifications added
- [ ] Trainer availability blocks session generation for unavailable times
- [ ] branch_user pivot allows multi-branch access
- [ ] Contacts polymorphic works on Person and Guardian
- [ ] Sidebar shows HR section with proper permissions
