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.
If you want to see the full system in action, check out the live demo of VMMS: https://vmms-app-production.up.railway.app/login
Happy to answer any questions in the comments!
Top comments (0)