hasyirin/laravel-kpi

This is my package laravel-kpi

Maintainers

Package info

github.com/hasyirin/laravel-kpi

pkg:composer/hasyirin/laravel-kpi

Fund package maintenance!

Hasyirin Fakhriy

Statistics

Installs: 144

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-04-18 20:30 UTC

This package is auto-updated.

Last update: 2026-04-20 20:05:30 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

Measure turnaround time against a working schedule, and track status transitions (movements) on any Eloquent model.

Given a start and end timestamp, laravel-kpi computes the effective working duration — skipping weekends, holidays, and any custom exclude dates — expressed as minutes, hours, and a period ratio against scheduled working minutes. It also ships a lightweight workflow layer: attach the InteractsWithMovement trait to a model and you get a chain of status transitions (pass, passIfNotCurrent), each stamped with the KPI duration it took.

Requirements

  • PHP 8.3+
  • Laravel 11, 12, or 13
  • ext-bcmath

Installation

composer require hasyirin/laravel-kpi

Publish and run the migrations:

php artisan vendor:publish --tag="laravel-kpi-migrations"
php artisan migrate

Publish the config file:

php artisan vendor:publish --tag="laravel-kpi-config"

Configuration

config/kpi.php:

use Hasyirin\KPI\Enums\Day;
use Hasyirin\KPI\Models\Holiday;
use Hasyirin\KPI\Models\Movement;

return [
    'formats' => [
        'datetime' => 'd/m/Y H:i A',
    ],

    'tables' => [
        'movements' => 'movements',
        'holidays'  => 'holidays',
    ],

    'models' => [
        'movement' => Movement::class,
        'holiday'  => Holiday::class,
    ],

    // Weekly work schedule — keyed by Day enum value (Sunday = 0 … Saturday = 6).
    // Days omitted are treated as non-working days.
    'schedule' => [
        Day::MONDAY->value    => ['8:00', '17:00'],
        Day::TUESDAY->value   => ['8:00', '17:00'],
        Day::WEDNESDAY->value => ['8:00', '17:00'],
        Day::THURSDAY->value  => ['8:00', '17:00'],
        Day::FRIDAY->value    => ['8:00', '15:30'],
    ],

    // Status values to exclude from KPI calculation, keyed by movable morph type.
    // e.g. 'App\Models\Task' => ['except' => ['on_hold']].
    'status' => [
        // 'App\Models\Task' => ['except' => ['on_hold']],
    ],
];

Calculating KPI

use Hasyirin\KPI\Facades\KPI;
use Illuminate\Support\Carbon;

$kpi = KPI::calculate(
    start: Carbon::parse('2025-01-01 08:00'),
    end:   Carbon::parse('2025-01-03 15:30'),
);

$kpi->minutes;  // 990.0          — effective working minutes in range
$kpi->hours;    // 16.5           — minutes / 60
$kpi->period;   // 2.0            — sum of (worked / scheduled) per day
$kpi->metadata; // KPIMetadata — counts of scheduled / unscheduled / excluded days

Excluding dates

Holidays in the holidays table within the range are always excluded. You can also pass ad-hoc dates:

$kpi = KPI::calculate(
    start: Carbon::parse('2025-01-01 08:00'),
    end:   Carbon::parse('2025-01-03 15:30'),
    excludeDates: [Carbon::parse('2025-01-02')],
);

Overriding the schedule per call

use Hasyirin\KPI\Data\WorkSchedule;
use Hasyirin\KPI\Enums\Day;

$kpi = KPI::calculate(
    start: Carbon::parse('2025-01-06 09:00'),
    end:   Carbon::parse('2025-01-06 15:00'),
    schedules: collect([
        Day::MONDAY->value => WorkSchedule::parse(['9:00', '15:00']),
    ]),
);

Holidays

Holiday is a regular Eloquent model with name and date fillables and a range() scope:

use Hasyirin\KPI\Models\Holiday;

Holiday::create(['name' => 'New Year', 'date' => '2025-01-01']);

Holiday::query()->range('2025-01-01', '2025-12-31')->get();

Tracking movements on a model

Implement HasMovement and apply the trait to any model you want to track:

use Hasyirin\KPI\Concerns\InteractsWithMovement;
use Hasyirin\KPI\Contracts\HasMovement;
use Illuminate\Database\Eloquent\Model;

class Task extends Model implements HasMovement
{
    use InteractsWithMovement;
}

pass()

Record a status transition. The previous open movement is auto-completed with received_at of the new one.

$task   = Task::create([...]);
$user   = auth()->user();
$system = $robot;

$task->pass(
    status:     'open',
    sender:     $system,          // who/what triggered the transition (optional)
    actor:     $user,             // who is now responsible (optional)
    receivedAt: now(),            // defaults to now()
    notes:      'Created via API',
    properties: ['source' => 'web'],
);

You can also pass a BackedEnum:

enum TaskStatus: string {
    case Open       = 'open';
    case InProgress = 'in_progress';
    case Closed     = 'closed';
}

$task->pass(TaskStatus::InProgress, actor: $user);

pass() runs inside a DB transaction — any failure rolls back the completion of the previous movement and the creation of the new one.

passIfNotCurrent()

Only creates a new movement if the current one doesn't already match the given status and actor. Returns false otherwise:

$movement = $task->passIfNotCurrent(TaskStatus::Open, actor: $user);
// Movement instance on change, false on no-op.

Reading movements

$task->movement;   // MorphOne — latest non-completed movement
$task->movements;  // MorphMany — full history

Computed attributes on Movement

Attribute Description
period Stored on save. Ratio of worked time to scheduled time on a completed movement.
hours Stored on save. Worked time in hours.
interval Accessor. hours * 3600 in seconds.
formatted_period Accessor. Falls back to an on-the-fly calculation for incomplete movements.
formatted_interval Accessor. Human-readable duration (e.g. 2 hours 15 minutes).
formatted_received_at Accessor. received_at formatted via config('kpi.formats.datetime').

Events

A Hasyirin\KPI\Events\Passed event fires after every successful pass():

use Hasyirin\KPI\Events\Passed;

Event::listen(function (Passed $event) {
    $event->current;   // Movement that was just created
    $event->previous;  // The movement it superseded, or null
});

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

Please review our security policy on how to report security vulnerabilities.

Credits

License

The MIT License (MIT). Please see License File for more information.