diff --git a/resources/fieldsets/event.yaml b/resources/fieldsets/event.yaml index 594ccb4..4993cf2 100644 --- a/resources/fieldsets/event.yaml +++ b/resources/fieldsets/event.yaml @@ -11,7 +11,7 @@ fields: monthly: Monthly every: Every multi_day: Multi-Day - width: 33 + width: 50 display: Recurrence default: none - @@ -22,14 +22,7 @@ fields: default: UTC type: dictionary display: Timezone - - - handle: all_day - field: - type: toggle - width: 33 - display: 'All Day?' - unless: - recurrence: 'equals multi_day' + width: 50 - handle: specific_days field: @@ -75,6 +68,13 @@ fields: multi_day: 'equals true' recurrence: 'equals multi_day' format: Y-m-d + - + handle: end_date_spacer + field: + type: spacer + width: 33 + if: + recurrence: 'equals none' - handle: end_date field: @@ -91,11 +91,44 @@ fields: if: recurrence: 'contains_any daily, weekly, monthly, every' format: Y-m-d + - + handle: exclude_dates + field: + type: grid + fullscreen: false + display: 'Exclude Days' + add_row: 'Add Day' + if_any: + recurrence: 'contains_any monthly, daily, weekly, every' + fields: + - + handle: date + field: + type: date + allow_blank: false + allow_time: false + require_time: false + input_format: YYYY/M/D/YYYY + display: Date + format: Y-m-d + - + handle: times_sections + field: + type: section + display: Times + - + handle: all_day + field: + type: toggle + width: 33 + display: 'All Day?' + unless: + recurrence: 'equals multi_day' - handle: start_time field: type: time - width: 25 + width: 33 display: 'Start Time' instructions: 'Input in [24-hour format](https://en.wikipedia.org/wiki/24-hour_clock)' unless_any: @@ -106,7 +139,7 @@ fields: handle: end_time field: type: time - width: 25 + width: 33 display: 'End Time' instructions: 'Input in [24-hour format](https://en.wikipedia.org/wiki/24-hour_clock)' unless_any: @@ -170,22 +203,3 @@ fields: field: 'events::event.all_day' config: width: 25 - - - handle: exclude_dates - field: - type: grid - display: 'Exclude Days' - add_row: 'Add Day' - if_any: - recurrence: 'contains_any monthly, daily, weekly, every' - fields: - - - handle: date - field: - type: date - allow_blank: false - allow_time: false - require_time: false - input_format: YYYY/M/D/YYYY - display: Date - format: Y-m-d diff --git a/src/Events.php b/src/Events.php index d05c350..8950d88 100644 --- a/src/Events.php +++ b/src/Events.php @@ -200,7 +200,7 @@ private function isMultiDay(Entry $occurrence): bool private function occurrences(callable $generator): EntryCollection { return $this->entries - ->filter(fn (Entry $occurrence) => $this->hasStartDate($occurrence)) + ->filter(fn (Entry $event) => $this->hasStartDate($event)) // take each event and generate the occurrences ->flatMap(callback: $generator) ->reject(fn (Entry $occurrence) => collect($occurrence->exclude_dates) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index afc41c3..7679343 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -5,14 +5,17 @@ use Statamic\Entries\Entry; use Statamic\Facades\Collection; use Statamic\Fields\Field; +use Statamic\Fields\Fields; use Statamic\Fields\Value; use Statamic\Fieldtypes\Dictionary; use Statamic\Providers\AddonServiceProvider; +use Statamic\Statamic; class ServiceProvider extends AddonServiceProvider { public function bootAddon() { + // Fields::default('events_timezone', fn () => Statamic::displayTimezone()); collect(Events::setting('collections', [['collection' => 'events']])) ->each(fn (array $collection) => Collection::computed( $collection['collection'], diff --git a/src/Types/Event.php b/src/Types/Event.php index 8728ae6..e1893fc 100644 --- a/src/Types/Event.php +++ b/src/Types/Event.php @@ -79,6 +79,11 @@ public function nextOccurrences(int $limit = 1): Collection return $this->collect($this->rule()->getOccurrencesAfter(date: now(), inclusive: true, limit: $limit)); } + public function spansDays(): bool + { + return $this->start()->setTimezone(config('app.timezone'))->day != $this->end()->setTimezone(config('app.timezone'))->day; + } + public function startTime(): string { return $this->start_time ?? now()->startOfDay()->toTimeString('second'); diff --git a/src/Types/SingleDayEvent.php b/src/Types/SingleDayEvent.php index 50128d8..f82a76c 100644 --- a/src/Types/SingleDayEvent.php +++ b/src/Types/SingleDayEvent.php @@ -4,15 +4,27 @@ use RRule\RRule; use RRule\RRuleInterface; +use RRule\RSet; class SingleDayEvent extends Event { protected function rule(): RRuleInterface { - return new RRule([ + $rset = tap(new RSet)->addRRule([ 'count' => 1, - 'dtstart' => $this->start()->setTimeFromTimeString($this->endTime()), + 'dtstart' => $this->end(), 'freq' => RRule::DAILY, ]); + + // if the occurrence spans days, include the start so that it's picked up on the "between" method + if ($this->spansDays()) { + $rset->addRRule([ + 'count' => 1, + 'dtstart' => $this->start(), + 'freq' => RRule::DAILY, + ]); + } + + return $rset; } } diff --git a/tests/EventsTest.php b/tests/EventsTest.php index 0d84ef7..cea5da6 100755 --- a/tests/EventsTest.php +++ b/tests/EventsTest.php @@ -2,6 +2,7 @@ namespace TransformStudios\Events\Tests; +use Carbon\CarbonImmutable; use Illuminate\Support\Carbon; use Statamic\Extensions\Pagination\LengthAwarePaginator; use Statamic\Facades\Entry; @@ -386,3 +387,24 @@ expect($occurrences)->toBeEmpty(); }); + +test('app and event in different timezone ', function () { + $startDate = CarbonImmutable::createFromDate(2026, 2, 15); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $startDate->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '05:00', + 'end_time' => '16:00', + 'all_day' => false, + ])->save(); + + $events = Events::fromCollection('events') + ->between( + CarbonImmutable::createFromDate(2026, 2, 1)->startOfDay(), + CarbonImmutable::createFromDate(2026, 2, 15)->endOfDay() + ); + + expect($events)->toHaveCount(1); +}); diff --git a/tests/Types/RecurringDailyEventsTest.php b/tests/Types/RecurringDailyEventsTest.php index daf09f0..ac7c9b0 100755 --- a/tests/Types/RecurringDailyEventsTest.php +++ b/tests/Types/RecurringDailyEventsTest.php @@ -6,6 +6,7 @@ use Carbon\CarbonImmutable; use Statamic\Facades\Entry; use TransformStudios\Events\EventFactory; +use TransformStudios\Events\Events; test('null next date if now after end date', function () { $recurringEntry = Entry::make() @@ -68,150 +69,24 @@ expect($nextOccurrences[0]->start)->toEqual($startDate); }); -// public function test_can_generate_next_day_if_after() -// { -// $startDate = CarbonImmutable::now()->setTimeFromTimeString('11:00:00'); -// $event = [ -// 'start_date' => $startDate->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'recurrence' => 'daily', -// ]; -// Carbon::setTestNow($startDate->addMinute()); -// $event = EventFactory::createFromArray($event); -// $nextOccurrences = $event->nextOccurrences(1); -// $this->assertEquals($startDate->addDay(), $nextDate->start()); -// } -// public function test_can_generate_next_x_dates_from_today_before_event_time() -// { -// $startDate = Carbon::now()->setTimeFromTimeString('11:00:00'); -// $event = EventFactory::createFromArray( -// [ -// 'start_date' => $startDate->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'recurrence' => 'daily', -// ] -// ); -// for ($x = 0; $x < 2; $x++) { -// $events[] = $startDate->copy()->addDays($x); -// } -// $this->events->add($event); -// Carbon::setTestNow($startDate->copy()->subMinutes(1)); -// $nextDates = $this->events->upcoming(2); -// $this->assertCount(2, $nextDates); -// $this->assertEquals($events[0], $nextDates[0]->start()); -// $this->assertEquals($events[1], $nextDates[1]->start()); -// } -// public function test_can_generate_next_x_dates_from_today() -// { -// $startDate = Carbon::now()->setTimeFromTimeString('11:00:00'); -// $event = EventFactory::createFromArray([ -// 'start_date' => $startDate->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'recurrence' => 'daily', -// ]); -// for ($x = 0; $x < 3; $x++) { -// $events[] = $startDate->copy()->addDays($x); -// } -// $this->events->add($event); -// Carbon::setTestNow($startDate->copy()->addMinutes(1)); -// $nextDates = $this->events->upcoming(3); -// $this->assertCount(3, $nextDates); -// $this->assertEquals($events[0], $nextDates[0]->start()); -// $this->assertEquals($events[1], $nextDates[1]->start()); -// $this->assertEquals($events[2], $nextDates[2]->start()); -// } -// public function test_generates_all_occurrences_when_daily_after_start_date() -// { -// $startDate = Carbon::now()->setTimeFromTimeString('11:00:00'); -// $event = EventFactory::createFromArray( -// [ -// 'start_date' => $startDate->copy()->addDay()->toDateString(), -// 'start_time' => '11:00', -// 'end_time' => '12:00', -// 'end_date' => $startDate->copy()->addDays(3)->toDateString(), -// 'recurrence' => 'daily', -// ] -// ); -// for ($x = 2; $x <= 3; $x++) { -// $events[] = $startDate->copy()->addDays($x); -// } -// $this->events->add($event); -// Carbon::setTestNow($startDate->copy()->addDays(1)->addHour(1)); -// $nextEvents = $this->events->upcoming(3); -// $this->assertCount(2, $nextEvents); -// $this->assertEquals($events[0], $nextEvents[0]->start()); -// $this->assertEquals($events[1], $nextEvents[1]->start()); -// } -// public function test_can_get_last_day_when_before() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// 'end_date' => Carbon::now()->addDays(7)->toDateString(), -// ])); -// $from = Carbon::now()->addDays(7); -// $to = Carbon::now()->endOfDay()->addDays(10); -// $events = $this->events->all($from, $to); -// $this->assertCount(1, $events); -// } -// public function test_generates_all_daily_occurrences_single_event_from_to() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// 'end_date' => Carbon::now()->addDays(7)->toDateString(), -// ])); -// $from = Carbon::now()->subDays(1); -// $to = Carbon::now()->endOfDay()->addDays(10); -// $events = $this->events->all($from, $to); -// $this->assertCount(8, $events); -// } -// public function test_generates_all_daily_occurrences_single_event_from_to_without_end_date() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// ])); -// $from = Carbon::now()->subDays(1); -// $to = Carbon::now()->endOfDay()->addDays(10); -// $events = $this->events->all($from, $to); -// $this->assertCount(11, $events); -// } -// public function test_can_exclude_dates() -// { -// Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); -// $this->events->add(EventFactory::createFromArray([ -// 'id' => 'daily-event', -// 'start_date' => Carbon::now()->toDateString(), -// 'start_time' => '13:00', -// 'end_time' => '15:00', -// 'recurrence' => 'daily', -// 'except' => [ -// ['date' => Carbon::now()->addDays(2)->toDateString()], -// ['date' => Carbon::now()->addDays(4)->toDateString()], -// ], -// ])); -// $from = Carbon::now()->subDays(1); -// $to = Carbon::now()->endOfDay()->addDays(5); -// $events = $this->events->all($from, $to)->toArray(); -// $this->assertCount(4, $events); -// $this->assertEquals(Carbon::now()->toDateString(), $events[0]['start_date']); -// $this->assertEquals(Carbon::now()->addDays(1)->toDateString(), $events[1]['start_date']); -// $this->assertEquals(Carbon::now()->addDays(3)->toDateString(), $events[2]['start_date']); -// $this->assertEquals(Carbon::now()->addDays(5)->toDateString(), $events[3]['start_date']); -// } +test('app and event in different timezone ', function () { + $startDate = CarbonImmutable::createFromDate(2026, 2, 15); + Entry::make() + ->collection('events') + ->data([ + 'start_date' => $startDate->toDateString(), + 'timezone' => 'America/Los_Angeles', + 'start_time' => '05:00', + 'end_time' => '16:00', + 'recurrence' => 'monthly', + 'specific_days' => ['third_monday'], + ])->save(); + + $events = Events::fromCollection('events') + ->between( + CarbonImmutable::createFromDate(2026, 2, 15)->startOfDay(), + CarbonImmutable::createFromDate(2026, 3, 16)->endOfDay() + ); + + expect($events)->toHaveCount(2); +}); diff --git a/tests/Types/RecurringEveryXEventsTest.php b/tests/Types/RecurringEveryXEventsTest.php index 1079266..87f52d6 100755 --- a/tests/Types/RecurringEveryXEventsTest.php +++ b/tests/Types/RecurringEveryXEventsTest.php @@ -245,120 +245,3 @@ expect($occurrences[0]->start)->toEqual($events[0]); expect($occurrences[1]->start)->toEqual($events[1]); }); - -/* - public function test_can_get_last_day_when_before() - { - Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); - - $event = [ - 'id' => 'daily-event', - 'start_date' => Carbon::now()->toDateString(), - 'start_time' => '13:00', - 'end_time' => '15:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'days', - 'end_date' => Carbon::now()->addDays(8)->toDateString(), - ]; - - $this->events->add(EventFactory::createFromArray($event)); - - $from = Carbon::now()->addDays(7); - $to = Carbon::now()->endOfDay()->addDays(10); - - $events = $this->events->all($from, $to); - - $this->assertCount(1, $events); - - $event['start_date'] = Carbon::now()->addDays(8)->toDateString(); - - $this->assertEquals($event, $events[0]->toArray()); - } - - public function test_generates_all_daily_occurrences_single_event_from_to_with_end_date() - { - Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); - - $this->events->add(EventFactory::createFromArray( - [ - 'id' => 'daily-event', - 'start_date' => Carbon::now()->toDateString(), - 'start_time' => '13:00', - 'end_time' => '15:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'days', - 'end_date' => Carbon::now()->addDays(8)->toDateString(), - ] - )); - - $from = Carbon::now()->subDays(1); - $to = Carbon::now()->endOfDay()->addDays(10); - - $events = $this->events->all($from, $to); - - $this->assertCount(5, $events); - } - - public function test_generates_all_daily_occurrences_single_event_from_to_without_end_date() - { - Carbon::setTestNow(Carbon::now()->setTimeFromTimeString('10:30')); - - $this->events->add(EventFactory::createFromArray( - [ - 'id' => 'daily-event', - 'start_date' => Carbon::now()->toDateString(), - 'start_time' => '13:00', - 'end_time' => '15:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'days', - ] - )); - - $from = Carbon::now()->subDays(1); - $to = Carbon::now()->endOfDay()->addDays(10); - - $events = $this->events->all($from, $to); - - $this->assertCount(6, $events); - } - - public function test_can_generate_next_x_weeks_if_in_different_weeks() - { - $event = EventFactory::createFromArray( - [ - 'start_date' => '2020-01-03', - 'start_time' => '11:00', - 'end_time' => '12:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'weeks', - ] - ); - - $day = $event->upcomingDate(Carbon::parse('2021-01-31')); - - $this->assertNotNull($day); - $this->assertEquals('2021-02-12', $day->startDate()); - } - - public function test_returns_null_when_dates_between_dont_have_event() - { - $event = EventFactory::createFromArray( - [ - 'start_date' => '2021-01-29', - 'start_time' => '11:00', - 'end_time' => '12:00', - 'recurrence' => 'every', - 'interval' => 2, - 'period' => 'weeks', - ] - ); - - $dates = $event->datesBetween('2021-02-18', '2021-02-19'); - - $this->assertEmpty($dates); - } -*/