# Mediate Laravel Platform - Architecture Map

**Last Updated:** May 26, 2026  
**Purpose:** Comprehensive guide for module development patterns and best practices  
**Target Use:** Serves as template for implementing new modules (e.g., Gallery Module)

---

## Table of Contents

1. [Project Overview](#project-overview)
2. [Module Structure Pattern](#module-structure-pattern)
3. [Model Architecture](#model-architecture)
4. [Service Layer](#service-layer)
5. [Controller Patterns](#controller-patterns)
6. [Route Organization](#route-organization)
7. [Blade Template Patterns](#blade-template-patterns)
8. [Permission & Authorization](#permission--authorization)
9. [Database Migrations](#database-migrations)
10. [File Handling](#file-handling)
11. [Existing Complex Modules](#existing-complex-modules)
12. [Quick Reference Checklist](#quick-reference-checklist)

---

## Project Overview

The Mediate platform is a comprehensive Laravel-based system managing multiple interconnected features:

- **Self-Paced Learning Portal** - courses, enrollments, exams, certificates
- **Training Management** - packages, registrations, sessions, attendance, certificates
- **Membership System** - applications, renewals, transactions, certificates
- **Mentorship** - programs, assignments, sessions
- **Knowledge Sharing** - research, publications, forums, discussions
- **Organizational Management** - staff history, leadership positions, achievements
- **Digital Machines** - listings, payments, categories
- **Partnerships & Advocacy**
- **Chat & Support** - tickets, discussions, knowledge base

**Key Stats:**
- 80+ Models
- 30+ Controllers (organized by module)
- 45+ Services (business logic layer)
- 80+ Route files (organized by feature)
- 550+ Blade templates
- Permission-based authorization (Spatie)

---

## Module Structure Pattern

### Directory Organization

```
app/
├── Models/                      # All data models
│   ├── [Module]/               # Feature-specific folders
│   │   ├── Course.php
│   │   ├── Enrollment.php
│   │   └── CourseMedia.php
│   ├── LeadershipHistory.php
│   ├── TrainingPackage.php
│   └── ...
├── Http/
│   ├── Controllers/
│   │   ├── [Module]/           # Feature-specific folders
│   │   │   ├── DashboardController.php
│   │   │   ├── CourseController.php
│   │   │   └── MediaController.php
│   │   ├── LeadershipHistoryController.php
│   │   ├── StaffHistoryController.php
│   │   └── [MainController].php
│   └── Requests/               # Form validation
│       ├── Store[Model]Request.php
│       ├── Update[Model]Request.php
│       └── ...
├── Services/                    # Business logic
│   ├── [Module]Service.php     # e.g., SelfPacedService
│   ├── RegistrationService.php
│   └── ...
└── Console/
    └── Commands/               # CLI commands

routes/
├── web.php                      # Main web routes
├── [module].php                 # Feature-specific
│   ├── self-paced.php
│   ├── training.php
│   ├── membership.php
│   └── ...
└── api.php

resources/views/
├── layouts/
│   ├── self-paced-trainee-dashboard.blade.php
│   ├── app.blade.php
│   └── ...
├── components/
│   ├── module-hero.blade.php   # Reusable header
│   ├── forms/
│   │   ├── input.blade.php
│   │   ├── select.blade.php
│   │   └── textarea.blade.php
│   └── ...
├── [module]/
│   ├── dashboard/
│   │   ├── index.blade.php
│   │   ├── create.blade.php
│   │   ├── edit.blade.php
│   │   └── show.blade.php
│   └── public/
│       └── ...
└── ...

database/migrations/
├── 2024_01_15_000001_create_training_packages_table.php
├── 2024_01_15_000004_create_training_registrations_table.php
├── 2026_04_20_000002_create_self_paced_courses_table.php
└── ...
```

### Naming Conventions

| Element | Pattern | Example |
|---------|---------|---------|
| Model | Singular, PascalCase | `Course`, `CourseMedia`, `TrainingPackage` |
| Controller | `[Resource]Controller` | `CourseController`, `TrainingPackageController` |
| Service | `[Resource]Service` | `SelfPacedService`, `TrainingService` |
| Request | `Store/Update[Resource]Request` | `StoreStaffHistoryRequest`, `UpdateStaffHistoryRequest` |
| Route Name | `[module].[resource].[action]` | `training.packages.index`, `self-paced.learn.my-courses` |
| Migration | `YYYY_MM_DD_HHMMSS_action_description` | `2026_04_20_000002_create_self_paced_courses_table` |
| View | Feature/Action based | `training/packages/create.blade.php` |
| Table | Plural, snake_case | `training_packages`, `self_paced_courses` |

---

## Model Architecture

### Standard Model Structure

Every model follows this structure:

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * [Model] Model
 *
 * Brief description of what this model represents.
 * Key responsibilities and relationships.
 *
 * @property int $id
 * @property string $name
 * @property \Illuminate\Support\Carbon|null $created_at
 * @property \Illuminate\Support\Carbon|null $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at (if using SoftDeletes)
 */
class CourseMedia extends Model
{
    use HasFactory, SoftDeletes;  // Always use both traits

    protected $table = 'self_paced_media';

    // =========================================================================
    // ATTRIBUTES & CASTING
    // =========================================================================

    protected $fillable = [
        'subtopic_id',
        'title',
        'type',        // enum: 'video', 'image', 'pdf', 'presentation'
        'file_path',
        'file_name',
        'file_size',
        'mime_type',
        'sort_order',
    ];

    protected $casts = [
        'file_size'  => 'integer',
        'sort_order' => 'integer',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    protected $attributes = [
        'file_size'  => 0,
        'sort_order' => 0,
    ];

    // =========================================================================
    // RELATIONSHIPS
    // =========================================================================

    public function subtopic(): BelongsTo
    {
        return $this->belongsTo(CourseSubTopic::class, 'subtopic_id');
    }

    public function parent(): BelongsTo
    {
        return $this->belongsTo(LeadershipHistory::class);
    }

    public function achievements(): HasMany
    {
        return $this->hasMany(LeadershipAchievement::class);
    }

    // =========================================================================
    // QUERY SCOPES
    // =========================================================================

    public function scopeActive($query)
    {
        return $query->where('status', 'active');
    }

    public function scopeSearch($query, string $search)
    {
        return $query->where('name', 'like', "%{$search}%")
                     ->orWhere('description', 'like', "%{$search}%");
    }

    public function scopeOfType($query, string $type)
    {
        return $query->where('type', $type);
    }

    public function scopeOrdered($query)
    {
        return $query->orderBy('sort_order');
    }

    // =========================================================================
    // MODEL EVENTS (Optional - only if business logic needed)
    // =========================================================================

    protected static function booted(): void
    {
        static::creating(function (CourseMedia $media) {
            // Validate file_size when creating
            // Generate slug if needed
        });

        static::updating(function (CourseMedia $media) {
            // Handle file replacement logic
            // Update audit trail
        });
    }

    // =========================================================================
    // HELPER METHODS & ACCESSORS
    // =========================================================================

    public function isVideo(): bool
    {
        return $this->type === 'video';
    }

    public function isPdf(): bool
    {
        return $this->type === 'pdf';
    }

    public function getFormattedFileSize(): string
    {
        $bytes = $this->file_size;
        if ($bytes >= 1073741824) {
            return number_format($bytes / 1073741824, 2) . ' GB';
        } elseif ($bytes >= 1048576) {
            return number_format($bytes / 1048576, 2) . ' MB';
        }
        return number_format($bytes / 1024, 2) . ' KB';
    }
}
```

### Common Traits & Patterns

| Trait | Purpose | When to Use |
|-------|---------|------------|
| `HasFactory` | Model factories for testing | Always include |
| `SoftDeletes` | Soft delete records | For "deletable" entities (not lookup tables) |
| `HasPermissions` | Spatie permissions | Only on User model |
| `Notifiable` | Can receive notifications | For models that need alerts |

### Common Relationships

**BelongsTo** (Many-to-One):
```php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'user_id');
}
```

**HasMany** (One-to-Many):
```php
public function achievements(): HasMany
{
    return $this->hasMany(LeadershipAchievement::class);
}
```

**HasManyThrough** (Intermediate model):
```php
// Get all achievements for a leader through history
public function achievements(): HasManyThrough
{
    return $this->hasManyThrough(
        LeadershipAchievement::class,
        LeadershipHistory::class,
        'user_id',              // FK on intermediate table
        'leadership_history_id' // FK on target table
    );
}
```

**BelongsToMany** (Many-to-Many with pivot):
```php
public function trainers(): BelongsToMany
{
    return $this->belongsToMany(
        User::class,
        'training_session_trainers',  // pivot table
        'session_id',                  // FK to this model
        'trainer_id',                  // FK to related model
        'id',                          // Local key
        'id'                           // Related key
    );
}
```

### Field Casting Patterns

```php
protected $casts = [
    // Dates
    'created_at'           => 'datetime',
    'updated_at'           => 'datetime',
    'deleted_at'           => 'datetime',
    'employment_start_date' => 'date',
    'achievement_date'     => 'date',
    
    // Numbers
    'fee_amount'           => 'decimal:2',
    'duration_hours'       => 'integer',
    'file_size'            => 'integer',
    
    // Booleans
    'is_published'         => 'boolean',
    'generates_certificate' => 'boolean',
    
    // JSON
    'metadata'             => 'json',
    'tags'                 => 'json',
];
```

### Real-world Example: LeadershipHistory Model

```php
class LeadershipHistory extends Model
{
    use HasFactory;

    protected $fillable = [
        'organization_id',
        'leadership_position_id',
        'user_id',
        'term_start_date',
        'term_end_date',
        'position_title',
        'notes',
    ];

    protected $casts = [
        'term_start_date' => 'date',
        'term_end_date' => 'date',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    // Relationships
    public function organization() { return $this->belongsTo(Organization::class); }
    public function leadershipPosition() { return $this->belongsTo(LeadershipPosition::class); }
    public function user() { return $this->belongsTo(User::class); }
    public function achievements(): HasMany { return $this->hasMany(LeadershipAchievement::class); }

    // Query Scopes
    public function scopeSearch($query, string $search)
    {
        return $query->where('position_title', 'like', "%{$search}%");
    }

    public function scopeOrderByTermStart($query)
    {
        return $query->orderBy('term_start_date', 'desc');
    }

    public function scopeDateRange($query, $startDate, $endDate)
    {
        return $query->whereBetween('term_start_date', [$startDate, $endDate]);
    }

    // Helper Methods
    public function addAchievement(array $data): LeadershipAchievement
    {
        return $this->achievements()->create($data);
    }
}
```

---

## Service Layer

### Purpose & Pattern

Services handle **business logic** that would otherwise bloat controllers. They:

- Encapsulate complex operations (multi-step workflows)
- Handle transactions and atomic operations
- Coordinate between multiple models
- Contain payment/external service integration
- Provide reusable methods across controllers

### Standard Service Structure

```php
<?php

namespace App\Services;

use App\Models\SelfPaced\Course;
use App\Models\SelfPaced\Enrollment;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

/**
 * SelfPacedService
 *
 * Core business logic for the Self-Paced Learning Portal.
 * Handles course management, enrollment lifecycle, payment integration,
 * learning progress tracking, and topic timer management.
 */
class SelfPacedService
{
    public function __construct(
        protected PaymentService $paymentService,
        protected NotificationService $notificationService
    ) {}

    // =========================================================================
    // COURSE MANAGEMENT
    // =========================================================================

    /**
     * Create a new course with validation.
     *
     * @param array $data Course data
     * @param int $createdBy User creating the course
     * @return Course
     */
    public function createCourse(array $data, int $createdBy): Course
    {
        $data['slug']       = $this->generateUniqueSlug($data['title'], Course::class);
        $data['created_by'] = $createdBy;
        $data['updated_by'] = $createdBy;

        return Course::create($data);
    }

    /**
     * Update an existing course.
     *
     * @param Course $course
     * @param array $data Updated data
     * @param int $updatedBy User updating the course
     * @return Course
     */
    public function updateCourse(Course $course, array $data, int $updatedBy): Course
    {
        if (isset($data['title']) && $data['title'] !== $course->title) {
            $data['slug'] = $this->generateUniqueSlug($data['title'], Course::class, $course->id);
        }

        $data['updated_by'] = $updatedBy;
        $course->update($data);

        return $course->fresh();
    }

    /**
     * Delete a course with guards.
     *
     * @param Course $course
     * @return bool
     * @throws \RuntimeException if active enrollments exist
     */
    public function deleteCourse(Course $course): bool
    {
        if ($course->hasActiveEnrollments()) {
            $count = $course->enrollments()
                ->whereIn('status', ['active', 'pending_payment'])
                ->count();

            throw new \RuntimeException(
                "Cannot delete course: {$count} active enrollment(s) exist."
            );
        }

        Log::info('Self-paced course deleted', ['course_id' => $course->id]);
        return $course->delete();
    }

    // =========================================================================
    // ENROLLMENT (Complex multi-step workflow)
    // =========================================================================

    /**
     * Create an enrollment for a user in a course.
     * Handles both free and paid courses with payment integration.
     *
     * @param int $userId
     * @param Course $course
     * @param string $phoneNumber
     * @param int $createdBy
     * @param string $paymentMethod (mobile, card)
     * @param string|null $cardNumber
     * @param string|null $cardExpiry
     * @param string|null $cardCvv
     *
     * @return array ['enrollment' => Enrollment, 'payment' => Payment|null, 'success' => bool]
     * @throws \RuntimeException if duplicate enrollment exists
     */
    public function createEnrollment(
        int $userId,
        Course $course,
        string $phoneNumber = '',
        int $createdBy = 0,
        string $paymentMethod = 'mobile',
        ?string $cardNumber = null,
        ?string $cardExpiry = null,
        ?string $cardCvv = null
    ): array {
        // Guard: Check for existing enrollment
        $existing = Enrollment::where('user_id', $userId)
            ->where('course_id', $course->id)
            ->whereIn('status', ['active', 'completed', 'pending_payment'])
            ->first();

        if ($existing) {
            throw new \RuntimeException('You are already enrolled in this course.');
        }

        return DB::transaction(function () use ($userId, $course, $phoneNumber, $createdBy, $paymentMethod, $cardNumber, $cardExpiry, $cardCvv) {
            // FREE COURSE — Activate immediately, no payment required
            if ($course->isFree()) {
                $enrollment = Enrollment::create([
                    'user_id'     => $userId,
                    'course_id'   => $course->id,
                    'status'      => 'active',
                    'amount_paid' => 0.00,
                    'enrolled_at' => now(),
                    'created_by'  => $createdBy ?: $userId,
                ]);

                Log::info('Free course enrollment activated', [
                    'enrollment_id' => $enrollment->id,
                    'user_id'       => $userId,
                    'course_id'     => $course->id,
                ]);

                return ['enrollment' => $enrollment, 'payment' => null, 'success' => true];
            }

            // PAID COURSE — Create pending enrollment and initiate payment
            $reference = 'SP-ENR-' . strtoupper(Str::random(10));
            $storedMethod = $paymentMethod === 'card' ? 'mtn_momo_card' : 'mtn_momo_mobile';

            $enrollment = Enrollment::create([
                'user_id'           => $userId,
                'course_id'         => $course->id,
                'status'            => 'pending_payment',
                'payment_reference' => $reference,
                'payment_method'    => $storedMethod,
                'created_by'        => $createdBy ?: $userId,
            ]);

            // Initiate payment with external provider
            $payment = $this->paymentService->initiate([
                'amount'         => $course->fee_amount,
                'currency'       => $course->currency,
                'reference'      => $reference,
                'phone'          => $phoneNumber,
                'payment_method' => $paymentMethod,
                'card_number'    => $cardNumber,
                'card_expiry'    => $cardExpiry,
                'card_cvv'       => $cardCvv,
            ]);

            return ['enrollment' => $enrollment, 'payment' => $payment, 'success' => true];
        });
    }

    // =========================================================================
    // HELPER METHODS
    // =========================================================================

    /**
     * Generate unique slug for course or other models.
     */
    private function generateUniqueSlug(string $title, string $model, int $excludeId = null): string
    {
        $slug = Str::slug($title);
        $count = $model::whereRaw("slug LIKE '{$slug}%'")->when($excludeId, function ($q) use ($excludeId) {
            return $q->where('id', '!=', $excludeId);
        })->count();

        return $count ? "{$slug}-{$count + 1}" : $slug;
    }
}
```

### Service Injection in Controllers

```php
class CourseController extends Controller
{
    // Inject service via constructor
    public function __construct(protected SelfPacedService $selfPacedService) {}

    public function store(StoreCourseRequest $request)
    {
        $course = $this->selfPacedService->createCourse(
            $request->validated(),
            auth()->id()
        );

        return redirect()->route('courses.show', $course)
                       ->with('success', 'Course created successfully!');
    }
}
```

---

## Controller Patterns

### Standard CRUD Controller

```php
<?php

namespace App\Http\Controllers;

use App\Models\StaffHistory;
use App\Models\StaffAchievement;
use App\Http\Requests\StoreStaffHistoryRequest;
use App\Http\Requests\UpdateStaffHistoryRequest;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;

/**
 * StaffHistoryController
 *
 * Handles CRUD operations for staff employment history and achievements.
 * Only accessible by users with "view staff history" permission.
 */
class StaffHistoryController extends Controller
{
    /**
     * Display a listing of staff history records.
     * Supports filtering, searching, and pagination.
     */
    public function index(Request $request): View
    {
        // Authorization Check (always first)
        abort_unless(auth()->user()->can('view staff history'), 403,
            'You do not have permission to view staff history.');

        // Query builder with relationships
        $query = StaffHistory::with(['organization', 'user', 'achievements'])
            ->withCount('achievements');  // Include count without loading all

        // Apply filters
        if ($request->filled('organization_id')) {
            $query->where('organization_id', $request->organization_id);
        }

        if ($request->filled('employment_status')) {
            $query->where('employment_status', $request->employment_status);
        }

        // Search
        if ($request->filled('search')) {
            $query->search($request->search);  // Uses scope method
        }

        // Execute query with pagination
        $staffHistories = $query->orderByStartDate()->paginate(20);

        // Load filter options
        $organizations = Organization::orderBy('name')->get();

        return view('organization.staff-history.index', compact('staffHistories', 'organizations'));
    }

    /**
     * Show the form for creating a new staff history record.
     */
    public function create(): View
    {
        abort_unless(auth()->user()->can('create staff history'), 403);

        $organizations = Organization::orderBy('name')->get();
        $users = User::orderBy('name')->get();

        return view('organization.staff-history.create', compact('organizations', 'users'));
    }

    /**
     * Store a newly created staff history record.
     */
    public function store(StoreStaffHistoryRequest $request): RedirectResponse
    {
        $validated = $request->validated();

        // XSS Prevention — strip HTML tags
        $validated['staff_name'] = strip_tags($validated['staff_name']);
        $validated['position'] = strip_tags($validated['position']);
        $validated['termination_reason'] = strip_tags($validated['termination_reason'] ?? '');

        // Handle file upload (if present)
        if ($request->hasFile('passport')) {
            $validated['passport_path'] = $request->file('passport')
                ->store('staff-passports', 'public');
        }

        // Business logic — ensure status/date consistency
        if (!empty($validated['employment_end_date'])) {
            $validated['employment_status'] = StaffHistory::STATUS_FORMER;
        } elseif ($validated['employment_status'] === StaffHistory::STATUS_CURRENT) {
            $validated['employment_end_date'] = null;
        }

        StaffHistory::create($validated);

        return redirect()
            ->route('organization.staff-history.index')
            ->with('success', 'Staff record created successfully!');
    }

    /**
     * Display the specified staff history record with relationships.
     */
    public function show(StaffHistory $staffHistory): View
    {
        abort_unless(auth()->user()->can('view staff history'), 403);

        $staffHistory->load(['organization', 'user', 'achievements' => function ($query) {
            $query->orderByDate();  // Uses scope method
        }]);

        return view('organization.staff-history.show', compact('staffHistory'));
    }

    /**
     * Show the form for editing the specified record.
     */
    public function edit(StaffHistory $staffHistory): View
    {
        abort_unless(auth()->user()->can('edit staff history'), 403);

        $organizations = Organization::orderBy('name')->get();
        $users = User::orderBy('name')->get();

        return view('organization.staff-history.edit', compact('staffHistory', 'organizations', 'users'));
    }

    /**
     * Update the specified staff history record.
     */
    public function update(UpdateStaffHistoryRequest $request, StaffHistory $staffHistory): RedirectResponse
    {
        $validated = $request->validated();

        // XSS Prevention
        $validated['staff_name'] = strip_tags($validated['staff_name']);
        $validated['position'] = strip_tags($validated['position']);

        // Handle file upload with old file replacement
        if ($request->hasFile('passport')) {
            // Delete old file
            if ($staffHistory->passport_path) {
                Storage::disk('public')->delete($staffHistory->passport_path);
            }
            // Store new file
            $validated['passport_path'] = $request->file('passport')
                ->store('staff-passports', 'public');
        }

        // Ensure status/date consistency
        if (!empty($validated['employment_end_date'])) {
            $validated['employment_status'] = StaffHistory::STATUS_FORMER;
        }

        $staffHistory->update($validated);

        return redirect()
            ->route('organization.staff-history.show', $staffHistory)
            ->with('success', 'Staff record updated successfully!');
    }

    /**
     * Delete the specified staff history record.
     */
    public function destroy(StaffHistory $staffHistory): RedirectResponse
    {
        abort_unless(auth()->user()->can('delete staff history'), 403);

        // Optional: Move to trash instead of hard delete
        $staffHistory->delete();

        return redirect()
            ->route('organization.staff-history.index')
            ->with('success', 'Staff record deleted successfully!');
    }
}
```

### Common Controller Patterns

**Query Optimization:**
```php
// ✅ GOOD — Eager load relationships
$staff = StaffHistory::with(['organization', 'user', 'achievements'])
    ->paginate(20);

// ❌ BAD — N+1 queries
$staff = StaffHistory::paginate(20);
foreach ($staff as $record) {
    echo $record->organization->name;  // Query for each record!
}
```

**Authorization:**
```php
// Pattern 1 — Using abort_unless
abort_unless(auth()->user()->can('view staff history'), 403, 'Unauthorized');

// Pattern 2 — Using authorize helper (preferred for complex logic)
$this->authorize('view', $staffHistory);

// Pattern 3 — Via request class
public function authorize(): bool
{
    return auth()->user()->can('create staff history');
}
```

**File Uploads:**
```php
// Store file
$path = $request->file('passport')->store('staff-passports', 'public');

// Delete old file
if ($staffHistory->passport_path) {
    Storage::disk('public')->delete($staffHistory->passport_path);
}

// Recreate after replacement
$validated['passport_path'] = $request->file('passport')
    ->store('staff-passports', 'public');
```

**Service Injection:**
```php
public function __construct(
    protected TrainingService $trainingService,
    protected PaymentService $paymentService
) {}

// Use in methods
$package = $this->trainingService->createPackage($validated);
```

---

## Route Organization

### Route Structure

Routes are organized by **feature/module** rather than by HTTP verb:

```
routes/
├── web.php              # Main entry point — includes other route files
├── api.php              # API endpoints
├── auth.php             # Authentication (login, register, password reset)
├── training.php         # Training module
├── self-paced.php       # Self-Paced portal (public, dashboard, learner)
├── membership.php       # Membership management
├── mentorship.php       # Mentorship programs
├── knowledge-sharing.php # Research, publications, forums
├── organization.php     # Staff, leadership, org structure
├── communication.php    # Email, SMS, chat
├── payment.php          # Payment configurations
├── modules.php          # Digital machines, partners, contributions
└── ...
```

### Self-Paced Route Example (Complete)

```php
<?php

use App\Http\Controllers\SelfPaced\Dashboard\CourseController;
use App\Http\Controllers\SelfPaced\Dashboard\MediaController;
use App\Http\Controllers\SelfPaced\Learner\LearnerController;
use App\Http\Controllers\SelfPaced\Public\PublicController;
use Illuminate\Support\Facades\Route;

/*
|--------------------------------------------------------------------------
| Self-Paced Learning Portal Routes
|--------------------------------------------------------------------------
|
| Three surfaces:
|   1. Public  — landing page, course detail, registration, payment, verification
|   2. Dashboard — admin/trainer management (permission-gated)
|   3. Learner Portal — trainee study interface (auth + enrolled)
|
| Route name prefixes:
|   self-paced.public.*      — public-facing pages
|   self-paced.dashboard.*   — portal management dashboard
|   self-paced.learn.*       — learner portal (authenticated + enrolled)
|
*/

// ============================================================================
// PUBLIC ROUTES — No authentication required
// ============================================================================

Route::prefix('self-paced')->name('self-paced.public.')->group(function () {
    // Landing page
    Route::get('/', [PublicController::class, 'index'])->name('index');
    
    // Single course detail
    Route::get('/courses/{course}', [PublicController::class, 'show'])->name('courses.show');
    
    // Registration & Payment
    Route::get('/courses/{course}/register', [PublicController::class, 'register'])->name('register');
    Route::post('/courses/{course}/register', [PublicController::class, 'submitRegistration'])->name('register.submit');
});

// ============================================================================
// AUTHENTICATED + PERMISSION-GATED ROUTES (Dashboard)
// ============================================================================

Route::middleware(['auth', 'verified'])
    ->prefix('dashboard/self-paced')
    ->name('self-paced.dashboard.')
    ->group(function () {
        
        // Courses
        Route::resource('courses', CourseController::class);
        Route::post('courses/{course}/publish', [CourseController::class, 'publish'])
            ->name('courses.publish');
        
        // Media
        Route::post('media/upload', [MediaController::class, 'upload'])
            ->name('media.upload');
        Route::delete('media/{media}', [MediaController::class, 'destroy'])
            ->name('media.destroy');
    });

// ============================================================================
// AUTHENTICATED + ENROLLED ROUTES (Learner Portal)
// ============================================================================

Route::middleware(['auth', 'verified', 'permission:access self-paced learner portal'])
    ->prefix('self-paced')
    ->name('self-paced.learn.')
    ->group(function () {
        
        // Dashboard
        Route::get('/my-courses', [LearnerController::class, 'myCourses'])
            ->name('my-courses');
        
        // Study interface
        Route::get('/courses/{course}/learn', [LearnerController::class, 'learn'])
            ->name('courses.learn');
    });
```

### Training Route Example (Complete)

```php
Route::prefix('training')->name('public.training.')->group(function () {
    Route::get('/', [PublicController::class, 'index'])->name('index');
    Route::get('/certificates/verify', [PublicController::class, 'verifyCertificatePage'])
        ->name('certificates.verify-page');
});

Route::middleware(['auth', 'verified'])
    ->prefix('dashboard/training')
    ->name('training.')
    ->group(function () {
        
        // Packages CRUD
        Route::resource('packages', TrainingController::class);
        
        // Sessions
        Route::resource('sessions', SessionController::class);
        Route::get('sessions/calendar', [SessionController::class, 'calendar'])
            ->name('sessions.calendar');
        
        // Registrations
        Route::resource('registrations', RegistrationController::class);
        Route::get('registrations/pending', [RegistrationController::class, 'pending'])
            ->name('registrations.pending');
        
        // Certificates
        Route::resource('certificates', CertificateController::class);
        
        // Reports
        Route::get('reports', [ReportController::class, 'index'])
            ->name('reports.index');
    });
```

### Naming Convention for Routes

| Type | Pattern | Example |
|------|---------|---------|
| Index (list) | `[module].[resource].index` | `training.packages.index` |
| Show (detail) | `[module].[resource].show` | `training.packages.show` |
| Create form | `[module].[resource].create` | `training.packages.create` |
| Store | `[module].[resource].store` | `training.packages.store` |
| Edit form | `[module].[resource].edit` | `training.packages.edit` |
| Update | `[module].[resource].update` | `training.packages.update` |
| Delete | `[module].[resource].destroy` | `training.packages.destroy` |
| Custom action | `[module].[resource].[action]` | `training.packages.publish` |
| Public routes | `public.[module].[action]` | `public.training.index` |
| API routes | `api.[module].[resource].[action]` | `api.training.packages.index` |

---

## Blade Template Patterns

### Layout Structure

**Master Layout** (`resources/views/layouts/app.blade.php`):
```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>@yield('title', config('app.name'))</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50">
    <x-app-header />
    <x-app-sidebar />
    
    <div class="lg:ml-64 p-4">
        @yield('content')
    </div>
    
    <x-app-footer />
</body>
</html>
```

**Feature-Specific Layout** (`resources/views/layouts/self-paced-trainee-dashboard.blade.php`):
```blade
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'Trainee Dashboard')</title>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-50">
    {{-- Custom header for trainee portal --}}
    <header class="bg-white shadow-sm">
        <div class="max-w-7xl mx-auto px-4 py-4">
            <div class="flex justify-between items-center">
                <h1 class="text-2xl font-bold">{{ config('app.name') }}</h1>
                <x-app-user-menu />
            </div>
        </div>
    </header>
    
    <main>
        @yield('content')
    </main>
    
    {{-- Custom footer --}}
    <footer class="bg-gray-800 text-white text-center py-4 mt-8">
        <p>&copy; {{ now()->year }} {{ config('app.name') }}. All rights reserved.</p>
    </footer>
</body>
</html>
```

### Component Usage

**Module Hero** (used for section headers with tabs):
```blade
<x-module-hero 
    title="Staff Management" 
    description="Manage staff employment history"
    icon="fa-users">
    
    <x-slot name="backButton">
        <a href="{{ route('organization.index') }}" class="text-sm text-slate-600 hover:text-slate-900">
            <i class="fas fa-arrow-left"></i> Back
        </a>
    </x-slot>
    
    <x-slot name="tabs">
        <a href="{{ route('organization.staff-history.index') }}"
           class="module-hero-tab {{ request()->routeIs('organization.staff-history.*') ? 'border-b-2 border-slate-900' : 'opacity-70' }}">
            <i class="fas fa-history"></i> History
        </a>
    </x-slot>
    
    <x-slot name="actions">
        @can('create staff history')
            <a href="{{ route('organization.staff-history.create') }}" class="btn btn-primary">
                <i class="fas fa-plus"></i> Add Staff
            </a>
        @endcan
    </x-slot>
</x-module-hero>
```

**Form Components**:
```blade
<form action="{{ route('organization.staff-history.store') }}" method="POST" enctype="multipart/form-data">
    @csrf

    {{-- Text Input --}}
    <x-forms.input 
        name="staff_name" 
        label="Staff Name" 
        required 
        :value="old('staff_name')" />

    {{-- Select Dropdown --}}
    <x-forms.select 
        name="organization_id" 
        label="Organization" 
        required>
        @foreach ($organizations as $org)
            <option value="{{ $org->id }}" {{ old('organization_id') == $org->id ? 'selected' : '' }}>
                {{ $org->name }}
            </option>
        @endforeach
    </x-forms.select>

    {{-- Textarea --}}
    <x-forms.textarea 
        name="termination_reason" 
        label="Reason for Termination" 
        rows="4" />

    {{-- File Input --}}
    <div class="mb-4">
        <label class="block text-sm font-medium text-gray-700 mb-1">
            Passport <span class="text-red-500">*</span>
        </label>
        <input type="file" name="passport" accept=".jpg,.jpeg,.png,.pdf" 
               class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500" 
               required>
        @error('passport')
            <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
        @enderror
    </div>

    {{-- Submit Button --}}
    <div class="flex gap-3">
        <button type="submit" class="btn btn-primary">Save Staff Record</button>
        <a href="{{ route('organization.staff-history.index') }}" class="btn btn-secondary">Cancel</a>
    </div>
</form>
```

### Error Handling & Validation Display

```blade
{{-- Error Summary --}}
@if ($errors->any())
    <div class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-center gap-2">
        <i class="fas fa-exclamation-circle"></i>
        <div>
            <p class="font-semibold">Please fix the errors below:</p>
            <ul class="list-disc list-inside text-sm">
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    </div>
@endif

{{-- Field-level Error --}}
<div class="mb-4">
    <label class="block text-sm font-medium text-gray-700 mb-1">
        Staff Name <span class="text-red-500">*</span>
    </label>
    <input type="text" name="staff_name" value="{{ old('staff_name') }}"
           class="w-full px-4 py-2 border {{ $errors->has('staff_name') ? 'border-red-500' : 'border-gray-300' }} rounded-lg">
    @error('staff_name')
        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
    @enderror
</div>
```

### Pagination

```blade
{{-- Data Table with Pagination --}}
<div class="overflow-x-auto bg-white rounded-lg shadow-sm border border-gray-200">
    <table class="w-full">
        <thead class="bg-gray-50 border-b border-gray-200">
            <tr>
                <th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Staff Name</th>
                <th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Position</th>
                <th class="px-6 py-3 text-left text-sm font-semibold text-gray-700">Status</th>
                <th class="px-6 py-3 text-right text-sm font-semibold text-gray-700">Actions</th>
            </tr>
        </thead>
        <tbody class="divide-y divide-gray-200">
            @forelse ($staffHistories as $staff)
                <tr class="hover:bg-gray-50 transition-colors">
                    <td class="px-6 py-4 text-sm text-gray-900">{{ $staff->staff_name }}</td>
                    <td class="px-6 py-4 text-sm text-gray-600">{{ $staff->position }}</td>
                    <td class="px-6 py-4 text-sm">
                        <span class="px-2 py-1 text-xs font-medium rounded-full
                            {{ $staff->employment_status === 'current' ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' }}">
                            {{ ucfirst($staff->employment_status) }}
                        </span>
                    </td>
                    <td class="px-6 py-4 text-right text-sm">
                        <a href="{{ route('organization.staff-history.show', $staff) }}" 
                           class="text-blue-600 hover:text-blue-900">View</a>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="4" class="px-6 py-8 text-center text-gray-500">
                        <i class="fas fa-inbox text-3xl mb-2 opacity-50"></i>
                        <p>No staff records found</p>
                    </td>
                </tr>
            @endforelse
        </tbody>
    </table>
</div>

{{-- Pagination Links --}}
<div class="mt-6">
    {{ $staffHistories->links() }}
</div>
```

### Grid/Card Layouts (Self-Paced Dashboard Example)

```blade
<div class="grid grid-cols-2 sm:grid-cols-2 lg:grid-cols-6 gap-3 sm:gap-4">
    {{-- Card Component --}}
    <div class="bg-white/10 backdrop-blur-sm border border-white/20 rounded-xl p-4 text-center">
        <p class="text-3xl sm:text-4xl font-bold text-white">{{ $stats['active_registrations'] }}</p>
        <p class="text-indigo-200 text-xs mt-1 font-medium">Active Courses</p>
    </div>
    
    {{-- Repeat for each stat --}}
</div>

{{-- Course Cards Grid --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    @forelse ($courses as $course)
        <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
            {{-- Featured Image --}}
            <div class="relative h-48 bg-gray-200 overflow-hidden">
                @if ($course->featured_image)
                    <img src="{{ asset('storage/' . $course->featured_image) }}" 
                         alt="{{ $course->title }}" 
                         class="w-full h-full object-cover">
                @else
                    <div class="w-full h-full bg-gradient-to-br from-indigo-400 to-purple-500 flex items-center justify-center">
                        <i class="fas fa-book text-white text-3xl opacity-50"></i>
                    </div>
                @endif
                
                {{-- Status Badge --}}
                <div class="absolute top-3 right-3">
                    <span class="px-2 py-1 text-xs font-semibold rounded-full
                        {{ $course->status === 'published' ? 'bg-green-500 text-white' : 'bg-gray-500 text-white' }}">
                        {{ ucfirst($course->status) }}
                    </span>
                </div>
            </div>
            
            {{-- Content --}}
            <div class="p-4">
                <h3 class="font-semibold text-gray-900 mb-2">{{ $course->title }}</h3>
                <p class="text-sm text-gray-600 mb-4 line-clamp-2">{{ $course->description }}</p>
                
                {{-- Footer with Actions --}}
                <div class="flex gap-2">
                    <a href="{{ route('courses.show', $course) }}" class="flex-1 btn btn-sm btn-primary">View</a>
                    @can('edit', $course)
                        <a href="{{ route('courses.edit', $course) }}" class="flex-1 btn btn-sm btn-secondary">Edit</a>
                    @endcan
                </div>
            </div>
        </div>
    @empty
        <div class="col-span-3 text-center py-12">
            <i class="fas fa-inbox text-4xl text-gray-300 mb-3"></i>
            <p class="text-gray-500">No courses found</p>
            <a href="{{ route('courses.create') }}" class="btn btn-primary mt-4">Create First Course</a>
        </div>
    @endforelse
</div>
```

---

## Permission & Authorization

### Using Spatie Permissions

**Permission Checks in Controllers:**
```php
// Method 1 — Using abort_unless
abort_unless(auth()->user()->can('view staff history'), 403,
    'You do not have permission to view staff history.');

// Method 2 — Using authorize
$this->authorize('view', $staffHistory);

// Method 3 — In FormRequest (automatic)
public function authorize(): bool
{
    return auth()->user()->can('create staff history');
}

// Method 4 — In Blade
@can('create staff history')
    <a href="{{ route('organization.staff-history.create') }}">Add Staff</a>
@endcan
```

**Permission Hierarchy (Common Roles):**
- `superadmin` — Full system access
- `manager` — Organization/department level management
- `trainer` — Can create and manage training
- `trainer-trainee` — Can view own trainings
- `mentor` — Can manage mentorship programs
- `mentee` — Trainee role
- `contributor` — Can contribute to knowledge base
- `health-marketor` — Can list digital machines
- `member` — General member access
- `partner` — Partner organization access

**Permission Naming Convention:**
- `view [resource]` — List/view resource
- `create [resource]` — Create new resource
- `edit [resource]` — Edit resource
- `delete [resource]` — Delete resource
- `approve [resource]` — Approve applications/requests
- `export [resource]` — Export data

**Example Permission Setup (in Seeder):**
```php
// Create permissions
$permissions = [
    'view staff history',
    'create staff history',
    'edit staff history',
    'delete staff history',
    'add staff achievements',
];

foreach ($permissions as $permission) {
    Permission::findOrCreate($permission);
}

// Create roles
$managerRole = Role::findOrCreate('manager');
$managerRole->givePermissionTo($permissions);

// Assign to users
$user->assignRole('manager');
```

---

## Database Migrations

### Migration Pattern

**Naming Convention:** `YYYY_MM_DD_HHMMSS_action_description.php`

**Structure:**
```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * Creates [description of what the table does].
     * Stores [what data it stores].
     * 
     * @return void
     */
    public function up(): void
    {
        Schema::create('staff_histories', function (Blueprint $table) {
            // Primary key
            $table->id();

            // Foreign keys
            $table->foreignId('organization_id')
                ->constrained('organizations')
                ->onDelete('cascade');
            
            $table->foreignId('user_id')
                ->nullable()
                ->constrained('users')
                ->onDelete('set null');

            // Main data fields
            $table->string('staff_name', 191);
            $table->string('position', 191);
            $table->date('employment_start_date');
            $table->date('employment_end_date')->nullable();

            // Enum fields
            $table->enum('employment_status', ['current', 'former']);

            // Optional fields
            $table->text('termination_reason')->nullable();

            // Timestamps & soft deletes
            $table->timestamps();
            $table->softDeletes();

            // Indexes for performance
            $table->index('organization_id');
            $table->index('user_id');
            $table->index('employment_status');
            $table->index('employment_start_date');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('staff_histories');
    }
};
```

### Common Column Types & Patterns

| Purpose | Type | Example |
|---------|------|---------|
| Text (short) | `string(191)` | Names, emails, slugs |
| Text (long) | `text()` | Descriptions, notes |
| Numbers | `integer()`, `decimal(10,2)` | Counts, prices |
| Dates | `date()`, `datetime()` | Birthday, timestamps |
| Status | `enum(['draft', 'published'])` | Status fields |
| Foreign keys | `foreignId('table_id')` | Relationships |
| Timestamps | `timestamps()` | Auto created_at, updated_at |
| Soft delete | `softDeletes()` | deleted_at column |
| Boolean | `boolean()` | Flags (default false) |
| JSON | `json()` | Metadata, tags |

### Foreign Key Constraints

```php
// Cascade delete — remove child when parent deleted
$table->foreignId('organization_id')
    ->constrained('organizations')
    ->cascadeOnDelete();

// Set null on delete — orphan the child
$table->foreignId('user_id')
    ->nullable()
    ->constrained('users')
    ->nullOnDelete();

// Restrict delete — prevent deletion if children exist
$table->foreignId('parent_id')
    ->constrained('parents')
    ->restrictOnDelete();
```

### Indexes

```php
// Single column index
$table->index('status');
$table->index('created_by');

// Unique index
$table->unique(['user_id', 'training_package_id'], 'unique_user_package');

// Unique on column
$table->unique('slug');

// Full-text index (MySQL)
$table->fullText('description');
```

---

## File Handling

### File Upload Pattern in Controllers

```php
// 1. Check if file exists in request
if ($request->hasFile('passport')) {
    // 2. Store file to 'staff-passports' directory in 'public' disk
    $path = $request->file('passport')->store('staff-passports', 'public');
    
    // 3. Save path to database
    $validated['passport_path'] = $path;
}

// In model save
StaffHistory::create($validated);
```

### File Upload in Update (with Replacement)

```php
public function update(UpdateStaffHistoryRequest $request, StaffHistory $staffHistory): RedirectResponse
{
    $validated = $request->validated();

    // If new file is uploaded
    if ($request->hasFile('passport')) {
        // Delete old file first
        if ($staffHistory->passport_path) {
            Storage::disk('public')->delete($staffHistory->passport_path);
        }
        
        // Store new file
        $validated['passport_path'] = $request->file('passport')
            ->store('staff-passports', 'public');
    }

    $staffHistory->update($validated);
    return redirect()->route('...')->with('success', 'Updated!');
}
```

### File Upload in Forms

```blade
<form action="{{ route('staff.store') }}" method="POST" enctype="multipart/form-data">
    @csrf

    <div class="mb-4">
        <label class="block text-sm font-medium text-gray-700 mb-1">
            Passport <span class="text-red-500">*</span>
        </label>
        <input type="file" 
               name="passport" 
               accept=".jpg,.jpeg,.png,.pdf" 
               class="w-full px-4 py-2 border border-gray-300 rounded-lg"
               required>
        @error('passport')
            <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
        @enderror
    </div>

    <button type="submit" class="btn btn-primary">Save</button>
</form>
```

### File Validation in FormRequest

```php
public function rules(): array
{
    return [
        'passport' => 'nullable|file|mimes:jpeg,jpg,png,pdf|max:5120',  // max 5MB
        'featured_image' => 'nullable|image|mimes:jpeg,jpg,png,webp|max:2048',  // max 2MB
        'document' => 'required|file|mimes:pdf,docx|max:10240',  // max 10MB
    ];
}

public function messages(): array
{
    return [
        'passport.mimes' => 'Passport must be a JPEG, PNG, or PDF file.',
        'passport.max' => 'Passport file size must not exceed 5MB.',
        'featured_image.image' => 'Featured image must be an image file.',
        'featured_image.max' => 'Featured image must not exceed 2MB.',
    ];
}
```

### File URL Display

```blade
{{-- Display file if exists --}}
@if ($staffHistory->passport_path)
    <a href="{{ asset('storage/' . $staffHistory->passport_path) }}" 
       target="_blank" 
       class="text-blue-600 hover:text-blue-900">
        Download Passport
    </a>
@endif

{{-- Display image --}}
@if ($course->featured_image)
    <img src="{{ asset('storage/' . $course->featured_image) }}" 
         alt="{{ $course->title }}"
         class="w-full h-48 object-cover rounded-lg">
@else
    <div class="w-full h-48 bg-gray-200 flex items-center justify-center rounded-lg">
        <i class="fas fa-image text-gray-400 text-3xl"></i>
    </div>
@endif
```

### Storage Configuration

**File storage locations:**
```
storage/
├── app/
│   ├── private/            # Not accessible via web
│   └── public/             # Accessible via storage/ symlink
│       ├── staff-passports/
│       ├── training/
│       │   ├── packages/   # Featured images
│       │   └── certificates/
│       ├── machines/
│       │   └── images/
│       └── ...
└── logs/
    └── laravel.log
```

**In .env:**
```
FILESYSTEM_DISK=public
```

---

## Existing Complex Modules

### 1. Self-Paced Learning Portal

**Key Files:**
- Models: `app/Models/SelfPaced/{Course, Enrollment, CourseMedia, ExamAttempt, SelfPacedCertificate}`
- Controllers: `app/Http/Controllers/SelfPaced/{Dashboard, Learner, Public}`
- Service: `app/Services/SelfPacedService.php`
- Routes: `routes/self-paced.php`
- Migrations: `database/migrations/2026_04_20_*`

**Key Features:**
- Free and paid course support with payment integration
- Enrollment with payment status tracking
- Exam system with multiple attempts (configurable)
- Learning progress tracking per topic
- Course media management (videos, PDFs, images)
- Certificate generation and verification
- Public course browsing + authenticated learner portal + admin dashboard

**Architectural Highlights:**
- **Transaction-based enrollment** — ensures atomic payment/enrollment creation
- **Service layer handles payments** — SelfPacedService -> PaymentService integration
- **Permission-gated access** — different routes for public/admin/learner with permission middleware
- **Soft deletes** — courses can be archived without losing enrollment history
- **Query scopes** — `Active()`, `Search()`, `Published()` for clean controller code
- **File handling** — CourseMedia stores video/PDF/image references

**Example Flow:**
1. User views public course
2. User clicks "Register" → PostForm creates Enrollment (pending_payment)
3. Payment service initiated → user pays via Paystack
4. Webhook confirms payment → enrollment status = "active"
5. User can now access course materials
6. User completes topics/exams
7. On passing exam score → certificate auto-issued

---

### 2. Training Management Module

**Key Files:**
- Models: `app/Models/{TrainingPackage, TrainingRegistration, TrainingSession, TrainingAttendance, TrainingCertificate}`
- Controllers: `app/Http/Controllers/Training/`
- Service: `app/Services/TrainingService.php`, `RegistrationService.php`
- Routes: `routes/training.php`
- Migrations: `database/migrations/2024_01_15_*`

**Key Features:**
- Training package creation with configurable duration, capacity, fees
- Registration with approval workflow (pending → approved/rejected)
- Session management with multiple trainers
- Attendance tracking
- Certificate issuance on completion
- Payment tracking (multiple payment methods)
- Public browsing + authenticated registration + admin dashboard

**Architectural Highlights:**
- **Status workflow** — pending → approved/rejected → completed with timestamp tracking
- **Approval workflow** — approved_by user tracks, rejection_reason stored
- **Unique constraints** — `unique(['user_id', 'training_package_id'])` prevents duplicate registrations
- **Pivot table for trainers** — `training_session_trainers` for many-to-many
- **Metadata storage** — JSON `metadata` field for flexible data
- **Multi-source registration** — tracks registration source (website, dashboard, admin, public)

**Example Flow:**
1. Admin creates Training Package with sessions
2. User applies for training (public form)
3. Registration created with status = "pending"
4. Admin reviews → approves/rejects
5. If approved, user sees sessions, can attend
6. Admin marks attendance
7. On completion → certificate issued

---

### 3. Organization Management (Staff & Leadership)

**Key Files:**
- Models: `app/Models/{StaffHistory, StaffAchievement, LeadershipHistory, LeadershipAchievement}`
- Controllers: `app/Http/Controllers/{StaffHistoryController, LeadershipHistoryController}`
- Routes: `routes/organization.php`
- Migrations: `database/migrations/2024_01_01_000009_*`, `2024_01_01_000007_*`

**Key Features:**
- Staff employment history tracking (current/former)
- Staff achievements recording
- Leadership position tracking with term dates
- Leadership achievements recording
- File uploads (passports for staff)
- Date range filtering
- Search functionality

**Architectural Highlights:**
- **SoftDeletes** — deleted records remain in database
- **Model events** — `booted()` ensures status/date consistency
- **File uploads** — staff passport storage with public access
- **Achievement tracking** — separate model for historical records
- **Through relationships** — access leader through history via HasOneThrough
- **Date casting** — automatic Carbon conversion

**Example: LeadershipAchievement HasOneThrough**
```php
// Get leader directly from achievement
public function leader()
{
    return $this->hasOneThrough(
        User::class,              // Final model
        LeadershipHistory::class, // Intermediate
        'id',                     // FK on histories matching local key
        'id',                     // FK on users (PK)
        'leadership_history_id',  // Local key on achievements
        'user_id'                 // Key on histories → users
    );
}
```

---

## Quick Reference Checklist

### Creating a New Module

#### 1. Database
- [ ] Create migration file: `database/migrations/YYYY_MM_DD_HHMMSS_create_[table]_table.php`
- [ ] Include relationships with proper foreign keys
- [ ] Add indexes on frequently queried fields
- [ ] Use `softDeletes()` if records can be deleted
- [ ] Use `timestamps()` for created_at/updated_at
- [ ] Define unique constraints where needed

#### 2. Models
- [ ] Create model in `app/Models/` or `app/Models/[Feature]/`
- [ ] Include docblock with `@property` annotations
- [ ] Use `HasFactory`, `SoftDeletes` traits
- [ ] Define fillable array
- [ ] Define casts array
- [ ] Add all relationships (BelongsTo, HasMany, etc.)
- [ ] Add query scopes for common filters
- [ ] Add helper methods for business logic
- [ ] Add model events if needed (created, updated hooks)

#### 3. Controllers
- [ ] Create in `app/Http/Controllers/` or `app/Http/Controllers/[Feature]/`
- [ ] Extend `Controller` base class
- [ ] Inject services via constructor
- [ ] Add authorization check at start of each method
- [ ] Use FormRequest classes for validation
- [ ] Eager load relationships with `with()`
- [ ] Use query scopes in index method
- [ ] Handle file uploads properly
- [ ] Return appropriate responses (View, RedirectResponse, JsonResponse)

#### 4. Services
- [ ] Create in `app/Services/[Feature]Service.php`
- [ ] Define dependencies via constructor injection
- [ ] Use transactions for multi-step operations
- [ ] Add descriptive docblocks
- [ ] Raise exceptions on invalid operations
- [ ] Log important actions

#### 5. Requests
- [ ] Create `app/Http/Requests/Store[Model]Request.php`
- [ ] Create `app/Http/Requests/Update[Model]Request.php`
- [ ] Define `authorize()` method
- [ ] Define `rules()` method
- [ ] Define `messages()` method for custom errors
- [ ] Handle data transformation if needed

#### 6. Routes
- [ ] Create `routes/[feature].php` if new feature
- [ ] Use resource routing where applicable: `Route::resource('models', ModelController::class)`
- [ ] Organize by access level (public, auth, admin)
- [ ] Use descriptive names: `[module].[resource].[action]`
- [ ] Group related routes with middleware
- [ ] Include the routes in `routes/web.php` or `routes/api.php`

#### 7. Blade Templates
- [ ] Create `resources/views/[feature]/`
- [ ] Create CRUD views: `index.blade.php`, `create.blade.php`, `edit.blade.php`, `show.blade.php`
- [ ] Use `<x-module-hero>` for headers
- [ ] Use form components for inputs
- [ ] Display validation errors
- [ ] Include pagination for list views
- [ ] Show empty states
- [ ] Use `@can()` directives for permissions

#### 8. Permissions
- [ ] Define permissions in seeder: `view`, `create`, `edit`, `delete` + custom actions
- [ ] Assign to appropriate roles
- [ ] Use in controllers: `abort_unless(auth()->user()->can('view [resource]'), 403)`
- [ ] Use in Blade: `@can('create [resource]')`

#### 9. Testing
- [ ] Write feature tests for CRUD operations
- [ ] Test authorization (with/without permission)
- [ ] Test validation errors
- [ ] Test file uploads
- [ ] Test complex workflows

#### 10. Documentation
- [ ] Update `ARCHITECTURE_MAP.md` with module details
- [ ] Add docblocks to all public methods
- [ ] Document any complex logic
- [ ] Include route examples

---

## Gallery Module Template

### 1. Database Structure

```php
// Migration: 2026_05_26_000001_create_gallery_images_table.php
Schema::create('gallery_images', function (Blueprint $table) {
    $table->id();
    $table->foreignId('gallery_id')->constrained('galleries')->cascadeOnDelete();
    $table->foreignId('uploaded_by')->constrained('users')->nullOnDelete();
    
    $table->string('title', 191);
    $table->text('description')->nullable();
    $table->string('image_path'); // stored path
    $table->string('thumbnail_path')->nullable();
    $table->integer('file_size');
    $table->string('mime_type', 50);
    
    $table->integer('sort_order')->default(0);
    $table->boolean('is_featured')->default(false);
    $table->boolean('is_published')->default(true);
    
    $table->integer('view_count')->default(0);
    $table->integer('download_count')->default(0);
    
    $table->timestamps();
    $table->softDeletes();
    
    $table->index('gallery_id');
    $table->index('is_published');
    $table->index('is_featured');
});
```

### 2. Models

**Gallery.php:**
```php
class Gallery extends Model {
    use HasFactory, SoftDeletes;
    
    protected $fillable = ['title', 'slug', 'description', 'created_by', 'updated_by'];
    
    public function images(): HasMany {
        return $this->hasMany(GalleryImage::class);
    }
}
```

**GalleryImage.php:**
```php
class GalleryImage extends Model {
    use HasFactory, SoftDeletes;
    
    protected $fillable = ['gallery_id', 'title', 'image_path', 'thumbnail_path'];
    
    public function gallery(): BelongsTo {
        return $this->belongsTo(Gallery::class);
    }
    
    public function uploader(): BelongsTo {
        return $this->belongsTo(User::class, 'uploaded_by');
    }
    
    public function scopePublished($query) {
        return $query->where('is_published', true);
    }
}
```

### 3. Controllers

```php
class GalleryController extends Controller {
    public function __construct(protected GalleryService $galleryService) {}
    
    public function index(): View {
        $galleries = Gallery::with('images')->paginate(20);
        return view('gallery.index', compact('galleries'));
    }
}

class GalleryImageController extends Controller {
    public function upload(Request $request): RedirectResponse {
        $validated = $request->validate([
            'gallery_id' => 'required|exists:galleries,id',
            'image' => 'required|image|max:5120',
            'title' => 'required|string|max:191',
        ]);
        
        $path = $request->file('image')->store('gallery-images', 'public');
        
        GalleryImage::create([
            'gallery_id' => $validated['gallery_id'],
            'image_path' => $path,
            'title' => $validated['title'],
            'uploaded_by' => auth()->id(),
        ]);
        
        return redirect()->back()->with('success', 'Image uploaded!');
    }
}
```

### 4. Service

```php
class GalleryService {
    public function uploadImage(Gallery $gallery, array $data): GalleryImage {
        $path = $data['image']->store('gallery-images', 'public');
        
        return $gallery->images()->create([
            'image_path' => $path,
            'title' => $data['title'],
            'uploaded_by' => auth()->id(),
        ]);
    }
    
    public function deleteImage(GalleryImage $image): void {
        Storage::disk('public')->delete($image->image_path);
        $image->delete();
    }
}
```

### 5. Routes

```php
// routes/gallery.php
Route::middleware('auth')->group(function () {
    Route::resource('galleries', GalleryController::class);
    Route::post('galleries/{gallery}/images', [GalleryImageController::class, 'upload'])
        ->name('galleries.images.upload');
    Route::delete('gallery-images/{image}', [GalleryImageController::class, 'destroy'])
        ->name('gallery-images.destroy');
});

Route::prefix('gallery')->name('public.gallery.')->group(function () {
    Route::get('/', [PublicGalleryController::class, 'index'])->name('index');
    Route::get('{gallery}', [PublicGalleryController::class, 'show'])->name('show');
});
```

This completes the comprehensive architecture map. Use this as your template for building the Gallery Module!

