# Implementation Guide

This guide walks through practical usage of `crumbls/timeline` from installation through building a live event calendar feed.

---

## Installation

```bash
composer require crumbls/timeline
```

Publish and run the migrations:

```bash
php artisan vendor:publish --tag=timeline-migrations
php artisan migrate
```

Optionally publish the config:

```bash
php artisan vendor:publish --tag=timeline-config
```

---

## Configuration

`config/timeline.php`:

```php
return [
    'table_prefix'                 => 'timeline_',
    'occurrence_generation_months' => 12,
    'default_timezone'             => 'UTC',
];
```

`table_prefix` controls every table name in the package. Change it before running migrations if your database has naming conventions.

`occurrence_generation_months` controls how far ahead the generator pre-computes occurrences when a Rule is saved.

---

## Core Concepts

| Concept | What it is |
|---|---|
| `Event` | The conceptual thing — "Laravel Meetup", "Board Meeting" |
| `Rule` | When it happens — an RRULE string or a single date |
| `Occurrence` | A concrete scheduled instance — "Meetup on June 5th" |
| `OccurrenceException` | A one-off change to a single Occurrence |
| `Location` | A reusable venue attached to Occurrences |

**Query Occurrences, not Events.** Events are containers. Everything a user sees comes from the `Occurrence` model.

---

## Creating Events

### One-time event

```php
use Crumbls\Timeline\Models\Event;
use Crumbls\Timeline\Models\Rule;
use Crumbls\Timeline\Enums\EventStatus;

$event = Event::create([
    'name'        => 'Product Launch',
    'description' => 'Q3 product announcement',
    'timezone'    => 'America/Denver',
    'status'      => EventStatus::Published,
]);

Rule::create([
    'event_id'  => $event->id,
    'starts_at' => '2025-09-15 14:00:00',
    'ends_at'   => '2025-09-15 16:00:00',
    // No rrule — treated as a single occurrence
]);
```

Saving the Rule automatically dispatches `GenerateOccurrencesJob`, which creates the single `Occurrence` record.

---

### Weekly recurring event

```php
$event = Event::create([
    'name'     => 'Laravel Meetup',
    'timezone' => 'America/Denver',
    'status'   => EventStatus::Published,
]);

Rule::create([
    'event_id'  => $event->id,
    'starts_at' => '2025-06-03 18:00:00',  // First Tuesday
    'ends_at'   => '2025-06-03 20:00:00',  // 2-hour duration
    'rrule'     => 'FREQ=WEEKLY;BYDAY=TU',
]);
```

---

### Event with multiple rules

An Event can have any number of Rules. This is the correct way to model "first and third Thursday of the month":

```php
$event = Event::create([
    'name'   => 'Board Meeting',
    'status' => EventStatus::Published,
]);

// First Thursday
Rule::create([
    'event_id'  => $event->id,
    'starts_at' => '2025-06-05 09:00:00',
    'ends_at'   => '2025-06-05 10:00:00',
    'rrule'     => 'FREQ=MONTHLY;BYDAY=1TH',
]);

// Third Thursday
Rule::create([
    'event_id'  => $event->id,
    'starts_at' => '2025-06-19 09:00:00',
    'ends_at'   => '2025-06-19 10:00:00',
    'rrule'     => 'FREQ=MONTHLY;BYDAY=3TH',
]);
```

---

### Rule with an end date

Use `until_at` to stop generating occurrences after a specific date. The RRULE `UNTIL` clause is also respected, but `until_at` lets you control it at the model level without modifying the RRULE string.

```php
Rule::create([
    'event_id'  => $event->id,
    'starts_at' => '2025-06-03 18:00:00',
    'rrule'     => 'FREQ=WEEKLY;BYDAY=TU',
    'until_at'  => '2025-12-31 23:59:59',
]);
```

---

## Locations

```php
use Crumbls\Timeline\Models\Location;
use Crumbls\Timeline\Models\Occurrence;

$venue = Location::create([
    'name'       => 'Denver Tech Center',
    'address_1'  => '4700 S. Syracuse St.',
    'city'       => 'Denver',
    'state'      => 'CO',
    'postal_code' => '80237',
    'country'    => 'US',
    'timezone'   => 'America/Denver',
    'latitude'   => 39.6475,
    'longitude'  => -104.8997,
]);

// Attach a location when the Occurrence is generated, or update it after:
Occurrence::where('event_id', $event->id)
    ->scheduled()
    ->update(['location_id' => $venue->id]);
```

To attach a location to all future occurrences of a specific event from the Rule level, set `location_id` on each Occurrence after generation — or override it per-occurrence using an `OccurrenceException`.

---

## Querying Occurrences

All queries go through the `Occurrence` model.

### Upcoming occurrences

```php
use Crumbls\Timeline\Models\Occurrence;

$occurrences = Occurrence::upcoming()
    ->with(['event', 'location'])
    ->orderBy('starts_at')
    ->get();
```

### Today

```php
$today = Occurrence::today()
    ->scheduled()
    ->with('event')
    ->get();
```

### Between two dates

```php
use Carbon\Carbon;

$occurrences = Occurrence::between(
    Carbon::parse('2025-06-01'),
    Carbon::parse('2025-06-30')
)->scheduled()->orderBy('starts_at')->get();
```

### For a specific event

```php
$occurrences = Occurrence::forEvent($event->id)
    ->upcoming()
    ->get();

// Or via the relationship:
$occurrences = $event->occurrences()
    ->upcoming()
    ->get();
```

### At a specific location

```php
$occurrences = Occurrence::atLocation($venue->id)
    ->between(Carbon::now(), Carbon::now()->addMonth())
    ->get();
```

### Chaining scopes

Scopes compose freely:

```php
$occurrences = Occurrence::scheduled()
    ->upcoming()
    ->forEvent($event->id)
    ->with(['event', 'location', 'exceptions'])
    ->orderBy('starts_at')
    ->paginate(20);
```

---

## Building a Calendar Feed

A calendar feed is a date-ranged list of occurrences with their event and location data. The pattern is always the same: query by range, eager-load relationships, transform.

### Basic controller

```php
use Crumbls\Timeline\Models\Occurrence;
use Carbon\Carbon;
use Illuminate\Http\Request;

class CalendarFeedController extends Controller
{
    public function __invoke(Request $request)
    {
        $start = Carbon::parse($request->get('start', Carbon::now()->startOfMonth()));
        $end   = Carbon::parse($request->get('end', Carbon::now()->endOfMonth()));

        $occurrences = Occurrence::between($start, $end)
            ->scheduled()
            ->with(['event', 'location', 'exceptions'])
            ->orderBy('starts_at')
            ->get();

        return response()->json(
            $occurrences->map(fn ($o) => $this->transform($o))
        );
    }

    private function transform(Occurrence $occurrence): array
    {
        return [
            'id'         => $occurrence->uuid,
            'title'      => $occurrence->event->name,
            'start'      => $occurrence->starts_at->toIso8601String(),
            'end'        => $occurrence->ends_at?->toIso8601String(),
            'status'     => $occurrence->status->value,
            'location'   => $occurrence->location ? [
                'name'    => $occurrence->location->name,
                'address' => $occurrence->location->address_1,
                'city'    => $occurrence->location->city,
                'lat'     => $occurrence->location->latitude,
                'lng'     => $occurrence->location->longitude,
            ] : null,
        ];
    }
}
```

### Route

```php
Route::get('/calendar/feed', CalendarFeedController::class)->name('calendar.feed');
```

### Fetching

```
GET /calendar/feed?start=2025-06-01&end=2025-06-30
```

---

## Handling Exceptions (One-off Changes)

Use `OccurrenceException` to modify a single occurrence without touching the Rule.

### Cancel one occurrence

```php
use Crumbls\Timeline\Models\OccurrenceException;
use Crumbls\Timeline\Enums\ExceptionAction;

$occurrence = Occurrence::find($id);

OccurrenceException::create([
    'occurrence_id' => $occurrence->id,
    'action'        => ExceptionAction::Cancel,
]);

$occurrence->update(['status' => \Crumbls\Timeline\Enums\OccurrenceStatus::Cancelled]);
```

### Reschedule one occurrence

```php
OccurrenceException::create([
    'occurrence_id' => $occurrence->id,
    'action'        => ExceptionAction::Reschedule,
    'starts_at'     => '2025-06-12 19:00:00',
    'ends_at'       => '2025-06-12 21:00:00',
]);

$occurrence->update([
    'starts_at' => '2025-06-12 19:00:00',
    'ends_at'   => '2025-06-12 21:00:00',
]);
```

### Change location for one occurrence

```php
OccurrenceException::create([
    'occurrence_id' => $occurrence->id,
    'action'        => ExceptionAction::Modify,
    'location_id'   => $newVenue->id,
    'payload'       => ['reason' => 'Venue change for this week only'],
]);

$occurrence->update(['location_id' => $newVenue->id]);
```

The Exception record serves as an audit trail. The Occurrence itself holds the current state.

---

## Regenerating Occurrences

When a Rule is created or updated, `GenerateOccurrencesJob` is dispatched automatically. You can also trigger generation manually:

```php
use Crumbls\Timeline\Services\OccurrenceGenerator;

$generator = app(OccurrenceGenerator::class);

// Generate from now to the configured horizon (default 12 months)
$generator->generate($rule);

// Generate a custom range
$generator->generateRange(
    $rule,
    Carbon::parse('2025-01-01'),
    Carbon::parse('2025-12-31')
);
```

The generator will:

1. Expand the RRULE into concrete dates within the window
2. Create new `Occurrence` records for dates not already present
3. Update `ends_at` on existing `Occurrence` records if the Rule changed
4. Delete future `Scheduled` occurrences that are no longer in the RRULE expansion
5. Leave `Cancelled` and `Completed` occurrences untouched

---

## Queue Setup

The package dispatches `GenerateOccurrencesJob` on the default queue. For production, ensure your queue worker is running:

```bash
php artisan queue:work
```

For scheduled maintenance (regenerating the rolling 12-month window), add this to your `routes/console.php` or scheduler:

```php
use Crumbls\Timeline\Models\Rule;
use Crumbls\Timeline\Jobs\GenerateOccurrencesJob;

Schedule::call(function () {
    Rule::where('is_active', true)->each(function (Rule $rule) {
        GenerateOccurrencesJob::dispatch($rule);
    });
})->monthly();
```

---

## Building RRULEs with RRuleBuilder

Rather than writing RRULE strings by hand, use the fluent `RRuleBuilder` to construct them:

```php
use Crumbls\Timeline\Support\RRuleBuilder;
```

### Common patterns

```php
// Every Tuesday
RRuleBuilder::make()->weekly()->onDays('TU')->toString();
// FREQ=WEEKLY;BYDAY=TU

// Every weekday
RRuleBuilder::make()->weekly()->onDays('MO', 'TU', 'WE', 'TH', 'FR')->toString();
// FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR

// Every other Monday
RRuleBuilder::make()->weekly()->every(2)->onDays('MO')->toString();
// FREQ=WEEKLY;INTERVAL=2;BYDAY=MO

// First Friday of the month
RRuleBuilder::make()->monthly()->onNthWeekday(1, 'FR')->toString();
// FREQ=MONTHLY;BYDAY=1FR

// Last Monday of the month
RRuleBuilder::make()->monthly()->onNthWeekday(-1, 'MO')->toString();
// FREQ=MONTHLY;BYDAY=-1MO

// 15th of every month
RRuleBuilder::make()->monthly()->onMonthDay(15)->toString();
// FREQ=MONTHLY;BYMONTHDAY=15

// Last day of every month
RRuleBuilder::make()->monthly()->onMonthDay(-1)->toString();
// FREQ=MONTHLY;BYMONTHDAY=-1

// Daily, 10 times then stop
RRuleBuilder::make()->daily()->count(10)->toString();
// FREQ=DAILY;COUNT=10

// Weekly until a specific date
RRuleBuilder::make()->weekly()->onDays('TU')->until('2025-12-31')->toString();
// FREQ=WEEKLY;BYDAY=TU;UNTIL=20251231T235959Z

// Every June 15th
RRuleBuilder::make()->yearly()->inMonth(6)->onMonthDay(15)->toString();
// FREQ=YEARLY;BYMONTHDAY=15;BYMONTH=6
```

### Using the result

Pass the output directly to a `Rule`:

```php
Rule::create([
    'event_id'  => $event->id,
    'starts_at' => '2025-06-03 18:00:00',
    'ends_at'   => '2025-06-03 20:00:00',
    'rrule'     => RRuleBuilder::make()->weekly()->onDays('TU')->toString(),
]);
```

The builder also casts to string automatically:

```php
'rrule' => (string) RRuleBuilder::make()->monthly()->onNthWeekday(1, 'FR'),
```

### Human-readable descriptions

```php
// From a builder instance
RRuleBuilder::make()->monthly()->onNthWeekday(1, 'FR')->toHuman();
// "monthly on the first Friday"

// From an existing RRULE string
RRuleBuilder::describe('FREQ=WEEKLY;BYDAY=MO,WE,FR');
// "weekly on Monday, Wednesday and Friday"
```

Use `toHuman()` / `describe()` to display schedules to users without exposing the raw RRULE syntax.

### Parsing existing RRULEs

If you have a stored RRULE string and want to modify it:

```php
$builder = RRuleBuilder::parse('FREQ=WEEKLY;BYDAY=TU');
$builder->count(10);

echo $builder->toString();
// FREQ=WEEKLY;BYDAY=TU;COUNT=10
```

The `RRULE:` prefix is stripped automatically if present.

---

## RRULE Reference

For cases where you write RRULE strings directly, store only the property value — omit the `RRULE:` prefix.

| Schedule | RRULE |
|---|---|
| Daily | `FREQ=DAILY` |
| Weekly on Tuesday | `FREQ=WEEKLY;BYDAY=TU` |
| Every weekday | `FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR` |
| First Friday of the month | `FREQ=MONTHLY;BYDAY=1FR` |
| Last day of the month | `FREQ=MONTHLY;BYMONTHDAY=-1` |
| Every other week on Monday | `FREQ=WEEKLY;INTERVAL=2;BYDAY=MO` |
| Annually | `FREQ=YEARLY` |
| 10 occurrences then stop | `FREQ=WEEKLY;COUNT=10` |

For one-time events, leave `rrule` null. The generator creates a single Occurrence at `starts_at`.

---

## Documentation

- [crumbls/timeline](https://documentation.crumbls.com/timeline/-/index.md)
