HR — Approver Management Documentation

Version: 1.0
Last updated: December 10, 2025

Table of Contents


1. Overview

HR Approver Management is a module within the HR System for managing approval workflows in the DTR System. HR managers and administrators can:

  • View assigned approvers for their department/organization
  • Assign approvers for leave requests
  • Assign approvers for overtime requests
  • Manage approver assignments by department or team
  • Track approval authority and hierarchy
  • Delegate approval responsibility to backup approvers
  • Monitor approver workload and pending approvals
  • Generate approver coverage reports

Key files involved:


2. Purpose & Scope

Purpose:

  • Centralized management of approval workflows within HR context
  • Assign and manage approvers per department or team
  • Ensure proper authorization hierarchy within HR operations
  • Facilitate delegation of approval authority
  • Track approval assignments and changes
  • Support leave and overtime request workflows
  • Monitor approver availability and workload

Scope:

  • View all approvers in organization or filtered by department
  • Create new approver assignments
  • Update existing approver assignments
  • Delete/remove approver assignments
  • Filter approvers by type (leave, overtime, all)
  • Search approvers by name, department, email
  • Track approver creation and modifications
  • Manage delegate/backup approvers
  • Generate approver reports
  • View pending requests per approver

Out of scope:

  • Approval workflow execution (documented separately)
  • Leave request processing (documented separately)
  • Overtime request processing (documented separately)
  • System-wide user management (documented in Admin)
  • Permission/role assignment (documented in Admin)

3. Access & Permissions

Required permissions:

  • view_approvers — view approver list and details
  • create_approvers — create new approver assignments
  • edit_approvers — modify existing approver assignments
  • delete_approvers — remove approver assignments
  • view_hr_dashboard — access HR module (includes approver management)

Recommended roles:

  • HR Director — full CRUD on approvers, view all departments, analytics
  • HR Manager — create/edit/delete approvers for own department(s)
  • HR Specialist — view approvers, assist with assignments
  • Department Manager — view approvers for own team (read-only)
  • Team Lead — view approvers assigned to own team (read-only)
  • Employee — view own approver (read-only)

Implementation note:

  • Enforce RBAC server-side via middleware or gate checks
  • UI hides approver management actions for non-HR roles
  • HR managers can only manage approvers in their assigned departments
  • Filter approver list based on user's department scope
  • Use authorize() in controller methods

4. How to Access

HR URLs:

  • Approvers index: /hr/approvers
  • Filter by type: /hr/approvers?type=leave or /hr/approvers?type=overtime
  • Filter by department: /hr/approvers?department={id}
  • Create approver: /hr/approvers/create
  • View approver: /hr/approvers/{id} (optional detail view)
  • Edit approver: /hr/approvers/{id}/edit
  • Delete approver: /hr/approvers/{id}/delete (POST with confirmation)
  • Search approvers: /hr/approvers?search={query}

Navigation path:

  • Main menu: HR → Approver Management
  • Or: HR Dashboard → Quick Links → Manage Approvers

Browser requirements: Modern browsers (Chrome, Firefox, Safari, Edge). Mobile responsive design.


5. Interface Layout & Views

5.1 Index (Approvers List)

Purpose: Display all approvers with filtering, search, and management actions.

View file: resources/views/users/hr/approver/index.blade.php

Top section - Filters & Search:

  • Department filter (dropdown):

    • All departments (default)
    • Specific department names
    • Shows only departments user has access to
  • Type filter (dropdown):

    • All (default)
    • Leave Approvers
    • Overtime Approvers
  • Search bar:

    • Placeholder: "Search by approver name, department, email..."
    • Real-time or on-submit search
    • Filters results by name, department, email
  • Quick actions:

    • Create Approver (button, if permission granted)
    • Export List (CSV/Excel, optional)
    • Refresh button

Approvers table - Columns:

Column Content Notes
Approver Name First + Last name Linked to user profile
Email User email address Linked to send email
Department Department name Shows approver's department
Approval Type Leave / Overtime / Both Badge styled
Delegate Approver Backup approver name Shows if assigned
Pending Requests Number count (Optional) Shows pending requests
Status Active / Inactive Green/Gray badge
Created By Admin user name Who created assignment
Created At Date/Time Timestamp
Actions Edit, Delete Buttons/Links

Table features:

  • Pagination: 10-25 approvers per page (configurable)
  • Sorting: By department, approval type, status (optional)
  • Row styling:
    • Inactive approvers: grayed out or lighter background
    • Hover effect: Highlight row
    • Clickable row: Navigate to detail/edit
  • Bulk actions (optional):
    • Select multiple approvers
    • Bulk deactivate / activate
    • Bulk delete (with confirmation)

Empty state:

  • Message: "No approvers found" or "No approvers in your department"
  • Call to action: "Create your first approver assignment"
  • Link to create approver form

Controller method: ApproverController::index(Request $request) (in HR namespace)

public function index(Request $request)
{
    $department = $request->get('department');
    $type = $request->get('type', 'all');
    $search = $request->get('search');

    $query = Approver::with(['user.profile', 'department', 'delegateApprover.profile'])
        ->when(auth()->user()->cannot('view_all_approvers'), function ($query) {
            // Filter by user's departments only
            $query->whereIn('department_id', auth()->user()->departments()->pluck('id'));
        })
        ->when($department, function ($query) use ($department) {
            $query->where('department_id', $department);
        })
        ->when($type !== 'all', function ($query) use ($type) {
            $query->where('approver_for', $type);
        })
        ->when($search, function ($query) use ($search) {
            $query->whereHas('user.profile', function ($q) use ($search) {
                $q->where('first_name', 'like', "%{$search}%")
                  ->orWhere('last_name', 'like', "%{$search}%")
                  ->orWhere('email', 'like', "%{$search}%");
            });
        })
        ->orderBy('department_id')
        ->orderBy('approver_for')
        ->orderBy('created_at', 'desc');

    $approvers = $query->paginate(10);
    $departments = Department::all(); // For filter dropdown

    return view('users.hr.approver.index', compact('approvers', 'departments'));
}

5.2 Create Approver

Purpose: Create a new approver assignment for leave or overtime requests.

Trigger: "Create Approver" button on index view.

View file: resources/views/users/hr/approver/create.blade.php

Form fields:

  • User (required, dropdown/searchable):

    • List of eligible users (with appropriate roles)
    • Search by name or email if long list
    • Display format: "Name (email)" or "Name — Department"
    • Validation: Must exist in users table, must have appropriate role
  • Department (required, dropdown):

    • List of departments (filtered to user's accessible departments if HR Manager)
    • Display: Department name with location (optional)
    • Validation: Must exist in departments table
  • Approval Type (required, radio buttons or dropdown):

    • Leave — Approves leave requests
    • Overtime — Approves overtime requests
    • Validation: Must be leave or overtime
  • Delegate Approver (optional, dropdown/searchable):

    • List of eligible users with appropriate roles
    • Backup approver (when primary is unavailable)
    • Display: "Name (email)"
    • Validation: Must exist, must have appropriate role, different from primary user
  • Active (optional, checkbox):

    • Toggle approver status (default: checked/active)
    • Validation: Boolean
  • Notes (optional, text area):

    • Additional notes about the approver assignment
    • Character limit: 500
    • Display: "Any special notes about this approver?"

Form submission (POST /hr/approvers):

  1. Validate all fields server-side
  2. Verify primary user has appropriate role (hr, admin, tl, or approver role)
  3. Verify delegate user (if provided) has appropriate role
  4. Check for duplicate assignments (same user + type + department)
  5. Create Approver record with:
    • user_id (primary approver)
    • department_id
    • approver_for (leave or overtime)
    • delegate_approver_id (optional)
    • active (default true)
    • notes (optional)
    • created_by (auth user ID)
  6. Create audit log entry: "Approver created: [User] as [Type] approver for [Department]"
  7. Send notification to new approver (email): "You've been assigned as an approver"
  8. Redirect to index with success message: "Approver assignment created successfully"

Validation rules:

$validated = $request->validate([
    'user_id' => 'required|exists:users,id',
    'department_id' => 'required|exists:departments,id',
    'approver_for' => 'required|in:leave,overtime',
    'active' => 'boolean|default:true',
    'delegate_approver_id' => 'nullable|exists:users,id|different:user_id',
    'notes' => 'nullable|string|max:500'
]);

// Check delegate approver has appropriate role
if ($request->delegate_approver_id) {
    $delegateUser = User::find($request->delegate_approver_id);
    if (!$delegateUser->hasAnyRole(['hr', 'admin', 'tl', 'approver'])) {
        return back()->withErrors([
            'delegate_approver_id' => 'The delegate approver must have appropriate permissions.'
        ])->withInput();
    }
}

// Check for duplicate assignment
$exists = Approver::where('user_id', $request->user_id)
    ->where('department_id', $request->department_id)
    ->where('approver_for', $request->approver_for)
    ->exists();

if ($exists) {
    return back()->withErrors([
        'user_id' => 'This user is already an approver for this type in this department.'
    ])->withInput();
}

Controller method:

public function store(Request $request)
{
    // Authorization check
    $this->authorize('create_approvers');

    $validated = $request->validate([
        'user_id' => 'required|exists:users,id',
        'department_id' => 'required|exists:departments,id',
        'approver_for' => 'required|in:leave,overtime',
        'active' => 'boolean|default:true',
        'delegate_approver_id' => 'nullable|exists:users,id|different:user_id',
        'notes' => 'nullable|string|max:500'
    ]);

    // Verify delegate has appropriate role
    if ($request->delegate_approver_id) {
        $delegateUser = User::find($request->delegate_approver_id);
        if (!$delegateUser->hasAnyRole(['hr', 'admin', 'tl', 'approver'])) {
            return back()->withErrors([
                'delegate_approver_id' => 'Delegate must have appropriate role.'
            ])->withInput();
        }
    }

    // Check duplicate
    $exists = Approver::where('user_id', $request->user_id)
        ->where('department_id', $request->department_id)
        ->where('approver_for', $request->approver_for)
        ->exists();

    if ($exists) {
        return back()->withErrors([
            'user_id' => 'Already assigned this role in this department.'
        ])->withInput();
    }

    $approver = Approver::create([
        ...$validated,
        'created_by' => auth()->id(),
    ]);

    // Audit log
    activity()
        ->causedBy(auth()->user())
        ->performedOn($approver)
        ->withProperties([
            'user_name' => User::find($validated['user_id'])->name,
            'department_id' => $validated['department_id'],
            'type' => $validated['approver_for']
        ])
        ->log('approver_created');

    // Notification
    // Notification::send($approver->user, new ApproverAssigned($approver));

    return redirect()->route('hr.approvers.index')
        ->with('success', 'Approver assigned successfully');
}

5.3 Edit Approver

Purpose: Modify existing approver assignment (change department, type, delegate, or status).

Trigger: Click "Edit" button next to approver in index view.

View file: resources/views/users/hr/approver/edit.blade.php

Form fields:

Same as create form, except:

  • User (read-only or disabled):

    • Cannot change the primary approver
    • Display only for clarity
    • Shows: "Name (ID: user_id)"
  • Department (required, dropdown):

    • Can change department
    • Current selection highlighted
    • Validation: Must exist
  • Approval Type (required, radio buttons or dropdown):

    • Can change from leave to overtime or vice versa
    • Current selection pre-selected
    • Validation: Must be leave or overtime
  • Delegate Approver (optional, dropdown):

    • Can update or remove delegate
    • Current selection highlighted
    • Validation: Must have appropriate role
  • Active (optional, checkbox):

    • Toggle approver status
    • Current state indicated
    • Unchecking deactivates approver
  • Notes (optional, text area):

    • Can update notes
    • Shows current notes

Confirmation details (if applicable):

  • Show if changes affect pending requests
  • Display pending request count (if any)
  • Warning: "This approver has X pending requests"

Form submission (PUT /hr/approvers/{id}):

  1. Validate fields
  2. Check authorization (can user edit this approver?)
  3. Verify delegate user (if provided) has appropriate role
  4. Update Approver record
  5. Create audit log entry with changes:
    • Show old values vs new values
    • Track what was changed
  6. Send notification if approval responsibility changed significantly
  7. Redirect to index with success message

Validation rules:

$validated = $request->validate([
    'department_id' => 'required|exists:departments,id',
    'approver_for' => 'required|in:leave,overtime',
    'active' => 'boolean|default:true',
    'delegate_approver_id' => 'nullable|exists:users,id|different:approver.user_id',
    'notes' => 'nullable|string|max:500'
]);

if ($request->delegate_approver_id) {
    $delegateUser = User::find($request->delegate_approver_id);
    if (!$delegateUser->hasAnyRole(['hr', 'admin', 'tl', 'approver'])) {
        return back()->withErrors([
            'delegate_approver_id' => 'Delegate must have appropriate role.'
        ])->withInput();
    }
}

Controller method:

public function update(Request $request, Approver $approver)
{
    // Authorization check
    $this->authorize('edit_approvers');

    $validated = $request->validate([
        'department_id' => 'required|exists:departments,id',
        'approver_for' => 'required|in:leave,overtime',
        'active' => 'boolean|default:true',
        'delegate_approver_id' => 'nullable|exists:users,id|different:' . $approver->user_id,
        'notes' => 'nullable|string|max:500'
    ]);

    if ($request->delegate_approver_id) {
        $delegateUser = User::find($request->delegate_approver_id);
        if (!$delegateUser->hasAnyRole(['hr', 'admin', 'tl', 'approver'])) {
            return back()->withErrors([
                'delegate_approver_id' => 'Delegate must have appropriate role.'
            ])->withInput();
        }
    }

    $oldValues = $approver->only(array_keys($validated));
    $approver->update($validated);

    // Audit log with changes
    activity()
        ->causedBy(auth()->user())
        ->performedOn($approver)
        ->withProperties([
            'old' => $oldValues,
            'new' => $validated
        ])
        ->log('approver_updated');

    return redirect()->route('hr.approvers.index')
        ->with('success', 'Approver assignment updated successfully');
}

Delete Approver:

Trigger: Click "Delete" button or link on index view.

Confirmation modal:

Are you sure you want to remove this approver?

User: [Name]
Type: [Leave / Overtime]
Department: [Department]
Pending Requests: X

Options:
[ Cancel ]  [ Confirm Delete ]

Flow:

  1. Show confirmation modal with:
    • Approver details
    • Pending request count (if any)
    • Warning about pending requests
  2. On confirmation:
    • Delete Approver record (soft delete or hard delete)
    • Create audit log: "Approver removed: [User Name]"
    • Handle pending requests (if any):
      • Option 1: Auto-reassign to delegate
      • Option 2: Mark as unassigned (requires re-approval)
      • Option 3: Fail/reject pending requests
    • Send notification to affected users
    • Redirect with success message

Controller method:

public function destroy(Approver $approver)
{
    // Authorization check
    $this->authorize('delete_approvers');

    // Check for pending requests
    $pendingCount = LeaveRequest::where('approver_id', $approver->user_id)
        ->where('status', 'pending')
        ->count();

    if ($pendingCount > 0) {
        // Handle pending requests
        // Option 1: Auto-reassign to delegate
        if ($approver->delegate_approver_id) {
            LeaveRequest::where('approver_id', $approver->user_id)
                ->where('status', 'pending')
                ->update(['approver_id' => $approver->delegate_approver_id]);
        }
    }

    // Audit log
    activity()
        ->causedBy(auth()->user())
        ->performedOn($approver)
        ->withProperties([
            'user_name' => $approver->user->name,
            'pending_requests' => $pendingCount
        ])
        ->log('approver_deleted');

    $approver->delete();

    return redirect()->route('hr.approvers.index')
        ->with('success', 'Approver assignment removed successfully');
}

6. Key Features

  • Department-based management: Assign approvers per department
  • Flexible approval types: Leave and overtime approvers
  • Delegation support: Assign backup/delegate approvers
  • Status management: Activate/deactivate without deleting
  • Role validation: Ensure approvers have appropriate permissions
  • Search & filter: Find approvers by name, department, type
  • Audit trail: Track all assignment changes
  • Pending request tracking: See workload per approver
  • Bulk actions: Manage multiple approvers (optional)
  • Export capability: Download approver list (optional)
  • Status indicators: Visual badges for active/inactive
  • Department scoping: Only manage approvers in accessible departments

7. Data Model & Database

Approver table fields:

id                    — Primary key
user_id               — Foreign key to users (primary approver)
department_id         — Foreign key to departments
approver_for          — Enum: 'leave' or 'overtime'
delegate_approver_id  — Foreign key to users (nullable, backup)
active                — Boolean (default true)
notes                 — Text (nullable, max 500 chars)
created_by            — Foreign key to users (who created)
created_at            — Timestamp
updated_at            — Timestamp
deleted_at            — Timestamp (soft delete, nullable)

Sample records:

Approver 1:
├─ id: 1
├─ user_id: 5 (Jane Doe)
├─ department_id: 1 (HR Department)
├─ approver_for: 'leave'
├─ delegate_approver_id: 8 (John Smith)
├─ active: true
├─ notes: "Primary leave approver for HR"
├─ created_by: 1 (Admin ID)
└─ created_at: 2025-01-01 09:00:00

Approver 2:
├─ id: 2
├─ user_id: 6 (Alice Johnson)
├─ department_id: 2 (Operations)
├─ approver_for: 'overtime'
├─ delegate_approver_id: null
├─ active: true
├─ notes: null
├─ created_by: 1
└─ created_at: 2025-01-05 10:30:00

Relationships:

  • Approver belongsTo User (primary approver)
  • Approver belongsTo User as delegateApprover (backup)
  • Approver belongsTo Department
  • Approver belongsTo User as createdBy (who created)
  • User hasMany Approver (user can be approver in multiple departments)
  • Department hasMany Approver

8. Approver Assignment Workflows

Leave Request Approvers

Approval Type: leave
├─ Responsibility: Review & approve employee leave requests
├─ Approval Authority: Accept/Reject leave requests
├─ Scope: Specific department
└─ Typical Users: HR Manager, HR Specialist, Team Lead

Example:
  Jane Doe (HR Manager) approves leave for HR Department
  - Handles all leave requests from HR employees
  - Can delegate to John Smith (HR Backup)
  - Can approve or reject requests

Overtime Request Approvers

Approval Type: overtime
├─ Responsibility: Review & approve overtime requests
├─ Approval Authority: Accept/Reject overtime requests
├─ Scope: Specific department
└─ Typical Users: Department Manager, Operations Manager

Example:
  Alice Johnson (Operations Manager) approves overtime for Operations
  - Handles overtime requests from Operations team
  - Ensures budget compliance
  - Can delegate to backup manager

Hierarchy by Department

HR Department:
├─ Leave Approver: Jane Doe (HR Manager)
│  └─ Delegate: John Smith (HR Specialist)
└─ Overtime Approver: Jane Doe (HR Manager)
   └─ No delegate

Operations Department:
├─ Leave Approver: Alice Johnson (Ops Manager)
│  └─ No delegate
└─ Overtime Approver: David Lee (Ops Director)
   └─ Delegate: Emma Brown (Ops Manager)

Finance Department:
├─ Leave Approver: Frank Martinez (Finance Manager)
│  └─ Delegate: Grace Kim (Finance Specialist)
└─ Overtime Approver: Frank Martinez (Finance Manager)
   └─ Delegate: Grace Kim

9. Common Tasks & Workflows

Task 1: Create Leave Approver for Department

  1. Navigate: HR → Approver Management
  2. Click "Create Approver"
  3. User: Jane Doe (HR Manager)
  4. Department: HR Department
  5. Type: Leave
  6. Delegate: John Smith
  7. Click "Create"
  8. Result: Jane assigned as leave approver ✓

Task 2: Assign Approver for Multiple Types

  1. Create approver: Jane → HR → Leave
  2. Create approver: Jane → HR → Overtime
  3. Jane now approves both types in HR ✓

Task 3: Update Delegate Approver

  1. Navigate: HR → Approver Management
  2. Find: Jane Doe, Leave, HR
  3. Click "Edit"
  4. Change Delegate: John Smith → Carol White
  5. Click "Update"
  6. Carol now backs up Jane ✓

Task 4: Deactivate Approver (Temporary)

  1. Navigate: HR → Approver Management
  2. Find: Bob Wilson (Overtime, Operations)
  3. Click "Edit"
  4. Uncheck "Active"
  5. Click "Update"
  6. Requests route to delegate Emma Brown ✓

Task 5: Remove Approver

  1. Navigate: HR → Approver Management
  2. Find: Bob Wilson (Overtime, Operations)
  3. Click "Delete"
  4. Confirm: "Remove as approver?"
  5. Handle pending requests
  6. Bob removed as approver ✓

Task 6: View All Approvers by Type

  1. Navigate: HR → Approver Management
  2. Filter: Type = "Leave"
  3. View all leave approvers by department ✓

Task 7: Search for Specific Approver

  1. On Approver Management page
  2. Search: "Jane Doe"
  3. View all approvals for Jane ✓

Task 8: Filter by Department

  1. Navigate: HR → Approver Management
  2. Filter: Department = "Operations"
  3. View all approvers in Operations ✓

10. Integrations (Leave / Overtime)

Leave Request Integration:

  • When leave request submitted: System finds approver for that department + leave type
  • Approver determined by: Department + approver_for='leave' + active=true
  • If primary inactive: Routes to delegate approver (if assigned)
  • Approver receives notification: New leave request pending
  • Approver can approve/reject: Updates leave request status
  • Notification sent to employee: Approval/rejection status

Overtime Request Integration:

  • When overtime submitted: System finds approver for department + overtime type
  • Approver determined by: Department + approver_for='overtime' + active=true
  • If primary inactive: Routes to delegate approver
  • Approver receives notification: New overtime request pending
  • Approver can approve/reject: Updates request status

Auto-routing logic:

// Find approver for request
$approver = Approver::where('department_id', $request->department_id)
    ->where('approver_for', 'leave') // or 'overtime'
    ->where('active', true)
    ->with('delegateApprover')
    ->first();

if (!$approver && isset($approver)) {
    // Primary not active, check delegate
    if ($approver->delegateApprover && $approver->delegateApprover->active) {
        $approver = $approver->delegateApprover;
    } else {
        // No active approver found
        $request->status = 'no_approver';
        // Log warning or notify admin
    }
}

$request->approver_id = $approver->user_id ?? null;
$request->save();

11. Filters, Search & Reports

Quick filters:

  • All approvers
  • Leave approvers only
  • Overtime approvers only
  • By department
  • Active / Inactive

Search functionality:

  • Search by approver name
  • Search by department name
  • Search by email
  • Real-time AJAX search (optional)

Advanced filters (optional):

  • Filter by active status
  • Filter by department
  • Filter by approval type
  • Filter by creation date range
  • Filter by delegate assigned (Yes/No)

Reports:

  • Approver workload: Pending requests per approver
  • Coverage report: Approvers by department and type
  • Delegation report: Primary and delegate approvers
  • Gap analysis: Departments missing approvers
  • Activity report: Approver assignment history

Export formats: CSV, Excel, PDF


12. API Endpoints

GET /hr/approvers

  • Returns index view with approvers list
  • Query params: type (leave, overtime, all), department, search, page
  • Response: HTML view
public function index(Request $request)
{
    $type = $request->get('type', 'all');
    $department = $request->get('department');
    $search = $request->get('search');

    $query = Approver::with(['user.profile', 'department', 'delegateApprover.profile'])
        // ... filtering logic ...
        ->paginate(10);

    return view('users.hr.approver.index', compact('approvers', 'departments'));
}

GET /hr/approvers/create

  • Show approver creation form
  • Returns view with:
    • $users — eligible users
    • $departments — accessible departments
public function create()
{
    $users = User::role(['hr', 'admin', 'tl', 'approver'])->get();
    $departments = auth()->user()->departments ?? Department::all();

    return view('users.hr.approver.create', compact('users', 'departments'));
}

POST /hr/approvers

  • Create new approver
  • Body: { user_id, department_id, approver_for, delegate_approver_id, active, notes }
  • Response: Redirect with success message

GET /hr/approvers/{id}/edit

  • Show approver edit form
  • Returns view with approver data
public function edit(Approver $approver)
{
    $this->authorize('edit_approvers');

    $users = User::role(['hr', 'admin', 'tl', 'approver'])->get();
    $departments = auth()->user()->departments ?? Department::all();

    return view('users.hr.approver.edit', compact('approver', 'users', 'departments'));
}

PUT /hr/approvers/{id}

  • Update approver
  • Body: { department_id, approver_for, active, delegate_approver_id, notes }
  • Response: Redirect with success message

DELETE /hr/approvers/{id}

  • Delete approver
  • Response: Redirect with success message

GET /api/hr/approvers

  • Returns JSON list of approvers
  • Query params: type, department_id, search, page
  • Response: JSON with paginated approvers
{
    "data": [
        {
            "id": 1,
            "user": {
                "id": 5,
                "name": "Jane Doe",
                "email": "jane@company.com"
            },
            "department": { "id": 1, "name": "HR Department" },
            "approver_for": "leave",
            "delegate_approver": { "id": 8, "name": "John Smith" },
            "active": true,
            "created_at": "2025-01-01T09:00:00Z"
        }
    ],
    "pagination": { "total": 10, "per_page": 25 }
}

GET /api/hr/approvers/for-request

  • Get approver for specific request
  • Query params: type, department_id
  • Response: JSON
{
    "approver": { "id": 5, "name": "Jane Doe" },
    "delegate": { "id": 8, "name": "John Smith" }
}

13. Controller Implementation Details

Location: app/Http/Controllers/Web/HR/ApproverController.php (expected)

index() method:

public function index(Request $request)
{
    // Authorization
    $this->authorize('view_approvers');

    $type = $request->get('type', 'all');
    $department = $request->get('department');
    $search = $request->get('search');

    // Base query with relationships
    $query = Approver::with(['user.profile', 'department', 'delegateApprover.profile'])
        // Department scope (only accessible departments)
        ->when(auth()->user()->cannot('view_all_approvers'), function ($query) {
            $query->whereIn('department_id', auth()->user()->departments()->pluck('id'));
        })
        // Filters
        ->when($department, function ($query) use ($department) {
            $query->where('department_id', $department);
        })
        ->when($type !== 'all', function ($query) use ($type) {
            $query->where('approver_for', $type);
        })
        ->when($search, function ($query) use ($search) {
            $query->whereHas('user.profile', function ($q) use ($search) {
                $q->where('first_name', 'like', "%{$search}%")
                  ->orWhere('last_name', 'like', "%{$search}%");
            })->orWhereHas('user', function ($q) use ($search) {
                $q->where('email', 'like', "%{$search}%");
            });
        })
        // Sorting
        ->orderBy('department_id')
        ->orderBy('approver_for')
        ->orderBy('created_at', 'desc');

    $approvers = $query->paginate(10);
    $departments = auth()->user()->departments ?? Department::all();

    return view('users.hr.approver.index', compact('approvers', 'departments'));
}

create() method:

public function create()
{
    $this->authorize('create_approvers');

    $users = User::whereHas('roles', function ($query) {
        $query->whereIn('name', ['hr', 'admin', 'tl', 'approver']);
    })->get();

    $departments = auth()->user()->departments ?? Department::all();

    return view('users.hr.approver.create', compact('users', 'departments'));
}

store() method:

public function store(Request $request)
{
    $this->authorize('create_approvers');

    $validated = $request->validate([
        'user_id' => 'required|exists:users,id',
        'department_id' => 'required|exists:departments,id',
        'approver_for' => 'required|in:leave,overtime',
        'active' => 'boolean|default:true',
        'delegate_approver_id' => 'nullable|exists:users,id|different:user_id',
        'notes' => 'nullable|string|max:500'
    ]);

    // Verify delegate role
    if ($request->delegate_approver_id) {
        $delegateUser = User::find($request->delegate_approver_id);
        if (!$delegateUser->hasAnyRole(['hr', 'admin', 'tl', 'approver'])) {
            return back()->withErrors([
                'delegate_approver_id' => 'Delegate must have appropriate permissions.'
            ])->withInput();
        }
    }

    // Check duplicate
    $exists = Approver::where('user_id', $request->user_id)
        ->where('department_id', $request->department_id)
        ->where('approver_for', $request->approver_for)
        ->exists();

    if ($exists) {
        return back()->withErrors([
            'user_id' => 'Already assigned this role in this department.'
        ])->withInput();
    }

    $approver = Approver::create([
        ...$validated,
        'created_by' => auth()->id(),
    ]);

    activity()
        ->causedBy(auth()->user())
        ->performedOn($approver)
        ->log('approver_created');

    return redirect()->route('hr.approvers.index')
        ->with('success', 'Approver assigned successfully');
}

edit() method:

public function edit(Approver $approver)
{
    $this->authorize('edit_approvers');

    $users = User::whereHas('roles', function ($query) {
        $query->whereIn('name', ['hr', 'admin', 'tl', 'approver']);
    })->get();

    $departments = auth()->user()->departments ?? Department::all();

    return view('users.hr.approver.edit', compact('approver', 'users', 'departments'));
}

update() method:

public function update(Request $request, Approver $approver)
{
    $this->authorize('edit_approvers');

    $validated = $request->validate([
        'department_id' => 'required|exists:departments,id',
        'approver_for' => 'required|in:leave,overtime',
        'active' => 'boolean|default:true',
        'delegate_approver_id' => 'nullable|exists:users,id|different:' . $approver->user_id,
        'notes' => 'nullable|string|max:500'
    ]);

    if ($request->delegate_approver_id) {
        $delegateUser = User::find($request->delegate_approver_id);
        if (!$delegateUser->hasAnyRole(['hr', 'admin', 'tl', 'approver'])) {
            return back()->withErrors([
                'delegate_approver_id' => 'Delegate must have appropriate permissions.'
            ])->withInput();
        }
    }

    $approver->update($validated);

    activity()
        ->causedBy(auth()->user())
        ->performedOn($approver)
        ->log('approver_updated');

    return redirect()->route('hr.approvers.index')
        ->with('success', 'Approver updated successfully');
}

destroy() method:

public function destroy(Approver $approver)
{
    $this->authorize('delete_approvers');

    activity()
        ->causedBy(auth()->user())
        ->performedOn($approver)
        ->log('approver_deleted');

    $approver->delete();

    return redirect()->route('hr.approvers.index')
        ->with('success', 'Approver removed successfully');
}

14. Audit, Notifications & Compliance

Audit logging:

  • Log all approver assignments: user, approver, department, type, created by, timestamp
  • Log all updates: changes, updated by, timestamp
  • Log all deletions: who deleted, when, approver details
  • Maintain immutable audit trail (minimum 1 year retention)

Notifications:

  • Approver assigned → Email: "You've been assigned as {type} approver for {department}"
  • Approver updated → Email if role/responsibility changes
  • Approver removed → Email: "Your approver status has been revoked"
  • Delegate assigned → Email: "You're backup approver for {primary user}"
  • High workload alert → Email if pending requests exceed threshold

Compliance:

  • Approver changes logged for audit
  • Primary and delegate must have appropriate roles
  • Prevent self-delegation
  • Track assignment history
  • Generate compliance reports on request

15. Practical Examples

Example 1: Single Department Setup

HR Department Structure:

Primary Approvers:
├─ Jane Doe (HR Manager)
│  ├─ Approves: Leave, Overtime
│  ├─ Pending: 5 requests
│  └─ Delegate: John Smith

├─ John Smith (HR Specialist)
│  ├─ Approves: Backup for Leave
│  ├─ Pending: 0 requests
│  └─ No delegate

Secondary Approvers:
└─ Carol White (HR Assistant)
   ├─ Approves: Internal use
   └─ Pending: 2 requests

Example 2: Multi-Department Setup

Organization Structure:

HR Department:
├─ Leave: Jane Doe (Delegate: John Smith)
└─ Overtime: Jane Doe (Delegate: Carol White)

Operations Department:
├─ Leave: Alice Johnson (No delegate)
└─ Overtime: David Lee (Delegate: Emma Brown)

Finance Department:
├─ Leave: Frank Martinez (Delegate: Grace Kim)
└─ Overtime: Frank Martinez (Delegate: Grace Kim)

Support Department:
├─ Leave: Helen Davis (No delegate)
└─ Overtime: Ian Brown (No delegate)

Example 3: Approver Transition

Scenario: Jane retiring, transition to Alice

Action Plan:
1. Create: Alice → HR → Leave (Delegate: Jane)
2. Notify: Employees of new approver
3. Training: Jane trains Alice on processes
4. Approval: Alice approves with Jane's guidance
5. Period: 2-week overlap
6. Final: Remove Jane as approver
7. Confirm: Alice is sole approver

Timeline:
├─ Week 1-2: Both active, Alice learning
├─ Week 3: Jane inactive, Alice handling all
└─ Week 4: Jane removed from system

16. Testing & QA Checklist

Unit tests:

  • ✓ Approver::create validates required fields
  • ✓ Approver::update prevents invalid roles for delegate
  • ✓ Delegate cannot be same as primary user
  • ✓ Duplicate assignment prevents creation
  • ✓ Department scoping filters correctly
  • ✓ Soft delete works properly

Integration tests:

  • ✓ Create approver → record in DB with audit log
  • ✓ Update approver → changes saved with history
  • ✓ Delete approver → soft delete applied
  • ✓ Filter by type → returns correct approvers
  • ✓ Filter by department → respects permissions
  • ✓ Search works across name, email, department
  • ✓ Leave request routes to correct approver
  • ✓ Overtime request routes to correct approver
  • ✓ Fallback to delegate when primary inactive

E2E tests (manual):

  • ✓ HR Manager logs in, navigates to approvers
  • ✓ Can view approvers in own departments only
  • ✓ Can create new approver assignment
  • ✓ Can edit existing approver
  • ✓ Can delete approver with confirmation
  • ✓ Filters work correctly
  • ✓ Search finds approvers
  • ✓ Pagination works
  • ✓ Delegation link works
  • ✓ Status indicators show correctly
  • ✓ Mobile responsive

Permission tests:

  • ✓ Non-HR cannot access approver management
  • ✓ HR Manager can only manage own departments
  • ✓ Approver role validation works
  • ✓ Delegate role validation works
  • ✓ Audit logs record actions

17. Troubleshooting & FAQs

Q: "Cannot create approver - user doesn't have role"

  • A:
    • User must have one of: hr, admin, tl, approver
    • Assign appropriate role to user first
    • Contact admin if user needs role

Q: "Approver not found for leave request"

  • A:
    • Check if approver exists for department
    • Verify approver type is 'leave'
    • Check if approver is active
    • Create missing approver via HR → Approver Management

Q: "Cannot edit approver - changes not saving"

  • A:
    • Verify you have edit_approvers permission
    • Check for validation errors (see form)
    • Ensure delegate (if set) has appropriate role
    • Try with different browser

Q: "Delegate not receiving requests"

  • A:
    • Primary approver may still be active
    • Set primary approver to inactive to trigger fallback
    • Verify delegate is properly linked
    • Check delegate has permissions

Q: "See approvers from other departments"

  • A:
    • Department scoping should filter results
    • Check if user has view_all_approvers permission
    • Only HR Manager role can see own departments
    • HR Director can see all departments

Q: "Approver appears twice in list"

  • A:
    • Normal - same user can be approver for multiple types
    • Example: Jane as leave and overtime approver
    • Each type creates separate record

Q: "Cannot delete approver - pending requests"

  • A:
    • Reassign pending requests to delegate
    • Or manually reassign before deleting
    • System should auto-suggest delegate

18. Appendix: Code Examples

Model: Approver with relationships

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Approver extends Model
{
    protected $fillable = [
        'user_id', 'department_id', 'approver_for',
        'delegate_approver_id', 'active', 'notes', 'created_by'
    ];

    protected $casts = [
        'active' => 'boolean',
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

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

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

    public function delegateApprover(): BelongsTo
    {
        return $this->belongsTo(User::class, 'delegate_approver_id');
    }

    public function createdBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'created_by');
    }

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

    public function scopeForLeave($query)
    {
        return $query->where('approver_for', 'leave');
    }

    public function scopeForOvertime($query)
    {
        return $query->where('approver_for', 'overtime');
    }

    public function scopeForDepartment($query, $departmentId)
    {
        return $query->where('department_id', $departmentId);
    }
}

SQL: Find approvers for department

SELECT a.*, u.first_name, u.last_name, u.email, d.name as department_name
FROM approvers a
JOIN users u ON a.user_id = u.id
JOIN departments d ON a.department_id = d.id
WHERE a.department_id = 1 AND a.active = 1
ORDER BY a.approver_for;

SQL: Find approver with delegate

SELECT a.id, u.name as approver, da.name as delegate, d.name as department, a.approver_for
FROM approvers a
JOIN users u ON a.user_id = u.id
LEFT JOIN users da ON a.delegate_approver_id = da.id
JOIN departments d ON a.department_id = d.id
WHERE a.delegate_approver_id IS NOT NULL
ORDER BY d.name;

SQL: Count pending requests per approver

SELECT a.user_id, u.name, COUNT(lr.id) as pending_count
FROM approvers a
JOIN users u ON a.user_id = u.id
LEFT JOIN leave_requests lr ON a.user_id = lr.approver_id
    AND lr.status = 'pending'
WHERE a.approver_for = 'leave' AND a.active = 1
GROUP BY a.user_id
ORDER BY pending_count DESC;

Blade template: Index table

<table class="table">
    <thead>
        <tr>
            <th>Approver</th>
            <th>Email</th>
            <th>Department</th>
            <th>Type</th>
            <th>Delegate</th>
            <th>Status</th>
            <th>Actions</th>
        </tr>
    </thead>
    <tbody>
        @foreach($approvers as $approver)
            <tr>
                <td>{{ $approver->user->profile->first_name }} {{ $approver->user->profile->last_name }}</td>
                <td>{{ $approver->user->email }}</td>
                <td>{{ $approver->department->name }}</td>
                <td>
                    <span class="badge badge-{{ $approver->approver_for === 'leave' ? 'info' : 'warning' }}">
                        {{ ucfirst($approver->approver_for) }}
                    </span>
                </td>
                <td>{{ $approver->delegateApprover->name ?? '—' }}</td>
                <td>
                    <span class="badge badge-{{ $approver->active ? 'success' : 'secondary' }}">
                        {{ $approver->active ? 'Active' : 'Inactive' }}
                    </span>
                </td>
                <td>
                    <a href="{{ route('hr.approvers.edit', $approver) }}" class="btn btn-sm btn-primary">Edit</a>
                    <form action="{{ route('hr.approvers.destroy', $approver) }}" method="POST" style="display:inline;">
                        @csrf @method('DELETE')
                        <button class="btn btn-sm btn-danger" onclick="return confirm('Delete?')">Delete</button>
                    </form>
                </td>
            </tr>
        @endforeach
    </tbody>
</table>

Blade template: Create form

<form action="{{ route('hr.approvers.store') }}" method="POST">
    @csrf

    <div class="form-group">
        <label for="user_id">Approver *</label>
        <select name="user_id" id="user_id" class="form-control" required>
            <option value="">— Select Approver —</option>
            @foreach($users as $user)
                <option value="{{ $user->id }}" {{ old('user_id') == $user->id ? 'selected' : '' }}>
                    {{ $user->profile->first_name }} {{ $user->profile->last_name }} ({{ $user->email }})
                </option>
            @endforeach
        </select>
        @error('user_id') <span class="text-danger">{{ $message }}</span> @enderror
    </div>

    <div class="form-group">
        <label for="department_id">Department *</label>
        <select name="department_id" id="department_id" class="form-control" required>
            <option value="">— Select Department —</option>
            @foreach($departments as $dept)
                <option value="{{ $dept->id }}" {{ old('department_id') == $dept->id ? 'selected' : '' }}>
                    {{ $dept->name }}
                </option>
            @endforeach
        </select>
        @error('department_id') <span class="text-danger">{{ $message }}</span> @enderror
    </div>

    <div class="form-group">
        <label for="approver_for">Type *</label>
        <select name="approver_for" id="approver_for" class="form-control" required>
            <option value="">— Select Type —</option>
            <option value="leave" {{ old('approver_for') === 'leave' ? 'selected' : '' }}>Leave</option>
            <option value="overtime" {{ old('approver_for') === 'overtime' ? 'selected' : '' }}>Overtime</option>
        </select>
        @error('approver_for') <span class="text-danger">{{ $message }}</span> @enderror
    </div>

    <div class="form-group">
        <label for="delegate_approver_id">Delegate Approver (Optional)</label>
        <select name="delegate_approver_id" id="delegate_approver_id" class="form-control">
            <option value="">— None —</option>
            @foreach($users as $user)
                <option value="{{ $user->id }}" {{ old('delegate_approver_id') == $user->id ? 'selected' : '' }}>
                    {{ $user->profile->first_name }} {{ $user->profile->last_name }}
                </option>
            @endforeach
        </select>
        @error('delegate_approver_id') <span class="text-danger">{{ $message }}</span> @enderror
    </div>

    <div class="form-group">
        <label for="notes">Notes (Optional)</label>
        <textarea name="notes" id="notes" class="form-control" rows="3" maxlength="500">{{ old('notes') }}</textarea>
        @error('notes') <span class="text-danger">{{ $message }}</span> @enderror
    </div>

    <div class="form-group">
        <label>
            <input type="checkbox" name="active" value="1" {{ old('active', true) ? 'checked' : '' }}>
            Active
        </label>
    </div>

    <button type="submit" class="btn btn-primary">Create Approver</button>
    <a href="{{ route('hr.approvers.index') }}" class="btn btn-secondary">Cancel</a>
</form>

Document version: 1.0
Maintainers: HR / Operations / Engineering
Last updated: December 10, 2025