Skip to content

Commit 7f88bc5

Browse files
qschmickclaude
andauthored
Add upcoming calendar view (#407)
* Add upcoming calendar view design doc * Add upcoming calendar view implementation plan * Add UpcomingTasksController and calendar page scaffold * Add UpcomingCalendar Vue component with 1-day and 3-day views * Fix race condition and v-else-if in UpcomingCalendar * Fix review issues: RefreshDatabase, loop guard, validation, sidebar active state, timezone, null guard * Apply fixes from StyleCI (#406) * Improve upcoming calendar: bug fixes, accessibility, UX - Fix _fetchGen reactivity bug (Vue 2 skips _ prefixed data props, causing loading to stick permanently) - Fix webpack UIKit parse failure by adding noParse for uikit/dist/js - Replace color palette with WCAG AA-compliant colors (all ≥ 4.5:1 contrast with white text, sourced from Tailwind 700/800 tier) - Sort events by scheduled_at then command for consistent pill order - Clamp past start times to now in the backend - Send midnight as start from frontend; backend clamps to now - Group same-minute pills side by side with flex-wrap per minute row - Persist start/end/days in URL query params for refresh/share - Make pills direct links to the task detail page - Increase description truncation from 20 to 50 characters - Pass task-base-url prop from blade for task links * Add testbench dev setup and PR screenshots - testbench.yaml: configure workbench with migrations and ScreenshotSeeder - database/seeders/: ScreenshotSeeder and DatabaseSeeder with 10 realistic tasks for local development and screenshot capture - docs/screenshots/pr-407/: Puppeteer capture script and PNG screenshots of upcoming 1-day view, 3-day view, and tasks list * Update PR screenshots to reflect latest UI changes * Add CACHE_STORE env to phpunit.xml for Laravel 11/12 compatibility * Fix flaky charset case in ExportTasksTest Use strtolower() for case-insensitive Content-Type assertion since PHP produces charset=UTF-8 or charset=utf-8 depending on the environment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 12ff84c commit 7f88bc5

22 files changed

Lines changed: 67363 additions & 31 deletions

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"psr-4": {
4141
"Studio\\Totem\\": "src/",
4242
"Studio\\Totem\\Tests\\": "tests/",
43-
"Database\\Factories\\": "database/factories/"
43+
"Database\\Factories\\": "database/factories/",
44+
"Database\\Seeders\\": "database/seeders/"
4445
},
4546
"files": [
4647
"src/helpers.php"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Database\Seeders;
4+
5+
use Illuminate\Database\Seeder;
6+
7+
class DatabaseSeeder extends Seeder
8+
{
9+
public function run(): void
10+
{
11+
$this->call(ScreenshotSeeder::class);
12+
}
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Database\Seeders;
4+
5+
use Illuminate\Database\Seeder;
6+
use Studio\Totem\Task;
7+
8+
class ScreenshotSeeder extends Seeder
9+
{
10+
public function run(): void
11+
{
12+
$tasks = [
13+
['description' => 'Send daily digest email', 'command' => 'email:digest', 'expression' => '0 8 * * *'],
14+
['description' => 'Generate sales report', 'command' => 'report:sales', 'expression' => '0 9 * * 1-5'],
15+
['description' => 'Sync user data', 'command' => 'sync:users', 'expression' => '*/30 * * * *'],
16+
['description' => 'Clear expired sessions', 'command' => 'session:clear', 'expression' => '0 0 * * *'],
17+
['description' => 'Database backup', 'command' => 'backup:run', 'expression' => '0 2 * * *'],
18+
['description' => 'Send weekly summary', 'command' => 'report:weekly', 'expression' => '0 10 * * 1'],
19+
['description' => 'Process payment queue', 'command' => 'payments:process', 'expression' => '*/15 * * * *'],
20+
['description' => 'Prune old logs', 'command' => 'logs:prune', 'expression' => '0 3 * * *'],
21+
['description' => 'Health check ping', 'command' => 'health:check', 'expression' => '*/5 * * * *'],
22+
['description' => 'Archive completed orders', 'command' => 'orders:archive', 'expression' => '0 1 * * *'],
23+
];
24+
25+
foreach ($tasks as $attributes) {
26+
Task::create(array_merge($attributes, ['is_active' => true]));
27+
}
28+
}
29+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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

Comments
 (0)