Skip to main content
The StepProgressBuilder service builds ordered step progress (e.g. lead workflow, onboarding) for use in the UI. You define steps with completion callbacks; the builder evaluates them and returns a StepProgressData DTO suitable for components like status steps.

Overview

  • Chainable APIfor(), step(), withoutAutoComplete(), then build().
  • Auto-completion — By default, the rightmost completed step and all steps to its left are marked completed, so you don’t encode dependencies between steps.
  • Optional strict mode — Use withoutAutoComplete() so only steps whose callback returned true are marked completed.

Basic usage

use Inly\Core\Services\StepProgressBuilder;
use Inly\Core\Data\StepProgressData;

$progress = app(StepProgressBuilder::class)
    ->for($model)
    ->step(__('Assign'), fn ($m) => $m->assigned_at !== null)
    ->step(__('Contact'), fn ($m) => $m->status === 'contacted', __('Phone/Email'), $model->assigned_at?->copy()?->addHours(2))
    ->step(__('Close'), fn ($m) => $m->status === 'closed')
    ->build();

// $progress is StepProgressData with ->steps (array of StepProgressStepData)

Defining steps

step()
self
Adds a step. Parameters:
  • label — Main label for the step.
  • isCompletecallable(mixed): bool; receives the context passed to for().
  • sublabel — Optional (e.g. “Phone/Email”).
  • deadline — Optional DateTimeInterface or string (e.g. for countdowns).
  • flag — Optional string (e.g. for badges or status).
Each step gets an id (index), label, completed, flag, sublabel, and deadline in the built output.

Auto-completion

By default, the builder finds the rightmost step whose isComplete callback is true and marks that step and all steps to its left as completed. Steps after it stay incomplete. That way you only define “is this step done?” and the builder infers a linear progression. To turn this off and mark only steps whose callback returned true as completed (no filling of prior steps), call withoutAutoComplete() before build():
$progress = app(StepProgressBuilder::class)
    ->for($model)
    ->withoutAutoComplete()
    ->step(__('Step A'), fn ($m) => $m->has_a)
    ->step(__('Step B'), fn ($m) => $m->has_b)
    ->build();

Conditional steps

The builder uses Laravel’s Conditionable trait, so you can use when() (and related methods) in the chain:
return app(StepProgressBuilder::class)
    ->for($lead)
    ->step(__('Assign'), fn (Lead $l) => $l->assigned_at !== null)
    ->when($lead->snooze_at?->isFuture(), function (StepProgressBuilder $builder) use ($lead) {
        $builder->step(__('Snooze'), fn (Lead $l) => $l->snooze_at?->isFuture(), null, $lead->snooze_at);
    })
    ->step(__('Close'), fn (Lead $l) => $l->status === 'closed')
    ->build();

Output DTOs

  • StepProgressDatasteps: array of StepProgressStepData.
  • StepProgressStepDataid, label, completed, flag, sublabel, deadline.
Both are Spatie Laravel Data classes with #[TypeScript]; use composer generate-ts (or your app’s script) to generate frontend types.

Frontend component

Use the StatusSteps component from Core to render the progress. It expects an array of steps matching StepProgressStepData (or your app’s equivalent). It shows a horizontal row of dots with labels, connector lines, and optional deadline countdowns; incomplete steps with a past deadline get a warning/error state. Import and props:
import { StatusSteps } from '@/core/components/status-steps';
import { CoreCard } from '@/core/components/core-card';
import { StatusSteps, type StatusStepItem } from '@/core/components/status-steps';

interface Props {
  progress: Inly.Core.Data.StepProgressData;
}

export default function LeadShow({ progress }: Props) {
  return (
    <CoreCard title={__('Progress')} description={__('Progress for this lead')}>
      <StatusStep items={progress.steps} />
    </CoreCard>
  );
}