Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fleetbase/core-api",
"version": "1.6.38",
"version": "1.6.39",
"description": "Core Framework and Resources for Fleetbase API",
"keywords": [
"fleetbase",
Expand Down Expand Up @@ -57,7 +57,8 @@
"sqids/sqids": "^0.4.1",
"xantios/mimey": "^2.2.0",
"spatie/laravel-pdf": "^1.9",
"mossadal/math-parser": "^1.3"
"mossadal/math-parser": "^1.3",
"rlanvin/php-rrule": "^2.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.34.1",
Expand Down Expand Up @@ -107,4 +108,4 @@
"@test:unit"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('schedule_items', function (Blueprint $table) {
// Link back to the ScheduleTemplate that generated this item (nullable for standalone items)
$table->string('template_uuid', 191)->nullable()->after('schedule_uuid')->index()
->comment('The ScheduleTemplate that generated this item via RRULE expansion');

// Flags for recurrence management
$table->boolean('is_exception')->default(false)->after('status')->index()
->comment('True when this item has been manually edited and should not be overwritten by re-materialization');

$table->string('exception_for_date', 20)->nullable()->after('is_exception')
->comment('The original RRULE occurrence date (YYYY-MM-DD) this item is an exception for');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('schedule_items', function (Blueprint $table) {
$table->dropColumn(['template_uuid', 'is_exception', 'exception_for_date']);
});
}
};
77 changes: 77 additions & 0 deletions migrations/2025_11_14_000007_create_schedule_exceptions_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*
* Stores explicit deviations from a driver's (or any subject's) recurring schedule.
* This replaces the ambiguous schedule_availability table for time-off and absence tracking.
*
* Examples:
* - Approved annual leave: type=time_off, status=approved
* - Sick day: type=sick, status=approved
* - Shift swap (driver unavailable): type=swap, status=pending
* - Public holiday override: type=holiday, status=approved
*
* @return void
*/
public function up()
{
Schema::create('schedule_exceptions', function (Blueprint $table) {
$table->increments('id');
$table->string('_key')->nullable();
$table->string('uuid', 191)->nullable()->unique()->index();
$table->string('public_id', 191)->nullable()->unique()->index();
$table->string('company_uuid', 191)->nullable()->index();

// Polymorphic subject — the entity this exception applies to (e.g. Driver)
$table->string('subject_uuid', 191)->nullable()->index();
$table->string('subject_type')->nullable()->index();

// Optional link to the schedule this exception belongs to
$table->string('schedule_uuid', 191)->nullable()->index();

// The date range the exception covers
$table->timestamp('start_at')->nullable()->index();
$table->timestamp('end_at')->nullable()->index();

// Exception classification
$table->string('type', 50)->nullable()->index()
->comment('e.g., time_off, sick, holiday, swap, training');

// Workflow status
$table->string('status', 50)->default('pending')->index()
->comment('pending | approved | rejected | cancelled');

// Human-readable reason and optional notes
$table->string('reason')->nullable();
$table->text('notes')->nullable();

// Who approved/rejected the exception
$table->string('reviewed_by_uuid', 191)->nullable()->index();
$table->timestamp('reviewed_at')->nullable();

$table->json('meta')->nullable();
$table->softDeletes();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->index(['subject_uuid', 'subject_type', 'start_at', 'end_at'], 'schedule_exception_subject_range_idx');
$table->index(['company_uuid', 'status', 'start_at', 'end_at'], 'schedule_exception_company_status_idx');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedule_exceptions');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*
* Adds columns to the schedules table to support the rolling materialization engine.
*
* - last_materialized_at: timestamp of the last successful materialization run
* - materialization_horizon: the furthest date up to which items have been materialized
*
* @return void
*/
public function up()
{
Schema::table('schedules', function (Blueprint $table) {
$table->timestamp('last_materialized_at')->nullable()->after('status')
->comment('Timestamp of the last successful RRULE materialization run');
$table->date('materialization_horizon')->nullable()->after('last_materialized_at')
->comment('The furthest future date up to which ScheduleItems have been generated');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('schedules', function (Blueprint $table) {
$table->dropColumn(['last_materialized_at', 'materialization_horizon']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration {
/**
* Run the migrations.
*
* Adds two columns that were referenced in the ScheduleTemplate model
* but omitted from the original create migration:
*
* - schedule_uuid: links an applied template copy to its parent Schedule
* (NULL for library/reusable templates, set when applyToSchedule() is called)
* - color: hex colour string used by the frontend calendar to render shift blocks
*
* @return void
*/
public function up()
{
Schema::table('schedule_templates', function (Blueprint $table) {
// Add schedule_uuid after company_uuid to keep column order logical
$table->string('schedule_uuid', 191)
->nullable()
->after('company_uuid')
->index()
->comment('UUID of the Schedule this template is applied to; NULL for library templates');

// Add color after rrule
$table->string('color', 20)
->nullable()
->after('rrule')
->comment('Hex colour for calendar rendering, e.g. #6366f1');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('schedule_templates', function (Blueprint $table) {
$table->dropIndex(['schedule_uuid']);
$table->dropColumn(['schedule_uuid', 'color']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;

/**
* Add 'scheduled' to the schedule_items.status ENUM and update the default.
*
* The original ENUM was: ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show']
* We add 'scheduled' as the canonical status for future materialised shifts and
* change the column default from 'pending' to 'scheduled'.
*
* 'pending' is retained for backwards compatibility (e.g. manually-created items
* that have not yet been confirmed).
*/
return new class extends Migration
{
public function up(): void
{
DB::statement("
ALTER TABLE `schedule_items`
MODIFY COLUMN `status`
ENUM('pending','scheduled','confirmed','in_progress','completed','cancelled','no_show')
NOT NULL
DEFAULT 'scheduled'
");
}

public function down(): void
{
// Revert any 'scheduled' rows back to 'pending' before shrinking the ENUM
DB::statement("UPDATE `schedule_items` SET `status` = 'pending' WHERE `status` = 'scheduled'");

DB::statement("
ALTER TABLE `schedule_items`
MODIFY COLUMN `status`
ENUM('pending','confirmed','in_progress','completed','cancelled','no_show')
NOT NULL
DEFAULT 'pending'
");
}
};
35 changes: 35 additions & 0 deletions migrations/2026_04_06_000001_add_hos_limits_to_schedules_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

/**
* Add per-schedule Hours of Service (HOS) limits.
*
* These columns allow each schedule to override the global HOS defaults
* configured in the scheduling settings. When NULL, the global defaults
* (11h daily / 70h weekly) are used.
*/
return new class extends Migration {
public function up(): void
{
Schema::table('schedules', function (Blueprint $table) {
// Per-schedule HOS limits (NULL = use global settings default)
$table->unsignedTinyInteger('hos_daily_limit')->nullable()->after('timezone')
->comment('Max driving hours per day. NULL = use global default (11h).');
$table->unsignedTinyInteger('hos_weekly_limit')->nullable()->after('hos_daily_limit')
->comment('Max driving hours per rolling 7-day period. NULL = use global default (70h).');
// HOS data source — extensible for future integrations
$table->string('hos_source', 50)->default('schedule')->after('hos_weekly_limit')
->comment('Source used to calculate HOS hours: schedule | telematics | manual');
});
}

public function down(): void
{
Schema::table('schedules', function (Blueprint $table) {
$table->dropColumn(['hos_daily_limit', 'hos_weekly_limit', 'hos_source']);
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;

return new class extends Migration {
public function up(): void
{
Schema::table('schedule_items', function (Blueprint $table) {
$table->string('company_uuid', 191)->nullable()->index()->after('uuid');
});

// Backfill from the parent schedule
DB::statement("
UPDATE schedule_items si
JOIN schedules s ON s.uuid = si.schedule_uuid
SET si.company_uuid = s.company_uuid
WHERE si.company_uuid IS NULL
AND si.schedule_uuid IS NOT NULL
");
}

public function down(): void
{
Schema::table('schedule_items', function (Blueprint $table) {
$table->dropColumn('company_uuid');
});
}
};
Loading
Loading