|
| 1 | +# Design: Upcoming Calendar View |
| 2 | + |
| 3 | +**Date:** 2026-02-24 |
| 4 | +**Status:** Approved |
| 5 | + |
| 6 | +## Problem |
| 7 | + |
| 8 | +There is no way to see at a glance which tasks will run over the next 1–3 days. The existing Tasks list shows last-run stats and the next single upcoming run per task, but gives no sense of density, overlap, or schedule across a time window. |
| 9 | + |
| 10 | +## Solution |
| 11 | + |
| 12 | +Add an "Upcoming" page with an Outlook-style time-grid calendar showing all scheduled task runs over a 1-day or 3-day window. The backend computes run times from each task's cron expression; the frontend renders a pure CSS/Vue grid — no new JS dependencies. |
| 13 | + |
| 14 | +## Architecture |
| 15 | + |
| 16 | +### Routes |
| 17 | + |
| 18 | +Two routes added to `web.php` inside the existing `tasks` group, before the `{totemTask}` wildcard: |
| 19 | + |
| 20 | +```php |
| 21 | +Route::get('upcoming', 'UpcomingTasksController@index')->name('totem.upcoming'); |
| 22 | +Route::get('upcoming/events', 'UpcomingTasksController@events')->name('totem.upcoming.events'); |
| 23 | +``` |
| 24 | + |
| 25 | +### Sidebar |
| 26 | + |
| 27 | +A second nav item — "Upcoming" — added to `resources/views/partials/sidebar.blade.php`, linking to `totem.upcoming`. |
| 28 | + |
| 29 | +### Files |
| 30 | + |
| 31 | +- Pop stash: `resources/views/tasks/schedules.blade.php` → repurposed as the calendar Blade page |
| 32 | +- Pop stash: `resources/assets/js/tasks/components/ScheduleRow.vue` → repurposed as `UpcomingCalendar.vue` |
| 33 | +- Create: `src/Http/Controllers/UpcomingTasksController.php` |
| 34 | +- Register Vue component in the existing app JS entry point |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Backend — UpcomingTasksController |
| 39 | + |
| 40 | +### `index()` |
| 41 | +Returns the Blade view `totem::tasks.schedules`. No data passed — Vue fetches everything via AJAX. |
| 42 | + |
| 43 | +### `events()` |
| 44 | + |
| 45 | +**Query parameters:** |
| 46 | +- `start` — ISO 8601 timestamp (default: now, floored to current minute) |
| 47 | +- `days` — integer, 1 or 3 (default: 1) |
| 48 | + |
| 49 | +**Logic:** |
| 50 | +1. Parse `$start` with Carbon, compute `$end = $start->copy()->addDays($days)` |
| 51 | +2. Load all active tasks via `EloquentTaskRepository::findAllActive()` |
| 52 | +3. For each task, loop using `CronExpression::factory($task->getCronExpression())->getNextRunDate($cursor)`, advancing `$cursor` to each result until `$cursor >= $end` |
| 53 | +4. Collect events as `{ task_id, description, command, scheduled_at (ISO 8601) }` |
| 54 | + |
| 55 | +**Response:** |
| 56 | +```json |
| 57 | +{ |
| 58 | + "start": "2026-02-24T00:00:00+00:00", |
| 59 | + "end": "2026-02-25T00:00:00+00:00", |
| 60 | + "days": 1, |
| 61 | + "events": [ |
| 62 | + { "task_id": 1, "description": "Send daily report", "command": "report:daily", "scheduled_at": "2026-02-24T08:00:00+00:00" } |
| 63 | + ] |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +All events shown — no truncation for high-frequency tasks. |
| 68 | + |
| 69 | +--- |
| 70 | + |
| 71 | +## Frontend — UpcomingCalendar.vue |
| 72 | + |
| 73 | +### State |
| 74 | +- `currentStart` — Date, defaults to start of current hour |
| 75 | +- `days` — integer, 1 or 3 (default: 1) |
| 76 | +- `events` — array of event objects from API |
| 77 | +- `loading` — boolean |
| 78 | + |
| 79 | +### Grid Layout |
| 80 | +CSS grid with `days + 1` columns: |
| 81 | +- Column 1: time labels (00:00 – 23:00) |
| 82 | +- Columns 2…n: one per day in the window |
| 83 | + |
| 84 | +25 rows: |
| 85 | +- Row 1: header row with date label per day column |
| 86 | +- Rows 2–25: hourly slots 00:00–23:00 |
| 87 | + |
| 88 | +Event chips are placed in the cell matching their day column and hour row. Multiple events in the same cell stack vertically. Each chip shows the task description (truncated ~20 chars) and exact run time (HH:mm). |
| 89 | + |
| 90 | +### Controls |
| 91 | +- **1-day / 3-day toggle** — updates `days`, re-fetches |
| 92 | +- **Prev / Next arrows** — shift `currentStart` by `days` days, re-fetches |
| 93 | +- **Today button** — resets `currentStart` to now, re-fetches |
| 94 | +- **Loading spinner** — shown while fetch in progress (UIKit spinner) |
| 95 | +- **Error alert** — UIKit alert on fetch failure |
| 96 | + |
| 97 | +### Data Flow |
| 98 | +On `mounted()` and whenever `currentStart` or `days` changes (watcher), fetch: |
| 99 | +``` |
| 100 | +GET /totem/tasks/upcoming/events?start=<ISO>&days=<1|3> |
| 101 | +``` |
| 102 | +Populate `events` from response. Frontend performs no cron computation — display only. |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Non-Goals |
| 107 | +- Condensing/grouping high-frequency tasks (deferred) |
| 108 | +- Click-through to task detail from calendar chip (can be added later — chips link to `totem.task.view`) |
| 109 | +- Timezone selector (uses server timezone) |
0 commit comments