# Profile Approval Flow — Implementation Plan

> **How to use:** Work one session at a time. When session done, change `[ ]` to `[x]` in that session's checkbox. Start next session fresh — paste only that session block for context.

---

## Progress Tracker

- [x] Session 1 — Migrations
- [x] Session 2 — Enum + Models
- [x] Session 3 — Registration Changes
- [x] Session 4 — Middleware + Route Wiring
- [x] Session 5 — Admin Controller (ProfileApprovalController)
- [x] Session 6 — User Resubmit Controller
- [x] Session 7 — Notifications
- [x] Session 8 — Blade Views (User-facing)
- [x] Session 9 — Blade Views (Admin)
- [ ] Session 10 — Testing & Cleanup

---

## Codebase Context (read this once — applies to all sessions)

### Admin system — SEPARATE from users

Admins live in their own `admins` table (migration: `2024_12_17_054922_create_admins_table.php`).
Columns: `id`, `name`, `email`, `email_verified_at`, `password`, `profile_picture`, `remember_token`, timestamps.
Admin auth guard: `auth:admin` (NOT `is_admin` middleware — that does not exist).
Admin route pattern: outer group `middleware('admin.session.cookie')`, inner group `middleware('auth:admin')`.
Admin route prefix: `config('banibanna.admin_dir')` — NOT hardcoded `/admin`.

```php
// How to get logged-in admin in admin controllers:
auth('admin')->user()   // returns Admin model instance
auth('admin')->id()     // returns admin id
```

### Users table — actual columns

File: `database/migrations/0001_01_01_000000_create_users_table.php`

Relevant existing columns:
- `blocked` — tinyInteger default 0
- `deactivated` — tinyInteger default 0
- `steps_completed` — tinyInteger default 0
- `last_active_at` — timestamp nullable

`profile_status` does NOT exist yet — we add it (tinyInteger default 0).

### User model — actual structure

File: `app/Models/User.php`

`$fillable` currently:
`slug`, `first_name`, `last_name`, `email`, `password`, `phone`, `gender`, `blocked`,
`profile_picture`, `profile_visibility`, `notify_email`, `allow_chat`, `show_online_status`,
`show_last_seen`, `email_verified_at`, `last_matched_id`, `steps_completed`

`casts()` is a **method** (not property) returning array. Currently:
```php
protected function casts(): array {
    return [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}
```
Add `profile_status` cast here.

### Existing middleware aliases (bootstrap/app.php)

```php
$middleware->alias([
    'admin.session.cookie' => SetSessionCookie::class,
    'profile.completed'    => CheckProfileCompletion::class,
    'check.blocked'        => CheckIfBlocked::class,
    'profile.last-active'  => UpdateLastActive::class,
]);
```
New alias `profile.approved` goes into this same array.

### User route group (user_routes.php)

Current middleware stack on member routes:
```php
Route::middleware(['auth', 'verified', 'profile.completed', 'check.blocked', 'profile.last-active'])
```
`profile.approved` must be inserted between `verified` and `profile.completed` so unapproved users never reach profile completion checks.

### Registration controller

File: `app/Http/Controllers/Auth/RegisteredUserController.php`
Method: `store()`
Currently creates user with: `first_name`, `last_name`, `email`, `gender`, `password`, `steps_completed => 1`
No transaction wrapping currently — we add one.
`profile_status` not set — we add `profile_status => ProfileStatus::PendingReview`.

### Existing enums

All in `app/Enums/`. Existing: `ProfileFor.php`, `HabitOption.php`, `ChildrenEnum.php`.
`ProfileFor` uses string-backed enum. Our `ProfileStatus` will be int-backed.

### Notifications

`app/Notifications/` — empty. We create all three from scratch.

### Blade layouts

User views extend: `layouts/app.blade.php`
Admin views extend: `vendor/adminlte/master.blade.php`
Look at an existing admin view (e.g. `resources/views/admin/users/`) for the exact `@extends` and `@section` names before writing admin views.

### Admin route file

`routes/admin_auth.php` — all admin routes go here.
Existing pattern for protected admin routes:
```php
Route::prefix(config('banibanna.admin_dir'))->name('admin.')->middleware('admin.session.cookie')->group(function () {
    Route::middleware('auth:admin')->group(function () {
        // routes here
    });
});
```

---

## Session 1 — Migrations

### Pre-session: read these files first
- `database/migrations/0001_01_01_000000_create_users_table.php` — confirm exact column types/names before adding
- Any recent migration in `database/migrations/` — see naming convention and latest timestamp to pick next timestamp

### Tasks

1. Generate migration:
   ```
   php artisan make:migration add_profile_status_to_users_table
   ```
   Add in `up()`:
   ```php
   $table->tinyInteger('profile_status')->default(0)->after('deactivated');
   ```
   Add in `down()`:
   ```php
   $table->dropColumn('profile_status');
   ```

2. Generate migration:
   ```
   php artisan make:migration create_profile_review_histories_table
   ```
   Schema:
   ```php
   $table->id();
   $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
   $table->foreignId('admin_id')->nullable()->constrained('admins')->nullOnDelete();
   // NOTE: FK references 'admins' table — NOT 'users' table
   $table->tinyInteger('status');
   $table->text('remarks')->nullable();
   $table->timestamps();
   ```

3. Run: `php artisan migrate`

4. **CRITICAL — backfill existing users.** After migration, all existing users will have `profile_status = 0` (PendingReview) and will be locked out. Add this to the migration's `up()` AFTER the column is added:
   ```php
   // Approve all users who have already verified their email (existing active accounts)
   DB::statement('UPDATE users SET profile_status = 1 WHERE email_verified_at IS NOT NULL');
   ```
   And add a matching first `ProfileReviewHistory` record for them — OR accept that existing users have no history row (both are valid; choose no-history if simpler).

   Down method should handle the dropped column (no data to restore needed).

### Done when
- Both migrations run without error
- `users.profile_status` column exists (tinyInteger, default 0)
- `profile_review_histories` table exists with `admin_id` FK to `admins`
- Existing verified users have `profile_status = 1` (not locked out)

**[ ] SESSION 1 DONE — proceed to Session 2**

---

## Session 2 — Enum + Models

### Pre-session: read these files first
- `app/Models/User.php` — see exact `$fillable`, `casts()` method, relationships before editing
- `app/Enums/ProfileFor.php` — see enum syntax style used in this project
- `app/Models/` — list files to confirm no `ProfileReviewHistory.php` exists yet

### Tasks

1. Create `app/Enums/ProfileStatus.php`:
   ```php
   <?php

   namespace App\Enums;

   enum ProfileStatus: int
   {
       case PendingReview  = 0;
       case Approved       = 1;
       case Rejected       = 2;
       case Suspended      = 3;
       case ActionRequired = 4;
   }
   ```

2. Create `app/Models/ProfileReviewHistory.php`:
   ```php
   <?php

   namespace App\Models;

   use App\Enums\ProfileStatus;
   use Illuminate\Database\Eloquent\Model;

   class ProfileReviewHistory extends Model
   {
       protected $fillable = ['user_id', 'admin_id', 'status', 'remarks'];

       protected function casts(): array
       {
           return ['status' => ProfileStatus::class];
       }

       public function user()
       {
           return $this->belongsTo(User::class, 'user_id');
       }

       public function admin()
       {
           // Belongs to Admin model (separate from User)
           return $this->belongsTo(Admin::class, 'admin_id');
       }

       public function scopeRejections($query)
       {
           return $query->where('status', ProfileStatus::Rejected);
       }

       public function scopeActionRequired($query)
       {
           return $query->where('status', ProfileStatus::ActionRequired);
       }
   }
   ```
   > `Admin` model confirmed at `app/Models/Admin.php`, namespace `App\Models\Admin`, uses `Notifiable` trait. Use this exact class.

3. Update `app/Models/User.php`:
   - Add `'profile_status'` to `$fillable`
   - Add to `casts()` method: `'profile_status' => \App\Enums\ProfileStatus::class`
   - Add helper methods:
     ```php
     public function isApproved(): bool      { return $this->profile_status === \App\Enums\ProfileStatus::Approved; }
     public function isPendingReview(): bool  { return $this->profile_status === \App\Enums\ProfileStatus::PendingReview; }
     public function isRejected(): bool       { return $this->profile_status === \App\Enums\ProfileStatus::Rejected; }
     public function isActionRequired(): bool { return $this->profile_status === \App\Enums\ProfileStatus::ActionRequired; }
     public function isSuspended(): bool      { return $this->profile_status === \App\Enums\ProfileStatus::Suspended; }
     ```
   - Add relationship:
     ```php
     public function reviewHistories()
     {
         return $this->hasMany(ProfileReviewHistory::class, 'user_id');
     }
     ```

### Done when
- Enum file exists, int-backed, 5 cases
- `ProfileReviewHistory` model with both scopes + both relationships
- `User` model has cast, 5 helpers, `reviewHistories()` relationship

**[ ] SESSION 2 DONE — proceed to Session 3**

---

## Session 3 — Registration Changes

### Pre-session: read these files first
- `app/Http/Controllers/Auth/RegisteredUserController.php` — read full `store()` method before editing
- `app/Models/User.php` — confirm `profile_status` is in `$fillable` (done in Session 2)

### Tasks

1. In `RegisteredUserController::store()`, wrap the `User::create()` call and the new history record in a `DB::transaction()`:
   ```php
   use Illuminate\Support\Facades\DB;
   use App\Enums\ProfileStatus;
   use App\Models\ProfileReviewHistory;

   $user = DB::transaction(function () use ($request, $email) {
       $user = User::create([
           'first_name'     => $request->first_name,
           'last_name'      => $request->last_name,
           'email'          => $email,
           'gender'         => $request->gender,
           'password'       => Hash::make($request->password),
           'steps_completed' => 1,
           'profile_status'  => ProfileStatus::PendingReview,
       ]);

       ProfileReviewHistory::create([
           'user_id'  => $user->id,
           'admin_id' => null,
           'status'   => ProfileStatus::PendingReview,
           'remarks'  => null,
       ]);

       return $user;
   });
   ```
   Keep all existing code before the `User::create()` call (validation, bbwebsite honeypot, email normalization) unchanged.
   > Note: `profile_for` is validated but intentionally NOT in `User::create()` in existing code — preserve this, do not add it.

### Done when
- Register new user → `users.profile_status = 0`
- `profile_review_histories` has one row: `user_id` set, `admin_id` null, `status = 0`
- Both writes atomic (transaction)

**[ ] SESSION 3 DONE — proceed to Session 4**

---

## Session 4 — Middleware + Route Wiring

### Pre-session: read these files first
- `bootstrap/app.php` — see exact `withMiddleware()` block before adding alias
- `routes/user_routes.php` — see full middleware group and existing routes before editing
- `routes/admin_auth.php` — see prefix/guard pattern before adding admin approval routes
- `app/Http/Middleware/CheckProfileCompletion.php` — see pattern for writing similar middleware

### Tasks

1. Create `app/Http/Middleware/EnsureProfileApproved.php`:
   ```php
   <?php

   namespace App\Http\Middleware;

   use App\Enums\ProfileStatus;
   use Closure;
   use Illuminate\Http\Request;

   class EnsureProfileApproved
   {
       public function handle(Request $request, Closure $next)
       {
           $user = $request->user();

           return match ($user->profile_status) {
               ProfileStatus::Approved       => $next($request),
               ProfileStatus::PendingReview  => redirect()->route('profile.pending'),
               ProfileStatus::Rejected       => redirect()->route('profile.rejected'),
               ProfileStatus::Suspended      => redirect()->route('profile.suspended'),
               ProfileStatus::ActionRequired => redirect()->route('profile.action-required'),
               default                       => redirect()->route('profile.pending'),
           };
       }
   }
   ```

2. Register alias in `bootstrap/app.php` inside the `$middleware->alias([...])` array:
   ```php
   'profile.approved' => \App\Http\Middleware\EnsureProfileApproved::class,
   ```

3. Update `routes/user_routes.php`:
   - Insert `profile.approved` into existing member route middleware array, between `verified` and `profile.completed`:
     ```php
     Route::middleware(['auth', 'verified', 'profile.approved', 'profile.completed', 'check.blocked', 'profile.last-active'])->group(function () {
         // existing member routes stay here unchanged
     });
     ```
   - Add status page routes (auth + verified only, no profile.approved):
     ```php
     Route::middleware(['auth', 'verified'])->group(function () {
         Route::get('/profile/pending',        [ProfileStatusController::class, 'pending'])->name('profile.pending');
         Route::get('/profile/rejected',       [ProfileStatusController::class, 'rejected'])->name('profile.rejected');
         Route::get('/profile/suspended',      [ProfileStatusController::class, 'suspended'])->name('profile.suspended');
         Route::get('/profile/action-required',[ProfileStatusController::class, 'actionRequired'])->name('profile.action-required');
         Route::get('/profile/resubmit',       [ProfileResubmitController::class, 'show'])->name('profile.resubmit');
         Route::post('/profile/resubmit',      [ProfileResubmitController::class, 'resubmit']);
     });
     ```
   > Create a stub `ProfileStatusController` with four methods returning stub views — full views in Session 8.

4. Add "Profile Approvals" menu entry to `config/adminlte.php`.
   Add this item inside the `'menu'` array, near the `users` entry (around line 468):
   ```php
   [
       'text'  => 'Profile Approvals',
       'route' => 'admin.approvals.index',
       'icon'  => 'fas fa-fw fa-user-check',
   ],
   ```
   Place it directly after the `users` menu item so it sits in the `account_settings` header section alongside users/admins.

5. Add admin approval routes inside the `auth:admin` group in `routes/admin_auth.php`:
   ```php
   Route::get('approvals',                           [ProfileApprovalController::class, 'index'])->name('approvals.index');
   Route::get('approvals/{user}',                    [ProfileApprovalController::class, 'show'])->name('approvals.show');
   Route::patch('approvals/{user}/approve',          [ProfileApprovalController::class, 'approve'])->name('approvals.approve');
   Route::patch('approvals/{user}/reject',           [ProfileApprovalController::class, 'reject'])->name('approvals.reject');
   Route::patch('approvals/{user}/request-action',   [ProfileApprovalController::class, 'requestAction'])->name('approvals.request-action');
   ```
   These will be named `admin.approvals.index`, `admin.approvals.show`, etc. (inheriting `admin.` prefix).
   Add the `use` import for `ProfileApprovalController` at top of file.

### Done when
- Middleware file created
- Alias registered in `bootstrap/app.php`
- Member routes now include `profile.approved` in chain
- Status page routes defined (can return stub views)
- Admin approval routes defined (can point to empty controller for now)
- "Profile Approvals" appears in admin sidebar (`config/adminlte.php` updated)

**[ ] SESSION 4 DONE — proceed to Session 5**

---

## Session 5 — Admin Controller (ProfileApprovalController)

### Pre-session: read these files first
- `app/Http/Controllers/Admin/UsersController.php` — see how existing admin controllers access auth guard, paginate, pass data to views
- `app/Models/User.php` — confirm `reviewHistories()` relationship exists (Session 2)
- `app/Models/ProfileReviewHistory.php` — confirm scopes (Session 2)
- `app/Models/Admin.php` (or wherever Admin model is) — confirm class path for admin relationship

### Tasks

Create `app/Http/Controllers/Admin/ProfileApprovalController.php`:

```php
<?php

namespace App\Http\Controllers\Admin;

use App\Enums\ProfileStatus;
use App\Http\Controllers\Controller;
use App\Models\ProfileReviewHistory;
use App\Models\User;
use App\Notifications\ProfileActionRequiredNotification;
use App\Notifications\ProfileApprovedNotification;
use App\Notifications\ProfileRejectedNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class ProfileApprovalController extends Controller
{
    public function index()
    {
        $users = User::where('profile_status', ProfileStatus::PendingReview)
            ->latest()
            ->paginate(20);

        return view('admin.approvals.index', compact('users'));
    }

    public function show(User $user)
    {
        $histories = $user->reviewHistories()->with('admin')->latest()->get();
        $rejectionCount      = $user->reviewHistories()->rejections()->count();
        $actionRequiredCount = $user->reviewHistories()->actionRequired()->count();

        return view('admin.approvals.show', compact('user', 'histories', 'rejectionCount', 'actionRequiredCount'));
    }

    public function approve(Request $request, User $user)
    {
        $request->validate(['remarks' => ['nullable', 'string', 'max:1000']]);

        DB::transaction(function () use ($request, $user) {
            $user->update(['profile_status' => ProfileStatus::Approved]);
            ProfileReviewHistory::create([
                'user_id'  => $user->id,
                'admin_id' => auth('admin')->id(),
                'status'   => ProfileStatus::Approved,
                'remarks'  => $request->remarks,
            ]);
        });

        $user->notify(new ProfileApprovedNotification($request->remarks));

        return redirect()->route('admin.approvals.index')->with('success', 'Profile approved.');
    }

    public function reject(Request $request, User $user)
    {
        $request->validate(['remarks' => ['required', 'string', 'max:1000']]);

        DB::transaction(function () use ($request, $user) {
            $user->update(['profile_status' => ProfileStatus::Rejected]);
            ProfileReviewHistory::create([
                'user_id'  => $user->id,
                'admin_id' => auth('admin')->id(),
                'status'   => ProfileStatus::Rejected,
                'remarks'  => $request->remarks,
            ]);
        });

        $user->notify(new ProfileRejectedNotification($request->remarks));

        return redirect()->route('admin.approvals.index')->with('success', 'Profile rejected.');
    }

    public function requestAction(Request $request, User $user)
    {
        $request->validate(['remarks' => ['required', 'string', 'max:1000']]);

        DB::transaction(function () use ($request, $user) {
            $user->update(['profile_status' => ProfileStatus::ActionRequired]);
            ProfileReviewHistory::create([
                'user_id'  => $user->id,
                'admin_id' => auth('admin')->id(),
                'status'   => ProfileStatus::ActionRequired,
                'remarks'  => $request->remarks,
            ]);
        });

        $user->notify(new ProfileActionRequiredNotification($request->remarks));

        return redirect()->route('admin.approvals.index')->with('success', 'Action requested.');
    }
}
```

### Done when
- Controller file exists with all 5 methods
- `auth('admin')->id()` used (not `auth()->id()`)
- Transactions wrap dual writes
- Notifications dispatched (stubs OK — built in Session 7)

**[ ] SESSION 5 DONE — proceed to Session 6**

---

## Session 6 — User Resubmit Controller

### Pre-session: read these files first
- `app/Http/Controllers/ProfileController.php` or `ProfileUpdateController.php` — see how profile data is loaded and passed to views
- `app/Models/User.php` — confirm `isActionRequired()`, `isRejected()`, `reviewHistories()` exist
- `routes/user_routes.php` — confirm resubmit routes added in Session 4

### Tasks

1. Create `app/Http/Controllers/ProfileResubmitController.php`:
   ```php
   <?php

   namespace App\Http\Controllers;

   use App\Enums\ProfileStatus;
   use App\Models\ProfileReviewHistory;
   use Illuminate\Http\Request;
   use Illuminate\Support\Facades\DB;

   class ProfileResubmitController extends Controller
   {
       private function authorizeStatus(): void
       {
           $user = auth()->user();
           if (! $user->isActionRequired() && ! $user->isRejected()) {
               abort(403);
           }
       }

       public function show()
       {
           $this->authorizeStatus();

           $lastRemarks = auth()->user()
               ->reviewHistories()
               ->whereNotNull('admin_id')
               ->latest()
               ->value('remarks');

           return view('profile.resubmit', compact('lastRemarks'));
       }

       public function resubmit(Request $request)
       {
           $this->authorizeStatus();

           DB::transaction(function () {
               auth()->user()->update(['profile_status' => ProfileStatus::PendingReview]);
               ProfileReviewHistory::create([
                   'user_id'  => auth()->id(),
                   'admin_id' => null,
                   'status'   => ProfileStatus::PendingReview,
                   'remarks'  => null,
               ]);
           });

           return redirect()->route('profile.pending');
       }
   }
   ```

2. Create stub `app/Http/Controllers/ProfileStatusController.php` (if not done in Session 4):
   ```php
   <?php

   namespace App\Http\Controllers;

   class ProfileStatusController extends Controller
   {
       public function pending()       { return view('profile.status.pending'); }
       public function rejected()      { return view('profile.status.rejected', ['lastRemarks' => auth()->user()->reviewHistories()->rejections()->latest()->value('remarks'), 'rejectionCount' => auth()->user()->reviewHistories()->rejections()->count()]); }
       public function suspended()     { return view('profile.status.suspended'); }
       public function actionRequired(){ return view('profile.status.action-required', ['lastRemarks' => auth()->user()->reviewHistories()->actionRequired()->latest()->value('remarks')]); }
   }
   ```

### Done when
- User with `ActionRequired` or `Rejected` can access `/profile/resubmit`
- After POST resubmit: `profile_status = 0` in DB, new history row created
- User with other statuses gets 403
- Redirect to `profile.pending` after resubmit

**[x] SESSION 6 DONE — proceed to Session 7**

---

## Session 7 — Notifications

### Pre-session: read these files first
- `config/mail.php` — confirm mail driver/transport in use
- `.env` — check MAIL_ settings
- `app/Providers/OutlookMailServiceProvider.php` — understand custom mail transport so notifications send correctly
- Look at any existing Mailable or class that sends email for patterns

### Tasks

Run:
```
php artisan make:notification ProfileApprovedNotification
php artisan make:notification ProfileRejectedNotification
php artisan make:notification ProfileActionRequiredNotification
```

1. `app/Notifications/ProfileApprovedNotification.php`:
   ```php
   <?php

   namespace App\Notifications;

   use Illuminate\Notifications\Messages\MailMessage;
   use Illuminate\Notifications\Notification;

   class ProfileApprovedNotification extends Notification
   {
       public function __construct(private readonly ?string $remarks = null) {}

       public function via(object $notifiable): array
       {
           return ['mail'];
       }

       public function toMail(object $notifiable): MailMessage
       {
           $mail = (new MailMessage)
               ->subject('Your Profile Has Been Approved')
               ->greeting('Congratulations, ' . $notifiable->first_name . '!')
               ->line('Your profile has been reviewed and approved. You now have full access.');

           if ($this->remarks) {
               $mail->line('Note from admin: ' . $this->remarks);
           }

           return $mail->action('Go to Dashboard', route('dashboard'));
       }
   }
   ```

2. `app/Notifications/ProfileRejectedNotification.php`:
   ```php
   public function __construct(private readonly string $remarks) {}

   // subject: 'Your Profile Was Not Approved'
   // greeting: 'Hello, ' . $notifiable->first_name
   // line: 'Unfortunately your profile was not approved.'
   // line: 'Reason: ' . $this->remarks
   // action: 'Contact Support', route('contact') or '#'
   ```

3. `app/Notifications/ProfileActionRequiredNotification.php`:
   ```php
   public function __construct(private readonly string $remarks) {}

   // subject: 'Action Required on Your Profile'
   // greeting: 'Hello, ' . $notifiable->first_name
   // line: 'Your profile requires changes before it can be approved.'
   // line: 'Admin note: ' . $this->remarks
   // action: 'Update Profile', route('profile.resubmit')
   ```

### Done when
- Three notification classes in `app/Notifications/`
- Each has correct constructor, `via()` returns `['mail']`, `toMail()` builds correct message
- Dispatched from `ProfileApprovalController` (Session 5 stubs replaced)

**[x] SESSION 7 DONE — proceed to Session 8**

---

## Session 8 — Blade Views (User-facing)

### Pre-session: read these files first
- `resources/views/layouts/app.blade.php` — see exact `@extends` name and available `@yield`/`@section` slots
- An existing user-facing blade view (e.g. `resources/views/home.blade.php` or `resources/views/profile/`) — see how they extend the layout, what CSS classes used
- `resources/views/profile/` — list existing files to avoid overwriting

### Tasks

Create directory `resources/views/profile/status/` if it does not exist.

1. `resources/views/profile/status/pending.blade.php`
   - Extend `layouts.app`
   - Message: "Your profile is under review"
   - Subtext: "Our team is reviewing your profile. You'll receive an email once approved."
   - No action button needed

2. `resources/views/profile/status/rejected.blade.php`
   - Show `$lastRemarks` in an alert box
   - Show `$rejectionCount` ("This is rejection #N")
   - Note about contacting support

3. `resources/views/profile/status/action-required.blade.php`
   - Show `$lastRemarks` in an alert/notice box
   - Button: "Update Profile" → `route('profile.resubmit')`

4. `resources/views/profile/status/suspended.blade.php`
   - Message: "Your account has been suspended"
   - Contact support note

5. `resources/views/profile/resubmit.blade.php`
   - Show `$lastRemarks` at top as a warning/info alert
   - Profile edit form — **reuse or include the existing profile edit form partial** (read `resources/views/profile/` to find it, do not duplicate fields)
   - Submit button label: "Resubmit for Approval"
   - Form action: `route('profile.resubmit')`, method POST

### Done when
- All 5 views render without Blade errors
- Resubmit form submits to correct POST route
- Status views accessible at correct URLs

**[x] SESSION 8 DONE — proceed to Session 9**

---

## Session 9 — Blade Views (Admin)

### Pre-session: read these files first
- `resources/views/admin/users/index.blade.php` — see exact AdminLTE `@extends`/`@section` names, how tables are built, pagination pattern
- `resources/views/admin/users/show.blade.php` — see how user detail is displayed
- `resources/views/vendor/adminlte/master.blade.php` — confirm section names if needed
- `routes/admin_auth.php` — confirm exact route names (`admin.approvals.*`) before writing `route()` calls in views

### Tasks

Create directory `resources/views/admin/approvals/`.

1. `resources/views/admin/approvals/index.blade.php`
   - Extend AdminLTE layout (match pattern in `admin/users/index.blade.php`)
   - Table columns: Name, Email, Registered date, Action (link to `admin.approvals.show`)
   - `$users->links()` for pagination
   - Show success/error flash messages

2. `resources/views/admin/approvals/show.blade.php`
   - User info section: name, email, gender, registered date
   - Stats badges: rejection count, action-required count
   - Review history table columns: Date, Status (badge, color-coded by status), Admin Name, Remarks
   - Action forms section — three separate `<form>` blocks:
     - **Approve**: `method="POST"` + `@method('PATCH')` → `route('admin.approvals.approve', $user)`, `remarks` textarea (optional), submit "Approve"
     - **Reject**: → `route('admin.approvals.reject', $user)`, `remarks` textarea (required), submit "Reject"
     - **Request Action**: → `route('admin.approvals.request-action', $user)`, `remarks` textarea (required), submit "Request Action"
   - All forms include `@csrf`
   - Status badge colors: Pending=yellow, Approved=green, Rejected=red, ActionRequired=orange, Suspended=gray

### Done when
- Index lists pending users, paginated
- Show page renders history table + three action forms
- All three forms POST to correct PATCH routes
- Flash messages display

**[x] SESSION 9 DONE — proceed to Session 10**

---

## Session 10 — Testing & Cleanup

### Pre-session: read these files first
- `tests/Feature/` — list existing test files to see naming convention
- `.env` — confirm DB connection for tests (MySQL, not SQLite)

### Tasks

1. Manual flow test — run `php artisan serve` + `php artisan queue:listen`:
   - [ ] Register → `profile_status = 0`, history row created
   - [ ] Login → hit `/home` → redirect to `/profile/pending` ✓
   - [ ] Verify email → still redirects to pending (not a full access bypass) ✓
   - [ ] Admin approves from panel → user gets email → user hits `/home` → full access ✓
   - [ ] Admin rejects → user hits `/profile/rejected` → sees remarks, count ✓
   - [ ] Admin requests action → user sees `/profile/action-required` → clicks update → fills form → resubmit → back to pending ✓
   - [ ] Resubmit from non-ActionRequired/Rejected status → 403 ✓

2. Write feature test `tests/Feature/ProfileApprovalTest.php`:
   - `test_registration_creates_pending_review_history`
   - `test_pending_user_cannot_access_dashboard`
   - `test_approved_user_can_access_dashboard`
   - `test_admin_can_approve_user`
   - `test_admin_can_reject_user_with_remarks`
   - `test_admin_reject_requires_remarks`
   - `test_user_can_resubmit_when_action_required`
   - `test_user_cannot_resubmit_when_approved`

3. Run code style: `vendor/bin/pint`

4. Run full test suite: `php artisan test`

5. Mark all sessions done in Progress Tracker above.

### Done when
- All manual flows pass
- `php artisan test` green
- No pint violations

**[ ] SESSION 10 DONE — Implementation complete**

---

## Critical Gotchas (read before any session)

| # | Gotcha | Correct approach |
|---|--------|-----------------|
| 1 | Admin auth guard | Use `auth('admin')` — NOT `auth()`. Different guard, different model. |
| 2 | Admin FK in history table | References `admins` table — NOT `users` table |
| 3 | Admin route prefix | `config('banibanna.admin_dir')` — NOT hardcoded `/admin` |
| 4 | `profile.approved` middleware position | Must be BEFORE `profile.completed` in chain |
| 5 | User model `casts()` | Is a method not a property — add to return array inside method |
| 6 | No existing notifications | Create from scratch with `make:notification` |
| 7 | DB transactions | Every action writing to both `users` + `profile_review_histories` needs `DB::transaction()` |
| 8 | Admin model class | `app/Models/Admin.php`, namespace `App\Models\Admin` — confirmed |
| 9 | Existing user lockout | Migration default 0 = PendingReview. Must backfill `profile_status = 1` for all users where `email_verified_at IS NOT NULL` inside migration `up()` |
| 10 | `CheckProfileCompletion` conflict | Status page routes must NOT include `profile.completed` middleware — use `['auth', 'verified']` only |
