**Prepared by:** Oditha

**Date:** 23 January 2026

**Version:** 1.0

**Reference:** [FRD - HoReCa Field Operations & Workflow Management](https://www.notion.so/FRD-HoReCa-Field-Operations-Workflow-Management-2f7cd2773bc548c9a2595d856f15ac15?pvs=21)

---

## Overview

This document provides detailed technical specifications for implementing the **System Admin Portal** of the Lion HoReCa Excellence system using **Laravel**.

---

## Technology Stack

### Backend Framework

-   **Laravel:** 10.x
-   **PHP:** 8.2+
-   **Database:** MySQL 8.0+
-   **Cache:** Redis 7.0+
-   **Session:** Redis-backed sessions

### Frontend

-   **Templating:** Blade templates
-   **CSS Framework:** Tailwind CSS 3.x or Bootstrap 5.x
-   **JavaScript:** Alpine.js or Vue.js 3.x
-   **Charts:** Chart.js or ApexCharts
-   **Icons:** Heroicons or Font Awesome

### Additional Packages

-   **Authentication:** Laravel Breeze or Jetstream
-   **Authorization:** Spatie Laravel Permission
-   **File Storage:** Laravel Filesystem (S3 driver)
-   **PDF Generation:** Barryvdh DomPDF
-   **CSV Export:** Maatwebsite Excel
-   **Forms:** Livewire (optional)

---

## Project Structure

### Directory Organization

```
app/
├── Http/
│   ├── Controllers/
│   │   ├── Admin/
│   │   │   ├── DashboardController.php
│   │   │   ├── WorkflowController.php
│   │   │   ├── WorkflowTemplateController.php
│   │   │   ├── OutletController.php
│   │   │   ├── POSMController.php
│   │   │   ├── ServiceRequestController.php
│   │   │   ├── ReportController.php
│   │   │   └── UserController.php
│   │   └── API/
│   │       └── Mobile/
│   ├── Middleware/
│   │   ├── RoleMiddleware.php
│   │   └── HierarchyMiddleware.php
│   └── Requests/
│       ├── WorkflowRequest.php
│       ├── OutletRequest.php
│       └── ...
├── Models/
│   ├── User.php
│   ├── Workflow.php
│   ├── WorkflowStep.php
│   ├── Outlet.php
│   ├── ServiceRequest.php
│   └── ...
├── Services/
│   ├── WorkflowService.php
│   ├── OutletService.php
│   ├── POSMAuditService.php
│   ├── ServiceRequestService.php
│   └── HierarchyService.php
├── Repositories/
│   ├── WorkflowRepository.php
│   └── ...
├── Jobs/
│   ├── GeneratePOSMAudits.php
│   ├── FlagOverdueAudits.php
│   └── CheckSLABreaches.php
└── Console/
    └── Commands/
        ├── GenerateAuditsCommand.php
        └── FlagOverdueAuditsCommand.php

resources/
├── views/
│   ├── layouts/
│   │   ├── app.blade.php
│   │   └── navigation.blade.php
│   ├── admin/
│   │   ├── dashboard/
│   │   │   └── index.blade.php
│   │   ├── workflows/
│   │   │   ├── index.blade.php
│   │   │   ├── create.blade.php
│   │   │   ├── edit.blade.php
│   │   │   └── show.blade.php
│   │   ├── outlets/
│   │   ├── posm/
│   │   ├── service-requests/
│   │   └── reports/
│   └── components/
│       ├── workflow-step-card.blade.php
│       └── ...
└── js/
    ├── app.js
    ├── chart-config.js
    └── ...

routes/
├── web.php
└── api.php
```

---

## Module Implementation

## Module 1: Authentication & Authorization

### Authentication Setup

**Use Laravel Breeze for basic authentication scaffold**

**Install:**

```bash
composer require laravel/breeze --dev
php artisan breeze:install
```

**Customize:**

-   Add role-based redirection after login
-   Add hierarchy validation
-   Implement remember me functionality

### Authorization with Spatie Permission

**Install:**

```bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
```

**Define Roles:**

-   admin
-   regional_manager
-   sm (Sales Manager)
-   tm (Territory Manager)
-   lsr (Local Sales Representative)

**Define Permissions:**

-   workflows.view
-   workflows.create
-   workflows.edit
-   workflows.delete
-   outlets.view
-   outlets.create
-   outlets.edit
-   reports.view
-   reports.export
-   users.manage

**Middleware:**

```php
// Apply in routes
Route::middleware(['role:admin'])->group(function () {
    Route::resource('workflows', WorkflowController::class);
});

Route::middleware(['permission:reports.view'])->group(function () {
    Route::get('reports/visit-coverage', [ReportController::class, 'visitCoverage']);
});
```

---

## Module 2: Dashboard

### DashboardController

**Route:**

```php
Route::get('/admin/dashboard', [DashboardController::class, 'index'])->name('admin.dashboard');
```

**Controller Logic:**

```php
public function index(Request $request)
{
    $dateRange = $request->input('date_range', 'today');

    // Apply date filter
    $dates = $this->getDateRange($dateRange);

    // Apply hierarchy filter
    $userIds = HierarchyService::getUserHierarchy(auth()->user());

    // Fetch KPIs
    $kpis = [
        'outlets_visited_today' => $this->getOutletsVisitedToday($userIds),
        'workflows_completed_today' => $this->getWorkflowsCompletedToday($userIds),
        'pending_service_requests' => $this->getPendingSRs($userIds),
        'overdue_audits' => $this->getOverdueAudits($userIds),
    ];

    // Fetch chart data
    $charts = [
        'visit_coverage' => $this->getVisitCoverageData($dates, $userIds),
        'sr_status' => $this->getSRStatusData($dates, $userIds),
        'workflow_trend' => $this->getWorkflowTrendData($dates, $userIds),
        'brand_availability' => $this->getBrandAvailabilityData($dates, $userIds),
    ];

    // Fetch red flags
    $redFlags = $this->getRedFlags($userIds);

    return view('admin.dashboard.index', compact('kpis', 'charts', 'redFlags', 'dateRange'));
}
```

**Caching Strategy:**

```php
protected function getOutletsVisitedToday($userIds)
{
    $cacheKey = 'dashboard.outlets_visited.' . md5(json_encode($userIds));

    return Cache::remember($cacheKey, 300, function () use ($userIds) {
        return OutletVisit::whereIn('user_id', $userIds)
            ->whereDate('check_in_time', today())
            ->distinct('outlet_id')
            ->count('outlet_id');
    });
}
```

**View (Blade):**

```
@extends('layouts.app')

@section('content')
<div class="container-fluid">
    <!-- Date Range Selector -->
    <div class="row mb-4">
        <div class="col-12">
            <select name="date_range" id="dateRangeSelect" class="form-select">
                <option value="today"  $dateRange == 'today' ? 'selected' : '' >Today</option>
                <option value="last_7_days"  $dateRange == 'last_7_days' ? 'selected' : '' >Last 7 Days</option>
                <option value="last_30_days"  $dateRange == 'last_30_days' ? 'selected' : '' >Last 30 Days</option>
                <option value="custom">Custom</option>
            </select>
        </div>
    </div>

    <!-- KPI Cards -->
    <div class="row mb-4">
        <div class="col-md-3">
            <x-kpi-card
                title="Outlets Visited Today"
                :value="$kpis['outlets_visited_today']"
                icon="building-storefront"
                color="blue"
            />
        </div>
        <!-- Repeat for other KPIs -->
    </div>

    <!-- Charts -->
    <div class="row mb-4">
        <div class="col-md-8">
            <x-chart-card title="Visit Coverage">
                <canvas id="visitCoverageChart"></canvas>
            </x-chart-card>
        </div>
        <div class="col-md-4">
            <x-chart-card title="Service Request Status">
                <canvas id="srStatusChart"></canvas>
            </x-chart-card>
        </div>
    </div>

    <!-- Red Flags -->
    <div class="row">
        <div class="col-12">
            <x-red-flags-card :flags="$redFlags" />
        </div>
    </div>
</div>

@push('scripts')
<script>
    // Initialize charts
    const visitCoverageData = @json($charts['visit_coverage']);
    initVisitCoverageChart(visitCoverageData);
</script>
@endpush
@endsection
```

---

## Module 3: Workflow Management

### WorkflowController

**Routes:**

```php
Route::resource('workflows', WorkflowController::class);
Route::post('workflows/{workflow}/duplicate', [WorkflowController::class, 'duplicate']);
Route::patch('workflows/{workflow}/toggle-status', [WorkflowController::class, 'toggleStatus']);
```

**Index Method:**

```php
public function index(Request $request)
{
    $query = Workflow::with('steps');

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

    if ($request->has('status')) {
        $query->where('status', $request->status);
    }

    if ($request->has('search')) {
        $query->where('name', 'LIKE', '%' . $request->search . '%');
    }

    $workflows = $query->paginate(25);

    return view('admin.workflows.index', compact('workflows'));
}
```

**Store Method:**

```php
public function store(WorkflowRequest $request)
{
    DB::beginTransaction();

    try {
        $workflow = Workflow::create($request->only(['name', 'description', 'category', 'icon', 'color', 'status']));

        // Create steps if provided
        if ($request->has('steps')) {
            foreach ($request->steps as $stepData) {
                $step = $workflow->steps()->create($stepData);

                // Create step options
                if (isset($stepData['options'])) {
                    $step->options()->createMany($stepData['options']);
                }

                // Create step conditions
                if (isset($stepData['conditions'])) {
                    $step->conditions()->createMany($stepData['conditions']);
                }
            }
        }

        DB::commit();

        return redirect()->route('workflows.index')
            ->with('success', 'Workflow created successfully');

    } catch (\Exception $e) {
        DB::rollback();

        return back()->withInput()
            ->with('error', 'Failed to create workflow: ' . $e->getMessage());
    }
}
```

**View - Index:**

```
@extends('layouts.app')

@section('content')
<div class="container-fluid">
    <div class="row mb-4">
        <div class="col">
            <h1>Workflows</h1>
        </div>
        <div class="col-auto">
            <a href=" route('workflows.create') " class="btn btn-primary">
                <i class="fas fa-plus"></i> Create Workflow
            </a>
        </div>
    </div>

    <!-- Filters -->
    <div class="row mb-4">
        <div class="col">
            <form method="GET" action=" route('workflows.index') ">
                <div class="row g-3">
                    <div class="col-md-4">
                        <input type="text" name="search" class="form-control" placeholder="Search..." value=" request('search') ">
                    </div>
                    <div class="col-md-3">
                        <select name="category" class="form-select">
                            <option value="">All Categories</option>
                            <option value="impulse">Impulse</option>
                            <option value="cooler">Cooler</option>
                            <!-- ... -->
                        </select>
                    </div>
                    <div class="col-md-3">
                        <select name="status" class="form-select">
                            <option value="">All Statuses</option>
                            <option value="active">Active</option>
                            <option value="inactive">Inactive</option>
                        </select>
                    </div>
                    <div class="col-md-2">
                        <button type="submit" class="btn btn-secondary w-100">Filter</button>
                    </div>
                </div>
            </form>
        </div>
    </div>

    <!-- Table -->
    <div class="row">
        <div class="col">
            <div class="card">
                <div class="table-responsive">
                    <table class="table table-hover">
                        <thead>
                            <tr>
                                <th>Icon</th>
                                <th>Name</th>
                                <th>Category</th>
                                <th>Steps</th>
                                <th>Status</th>
                                <th>Used In Templates</th>
                                <th>Actions</th>
                            </tr>
                        </thead>
                        <tbody>
                            @forelse($workflows as $workflow)
                            <tr>
                                <td><span style="font-size: 1.5rem;"> $workflow->icon </span></td>
                                <td><strong> $workflow->name </strong></td>
                                <td><span class="badge bg- $workflow->category_color "> $workflow->category </span></td>
                                <td> $workflow->steps_count  steps</td>
                                <td>
                                    <x-status-toggle :model="$workflow" />
                                </td>
                                <td> $workflow->templates_count </td>
                                <td>
                                    <div class="btn-group">
                                        <a href=" route('workflows.edit', $workflow) " class="btn btn-sm btn-outline-primary">Edit</a>
                                        <button class="btn btn-sm btn-outline-secondary" onclick="duplicateWorkflow( $workflow->id )">Duplicate</button>
                                        @if($workflow->templates_count == 0)
                                        <form action=" route('workflows.destroy', $workflow) " method="POST" class="d-inline">
                                            @csrf
                                            @method('DELETE')
                                            <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Are you sure?')">Delete</button>
                                        </form>
                                        @endif
                                    </div>
                                </td>
                            </tr>
                            @empty
                            <tr>
                                <td colspan="7" class="text-center py-4">
                                    <p class="text-muted">No workflows found</p>
                                </td>
                            </tr>
                            @endforelse
                        </tbody>
                    </table>
                </div>
                <div class="card-footer">
                     $workflows->links()
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
```

---

## Module 4: Outlet Management

### OutletController

**Key Features:**

-   CSV import/export
-   Bulk operations
-   Map view
-   Hierarchy-based filtering

**Import CSV Method:**

```php
public function import(Request $request)
{
    $request->validate([
        'file' => 'required|mimes:csv,txt|max:10240'
    ]);

    $file = $request->file('file');
    $path = $file->storeTemp();

    // Dispatch job for async processing
    ImportOutletsJob::dispatch($path, auth()->id());

    return back()->with('success', 'Import started. You will be notified when complete.');
}
```

**Bulk Assignment Method:**

```php
public function bulkAssignTemplate(Request $request)
{
    $request->validate([
        'outlet_ids' => 'required|array',
        'template_id' => 'required|exists:workflow_group_templates,id'
    ]);

    $outlets = Outlet::whereIn('id', $request->outlet_ids)->get();

    foreach ($outlets as $outlet) {
        $outlet->update([
            'workflow_group_template_id' => $request->template_id
        ]);

        // Clear existing customizations
        $outlet->workflowCustomizations()->delete();
    }

    return back()->with('success', count($outlets) . ' outlets updated');
}
```

---

## Module 5: POSM Module

### POSMAuditController

**Automated Generation (Console Command):**

```php
namespace App\Console\Commands;

use App\Jobs\GeneratePOSMAudits;
use Illuminate\Console\Command;

class GenerateAuditsCommand extends Command
{
    protected $signature = 'posm:generate-audits';
    protected $description = 'Generate POSM audit tasks for outlets';

    public function handle()
    {
        $this->info('Generating POSM audits...');

        GeneratePOSMAudits::dispatch();

        $this->info('Audit generation job dispatched');
    }
}
```

**Job Implementation:**

```php
namespace App\Jobs;

use App\Models\Outlet;
use App\Models\POSMAudit;
use Carbon\Carbon;

class GeneratePOSMAudits implements ShouldQueue
{
    public function handle()
    {
        // Get outlets with completed audits 20 days ago
        $outlets = Outlet::whereHas('audits', function ($query) {
            $query->where('status', 'completed')
                  ->whereDate('completed_date', Carbon::now()->subDays(20));
        })->with('assignedTM')->get();

        foreach ($outlets as $outlet) {
            POSMAudit::create([
                'outlet_id' => $outlet->id,
                'assigned_tm_id' => $outlet->assigned_tm_id,
                'scheduled_date' => Carbon::now(),
                'due_date' => Carbon::now()->addDays(5),
                'status' => 'scheduled'
            ]);

            // Send notification to TM
            $outlet->assignedTM->notify(new AuditAssignedNotification($audit));
        }
    }
}
```

**Schedule in Kernel:**

```php
protected function schedule(Schedule $schedule)
{
    // Run at 2:00 AM Asia/Colombo time
    $schedule->command('posm:generate-audits')
             ->dailyAt('02:00')
             ->timezone('Asia/Colombo');

    // Flag overdue audits at 3:00 AM
    $schedule->command('posm:flag-overdue')
             ->dailyAt('03:00')
             ->timezone('Asia/Colombo');
}
```

---

## Module 6: Service Request Management

### ServiceRequestController

**Key Features:**

-   Auto-assignment based on hierarchy
-   SLA tracking
-   Activity log
-   Status transitions with validation

**Status Update Method:**

```php
public function updateStatus(Request $request, ServiceRequest $serviceRequest)
{
    $request->validate([
        'status' => 'required|in:open,in_progress,resolved,closed,cancelled',
        'comment' => 'nullable|string|max:1000'
    ]);

    // Check permission
    if (!$this->canUpdateStatus(auth()->user(), $serviceRequest, $request->status)) {
        abort(403, 'You do not have permission to update this status');
    }

    // Validate status transition
    if (!$this->isValidTransition($serviceRequest->status, $request->status)) {
        return back()->with('error', 'Invalid status transition');
    }

    $oldStatus = $serviceRequest->status;
    $serviceRequest->update(['status' => $request->status]);

    // Log activity
    $serviceRequest->activities()->create([
        'user_id' => auth()->id(),
        'action_type' => 'status_change',
        'old_value' => $oldStatus,
        'new_value' => $request->status,
        'comment' => $request->comment
    ]);

    // Send notifications
    $this->sendStatusChangeNotifications($serviceRequest, $oldStatus);

    return back()->with('success', 'Status updated successfully');
}
```

---

## Module 7: Reports

### ReportController

**Visit Coverage Report:**

```php
public function visitCoverage(Request $request)
{
    $dateRange = $request->input('date_range', 'last_30_days');
    $userId = $request->input('user_id');

    // Build query
    $query = OutletVisit::query();

    // Apply date filter
    $dates = $this->getDateRange($dateRange);
    $query->whereBetween('check_in_time', [$dates['start'], $dates['end']]);

    // Apply hierarchy filter
    if ($userId) {
        $query->where('user_id', $userId);
    } else {
        $userIds = HierarchyService::getUserHierarchy(auth()->user());
        $query->whereIn('user_id', $userIds);
    }

    // Get data
    $visits = $query->with('outlet', 'user')
                    ->orderBy('check_in_time', 'desc')
                    ->paginate(50);

    // Calculate metrics
    $metrics = $this->calculateVisitCoverageMetrics($visits);

    return view('admin.reports.visit-coverage', compact('visits', 'metrics', 'dateRange'));
}

public function exportVisitCoverage(Request $request)
{
    return Excel::download(
        new VisitCoverageExport($request->all()),
        'visit-coverage-' . date('Y-m-d') . '.xlsx'
    );
}
```

---

## Utility Services

### HierarchyService

**Get User Hierarchy:**

```php
namespace App\Services;

class HierarchyService
{
    public static function getUserHierarchy(User $user): array
    {
        $userIds = [$user->id];

        switch ($user->role) {
            case 'admin':
            case 'regional_manager':
                // Get all users
                $userIds = User::pluck('id')->toArray();
                break;

            case 'sm':
                // Get all TMs and LSRs under this SM
                $tms = User::where('reports_to_id', $user->id)->pluck('id');
                $lsrs = User::whereIn('reports_to_id', $tms)->pluck('id');
                $userIds = array_merge($userIds, $tms->toArray(), $lsrs->toArray());
                break;

            case 'tm':
                // Get all LSRs under this TM
                $lsrs = User::where('reports_to_id', $user->id)->pluck('id');
                $userIds = array_merge($userIds, $lsrs->toArray());
                break;

            case 'lsr':
                // Only own data
                break;
        }

        return $userIds;
    }
}
```

---

## Testing Considerations

### Unit Tests

-   Test business logic in Services
-   Test model relationships
-   Test calculations (SLA, variance, etc.)

### Feature Tests

-   Test controller actions
-   Test form submissions
-   Test validation rules
-   Test authorization

### Browser Tests (Dusk)

-   Test complete workflows
-   Test UI interactions
-   Test JavaScript functionality

---

**Document Status:** Final

**Next Steps:** Share with backend development team for Laravel implementation
