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
127 changes: 127 additions & 0 deletions SCHEDULING_MODULE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Core Scheduling Module

This module provides a polymorphic, reusable scheduling system for the Fleetbase platform.

## Features

- **Polymorphic Architecture**: Schedule any entity type (drivers, vehicles, stores, warehouses, etc.)
- **Flexible Schedule Items**: Assign items to any assignee with any resource
- **Availability Management**: Track availability windows for any entity
- **Constraint System**: Pluggable constraint validation framework
- **Event-Driven**: Comprehensive event system for extensibility
- **Activity Logging**: All scheduling activities logged via Spatie Activity Log

## Database Tables

1. **schedules** - Master schedule records
2. **schedule_items** - Individual scheduled items/slots
3. **schedule_templates** - Reusable schedule patterns
4. **schedule_availability** - Availability tracking
5. **schedule_constraints** - Configurable scheduling rules

## Models

- `Schedule` - Main schedule model with polymorphic subject
- `ScheduleItem` - Schedule item with polymorphic assignee and resource
- `ScheduleTemplate` - Reusable template patterns
- `ScheduleAvailability` - Availability windows
- `ScheduleConstraint` - Constraint definitions

## Services

- `ScheduleService` - Core scheduling operations
- `AvailabilityService` - Availability management
- `ConstraintService` - Pluggable constraint validation

## Events

- `ScheduleCreated`
- `ScheduleUpdated`
- `ScheduleDeleted`
- `ScheduleItemCreated`
- `ScheduleItemUpdated`
- `ScheduleItemDeleted`
- `ScheduleItemAssigned`
- `ScheduleConstraintViolated`

## API Endpoints

All endpoints are available under `/int/v1/` prefix:

- `/schedules` - Schedule CRUD operations
- `/schedule-items` - Schedule item CRUD operations
- `/schedule-templates` - Template CRUD operations
- `/schedule-availability` - Availability CRUD operations
- `/schedule-constraints` - Constraint CRUD operations

## Extension Integration

Extensions can integrate with the scheduling module by:

1. **Registering Constraints**: Use `ConstraintService::register()` to add domain-specific constraints
2. **Listening to Events**: Subscribe to scheduling events to trigger extension-specific workflows
3. **Using the Meta Field**: Store extension-specific data in the `meta` JSON field

### Example: FleetOps HOS Constraint

```php
// In FleetOps ServiceProvider
public function boot()
{
$constraintService = app(\Fleetbase\Services\Scheduling\ConstraintService::class);
$constraintService->register('driver', \Fleetbase\FleetOps\Constraints\HOSConstraint::class);
}
```

## Usage Examples

### Creating a Schedule

```php
$schedule = Schedule::create([
'company_uuid' => $company->uuid,
'subject_type' => 'fleet',
'subject_uuid' => $fleet->uuid,
'name' => 'Weekly Driver Schedule',
'start_date' => '2025-11-15',
'end_date' => '2025-11-22',
'timezone' => 'America/New_York',
'status' => 'active',
]);
```

### Creating a Schedule Item

```php
$item = ScheduleItem::create([
'schedule_uuid' => $schedule->uuid,
'assignee_type' => 'driver',
'assignee_uuid' => $driver->uuid,
'resource_type' => 'vehicle',
'resource_uuid' => $vehicle->uuid,
'start_at' => '2025-11-15 08:00:00',
'end_at' => '2025-11-15 17:00:00',
'status' => 'confirmed',
]);
```

### Setting Availability

```php
$availability = ScheduleAvailability::create([
'subject_type' => 'driver',
'subject_uuid' => $driver->uuid,
'start_at' => '2025-11-20 00:00:00',
'end_at' => '2025-11-22 23:59:59',
'is_available' => false,
'reason' => 'vacation',
]);
```

## Future Enhancements

- Optimization algorithms for automatic schedule generation
- RRULE processing for recurring patterns
- Conflict detection and resolution
- Capacity planning and load balancing
- Multi-timezone support improvements
49 changes: 49 additions & 0 deletions migrations/2025_11_14_000001_create_schedules_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?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::create('schedules', function (Blueprint $table) {
$table->increments('id');
$table->string('_key')->nullable();
$table->string('uuid', 191)->nullable()->index();
$table->string('public_id', 191)->nullable()->unique()->index();
$table->string('company_uuid', 191)->nullable()->index();
$table->string('subject_uuid', 191)->nullable()->index();
$table->string('subject_type')->nullable()->index();
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->date('start_date')->nullable();
$table->date('end_date')->nullable();
$table->string('timezone', 50)->default('UTC');
$table->enum('status', ['draft', 'published', 'active', 'paused', 'archived'])->default('draft')->index();
$table->json('meta')->nullable();
$table->softDeletes();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->unique(['uuid']);
$table->index(['subject_uuid', 'subject_type', 'status']);
$table->index(['company_uuid', 'status', 'start_date', 'end_date']);
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedules');
}
};
53 changes: 53 additions & 0 deletions migrations/2025_11_14_000002_create_schedule_items_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?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::create('schedule_items', function (Blueprint $table) {
$table->increments('id');
$table->string('_key')->nullable();
$table->string('uuid', 191)->nullable()->index();
$table->string('public_id', 191)->nullable()->unique()->index();
$table->string('schedule_uuid', 191)->nullable()->index();
$table->string('assignee_uuid', 191)->nullable()->index();
$table->string('assignee_type')->nullable()->index();
$table->string('resource_uuid', 191)->nullable()->index();
$table->string('resource_type')->nullable()->index();
$table->timestamp('start_at')->nullable()->index();
$table->timestamp('end_at')->nullable()->index();
$table->integer('duration')->nullable()->comment('Duration in minutes');
$table->timestamp('break_start_at')->nullable();
$table->timestamp('break_end_at')->nullable();
$table->enum('status', ['pending', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show'])->default('pending')->index();
$table->json('meta')->nullable();
$table->softDeletes();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->unique(['uuid']);
$table->index(['schedule_uuid', 'start_at', 'end_at']);
$table->index(['assignee_uuid', 'assignee_type', 'status']);
$table->index(['resource_uuid', 'resource_type', 'start_at', 'end_at']);
$table->index(['status', 'start_at']);
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedule_items');
}
};
50 changes: 50 additions & 0 deletions migrations/2025_11_14_000003_create_schedule_templates_table.php
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.
*
* @return void
*/
public function up()
{
Schema::create('schedule_templates', function (Blueprint $table) {
$table->increments('id');
$table->string('_key')->nullable();
$table->string('uuid', 191)->nullable()->index();
$table->string('public_id', 191)->nullable()->unique()->index();
$table->string('company_uuid', 191)->nullable()->index();
$table->string('subject_uuid', 191)->nullable()->index();
$table->string('subject_type')->nullable()->index();
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->time('start_time')->nullable();
$table->time('end_time')->nullable();
$table->integer('duration')->nullable()->comment('Duration in minutes');
$table->integer('break_duration')->nullable()->comment('Break duration in minutes');
$table->text('rrule')->nullable()->comment('RFC 5545 recurrence rule');
$table->json('meta')->nullable();
$table->softDeletes();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->unique(['uuid']);
$table->index(['subject_uuid', 'subject_type']);
$table->index(['company_uuid']);
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedule_templates');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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::create('schedule_availability', function (Blueprint $table) {
$table->increments('id');
$table->string('_key')->nullable();
$table->string('uuid', 191)->nullable()->index();
$table->string('subject_uuid', 191)->nullable()->index();
$table->string('subject_type')->nullable()->index();
$table->timestamp('start_at')->nullable()->index();
$table->timestamp('end_at')->nullable()->index();
$table->boolean('is_available')->default(true)->index();
$table->tinyInteger('preference_level')->nullable()->comment('1-5 preference strength');
$table->text('rrule')->nullable()->comment('RFC 5545 recurrence rule');
$table->string('reason')->nullable();
$table->text('notes')->nullable();
$table->json('meta')->nullable();
$table->softDeletes();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->unique(['uuid']);
$table->index(['subject_uuid', 'subject_type', 'start_at', 'end_at', 'is_available'], 'schedule_availability_composite_idx');
$table->index(['subject_type', 'is_available', 'start_at', 'end_at']);
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedule_availability');
}
};
52 changes: 52 additions & 0 deletions migrations/2025_11_14_000005_create_schedule_constraints_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?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::create('schedule_constraints', function (Blueprint $table) {
$table->increments('id');
$table->string('_key')->nullable();
$table->string('uuid', 191)->nullable()->index();
$table->string('company_uuid', 191)->nullable()->index();
$table->string('subject_uuid', 191)->nullable()->index();
$table->string('subject_type')->nullable()->index();
$table->string('name')->nullable();
$table->text('description')->nullable();
$table->string('type', 50)->nullable()->index()->comment('e.g., hos, labor, business, capacity');
$table->string('category', 50)->nullable()->index()->comment('e.g., compliance, optimization');
$table->string('constraint_key', 100)->nullable()->index();
$table->text('constraint_value')->nullable();
$table->string('jurisdiction', 50)->nullable()->comment('e.g., US-Federal, US-CA, EU');
$table->integer('priority')->default(0)->comment('Higher = more important');
$table->boolean('is_active')->default(true)->index();
$table->json('meta')->nullable();
$table->softDeletes();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->unique(['uuid']);
$table->index(['type', 'category', 'is_active']);
$table->index(['company_uuid', 'is_active']);
$table->index(['subject_uuid', 'subject_type', 'is_active']);
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schedule_constraints');
}
};
Loading