How I Built a Multi-Department Workflow Routing System in Laravel with Inertia.js

Published: (May 3, 2026 at 09:54 AM EDT)
3 min read
Source: Dev.to

Source: Dev.to

The Problem I Was Trying to Solve

When I started building VMMS — a voucher management system — the biggest challenge was the workflow routing.
A voucher request doesn’t go to just one office; it passes through multiple departments. At any point a department can:

  • Complete its step and pass the request forward
  • Reject the entire request
  • Flag it for missing documents and pause processing

I needed a system that could handle all of that cleanly.

Configurable Processing Flow

Each voucher type has a configurable processing flow stored in the transaction_flows table:

Schema::create('transaction_flows', function (Blueprint $table) {
    $table->id();
    $table->foreignId('voucher_id')->constrained()->cascadeOnDelete();
    $table->string('department');
    $table->integer('order_number');
    $table->timestamps();
});

When a client submits a request, the system reads this table to determine the sequence of departments.

Audit Trails

Every time a department processes a request, an audit record is created in the audit_trails table:

Schema::create('audit_trails', function (Blueprint $table) {
    $table->id();
    $table->foreignId('transaction_id')->constrained()->cascadeOnDelete();
    $table->string('processing_offices');
    $table->timestamp('process_initiate')->nullable();
    $table->timestamp('process_accomplished')->nullable();
    $table->date('deadline')->nullable();
    $table->timestamps();
});
  • process_initiate is set when a department starts processing.
  • process_accomplished is set when they finish.

Determining the Current Step

The logic to find the active department (or the next one) looks like this:

$activeAudit = $auditTrails->first(
    fn($a) => $a->process_initiate && !$a->process_accomplished
);

if ($activeAudit) {
    return $activeAudit->processing_offices;
}

// If no active audit, find the next step
$doneOrders = $auditTrails
    ->filter(fn($a) => $a->process_accomplished)
    ->map(fn($a) => $flow->firstWhere('department', $a->processing_offices)?->order_number ?? 0);

$lastDone = $doneOrders->max() ?? 0;
$nextStep = $flow->firstWhere('order_number', $lastDone + 1);

return $nextStep?->department ?? 'Completed';

Edge Cases Handled

  • Skipped department – the code looks for the next order number.
  • Missing documents – the request is paused until the required documents are provided.
  • Final department completed – returns 'Completed'.

Vue 3 Stepper Component

On the front‑end I built a stepper component to visualise the progress:

const stepStatus = (req, step, stepIndex) => {
    const audits = req.audit_trails ?? [];
    const accomplishedCount = audits.filter(
        a => a.process_accomplished !== null
    ).length;

    if (stepIndex whereNull('resume_transaction')
    ->pluck('transaction_id')
    ->toArray();

When resume_transaction is filled, the workflow resumes automatically.

Key Takeaways

  1. Model the flow separately from the transaction – keeping transaction_flows distinct from voucher_transactions makes it easy to modify routing logic.
  2. Audit trails are essential – they provide a reliable source of truth for the current state and history.
  3. Plan for edge cases – skipped steps, document pauses, and final completions need explicit handling.

Demo & Availability

  • Live demo: 🔴
  • Purchase VMMS: Available on Gumroad –

Feel free to ask any questions in the comments!

0 views
Back to Blog

Related posts

Read more »

Integrate SB Admin 2 in Laravel

Laravel 11 Requirements bash php -v >= 8.2 composer -v node -v >= v14.16 npm -v Start Apache & MySQL on your web server. Install Laravel 11 bash composer creat...