1. Go to this page and download the library: Download hasyirin/laravel-kpi library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
hasyirin / laravel-kpi example snippets
use Hasyirin\KPI\Enums\Day;
use Hasyirin\KPI\Models\Holiday;
use Hasyirin\KPI\Models\Movement;
use Hasyirin\KPI\Models\RecurringHoliday;
return [
'formats' => [
'datetime' => 'd/m/Y H:i A',
],
'tables' => [
'movements' => 'movements',
'holidays' => 'holidays',
'recurring_holidays' => 'recurring_holidays',
],
'models' => [
'movement' => Movement::class,
'holiday' => Holiday::class,
'recurring_holiday' => RecurringHoliday::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'],
],
// Day-of-week values whose holidays substitute forward to the next working day
// when observes_substitute = true on the row. Default empty = no substitution.
// Malaysian state examples:
// Sat-Sun states + post-2025 Johor → [Day::SUNDAY->value]
// Kelantan, Terengganu → [Day::SATURDAY->value]
// Kedah → [Day::FRIDAY->value]
'substitute' => [],
// 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']],
],
];
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
use Hasyirin\KPI\Concerns\InteractsWithMovement;
use Hasyirin\KPI\Contracts\HasMovement;
use Illuminate\Database\Eloquent\Model;
class Task extends Model implements HasMovement
{
use InteractsWithMovement;
}
$task = Task::create([...]);
$movement = $task->pass(
status: 'open',
sender: $system, // who triggered the transition (optional)
actor: $user, // who is now responsible (optional)
receivedAt: now(), // defaults to now()
notes: 'Created via API',
properties: ['source' => 'web'],
supersede: null, // see "supersede" below
expectsChildren: false, // see "expectsChildren" below
);
enum TaskStatus: string {
case Open = 'open';
case InProgress = 'in_progress';
case Closed = 'closed';
}
$task->pass(TaskStatus::InProgress, actor: $user);
// $root is a Movement returned by $task->pass(...)
$child = $root->pass('with_bob', actor: $bob);
// $child has parent_id = $root->id, inherits movable_id/movable_type from $root.
// $root is NOT auto-completed by this call.
$dave = $root->pass('with_dave', actor: $dave_user, supersede: false);
// $dave is a child of $root, sibling of $child.
// supersede: false keeps $child open alongside $dave.
$movement->complete(); // closed at now()
$movement->complete($at); // closed at a specific Carbon instance
$agency = $file->pass('at_agency', actor: $agency_user, expectsChildren: true);
// Later, even before $agency has any children:
$file->pass('review', actor: $reviewer);
// $agency stays open because expects_children = true.
$movement = $task->passIfNotCurrent(TaskStatus::Open, actor: $user);
// Movement instance on change, false on no-op.
$task->movement; // latest open movement of any depth (root or child), or null
$task->movements; // MorphMany — full history, all depths
$task->movements()->roots()->open()->get(); // open root tracks
$task->movements()->roots()->closed()->get(); // historical roots
$movement->parent; // BelongsTo Movement (null for roots)
$movement->children; // HasMany Movement
$movement->children()->open()->get();
$movement->children()->closed()->get();
$movement->previous; // BelongsTo Movement — same-level chain pointer
$movement->movable; // MorphTo — back to the resource
use Hasyirin\KPI\Events\Passed;
Event::listen(function (Passed $event) {
$event->current; // Movement that was just created
$event->previous; // Movement that was just CLOSED by this pass via supersede, or null
});
use Hasyirin\KPI\Events\Completed;
Event::listen(function (Completed $event) {
$event->movement; // the movement that just closed
});