TL;DR: Step-by-step guide to building a multi-department approval workflow in Laravel with database-driven routing, Events, and role-based access. Code included.
Introduction
One of the most common problems I saw while building VMMS was this: documents get created, emailed to the next department, sit in someone's inbox for days, and when management asks "where's that voucher?" — nobody knows.
In this article I'll walk you through how I built a flexible multi-department approval workflow in Laravel that solves exactly this problem.
The Core Concept
Instead of hardcoding approval steps, I built a pipeline where each document has a current stage, and the next stage is determined by a department configuration stored in the database — not in the code.
This means you can add, remove, or reorder departments without touching a single line of PHP.
Database Structure
Here are the key tables:
// vouchers table
Schema::create('vouchers', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->foreignId('current_department_id')->constrained('departments');
$table->enum('status', ['ongoing', 'accomplished', 'rejected']);
$table->timestamps();
});
// departments table
Schema::create('departments', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('order'); // determines routing sequence
$table->timestamps();
});
Routing Logic
When a staff member approves a voucher, it automatically moves to the next department based on the order column:
public function approve(Voucher $voucher)
{
$currentOrder = $voucher->department->order;
$nextDepartment = Department::where('order', '>', $currentOrder)
->orderBy('order')
->first();
if ($nextDepartment) {
$voucher->update([
'current_department_id' => $nextDepartment->id,
'status' => 'ongoing'
]);
} else {
// No more departments — voucher is complete
$voucher->update(['status' => 'accomplished']);
}
// Fire event for email notification
event(new VoucherAdvanced($voucher));
}
Email Notifications
I used Laravel Events and Listeners to trigger email notifications every time a voucher moves to the next stage:
// VoucherAdvanced Event
class VoucherAdvanced
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Voucher $voucher) {}
}
// Listener
class SendVoucherNotification implements ShouldQueue
{
public function handle(VoucherAdvanced $event): void
{
$voucher = $event->voucher;
$staff = $voucher->department->staff;
foreach ($staff as $member) {
Mail::to($member->email)->send(new VoucherNotification($voucher));
}
}
}
Role-Based Access
Each department only sees vouchers assigned to them using Laravel Policies:
public function view(User $user, Voucher $voucher): bool
{
return $user->department_id === $voucher->current_department_id
|| $user->role === 'admin';
}
Real-Time Pipeline Tracker
To show users where a voucher is in real time, I built a simple status tracker on the Vue 3 frontend that reads the current department and renders it as a progress indicator — no websockets needed, just a clean Inertia.js page refresh on action.
What I Learned
Keep routing logic in the database, not the code — it makes the system far more flexible
Laravel Events are perfect for notifications — clean, queueable, and easy to extend
Policies over middleware for row-level access — much cleaner when you have complex role rules
Conclusion
This pattern works for any multi-step approval system — not just vouchers. You could use it for leave requests, purchase orders, or any document that needs to pass through multiple departments.
Want to see this in action?
I built VMMS — a complete Voucher Management & Monitoring System using everything covered in this article.
🔴 Live demo: https://vmms-app-production.up.railway.app/login
💰 Get the full source code: https://getvmms.gumroad.com/l/zeroqz
Happy to answer any questions in the comments! 🚀
Top comments (0)