Skip to content

A Time field holds one time, so a 9-to-12 shift needs two fields and reads "9:00 AM · 12:00 PM" #590

Open

Description

@richshaw

Needs your OK: this changes the field editor and the slot editor, and how a shift's time reads on the signup page, in emails and in "Add to calendar". Swap pipeline:proposed for pipeline:ready to have it built.

What happens

A Time field holds one time of day, as HH:MM (TimeConfigSchema in src/schemas/slot-fields.ts, checked by validateOneValue in src/services/slot-fields.ts). Most shifts have a start and an end. So an organizer building shifts of 9–12, 12–3 and 3–6 has to add two Time fields, such as Starts and Ends, and fill in both on every slot.

The two values then read as two separate things:

  • The signup page and the Build tab join a slot's values with " · ". A shift reads "9:00 AM · 12:00 PM", not "9:00 AM – 12:00 PM" (meta.join(' · ') in src/app/s/[slug]/signup-view.tsx, and the row summary in src/components/build-wysiwyg/WysiwygSlot.tsx).
  • Emails print "Starts: 9:00 AM" and "Ends: 12:00 PM" on separate lines (slotDetails in src/lib/slot-label.ts). The subject line has "9:00 AM · 12:00 PM".
  • "Add to calendar" knows only the start. handleDownloadIcs in src/app/s/[slug]/commit-dialog.tsx passes no end, so buildIcs in src/lib/ics.ts makes every timed slot one hour long. A 9–12 shift goes into the calendar as 9–10.

The AI paths have the same gap. The field guides for Magic Compose and the AI connector say a time is HH:MM, used for "shift starts" (FIELD_TYPE_GUIDE in src/lib/signup-rules.ts, FIELD_GUIDE in src/mcp/tools/guides.ts). So an AI draft keeps the start and drops the end, or types the range into a text field. #503's check 7 already expects ranges typed into text fields, like "Morning 8–12".

Fix, in outline

  • One field, with an optional end. TimeConfigSchema gains an optional hasEnd: boolean. It lives in the field's config jsonb, so there is no migration.
  • The value. A Time field with hasEnd takes "09:00" or "09:00-12:00". A plain Time field still takes only "09:00". validateOneValue checks both times with isRealTime, and refuses an end that isn't later than the start.
  • One place reads the start. slotTimeOfDay in src/lib/reminder-fields.ts returns the start of a range. extractSlotAt, the signup page's hasTime, reminders and the sign-up lockout then work from the start time, as they do today.
  • One place shows it. renderFieldValue in src/lib/slot-label.ts shows "9:00 AM – 12:00 PM". The Build tab, the signup page, the emails and their subjects, and the Responses tab all go through it, so they agree.
  • "Add to calendar" gets the end. A small helper beside slotTimeOfDay gives the end of the same paired time field, and the dialog passes it to buildIcs as end.
  • The Build tab. The field editor (InlineFieldForm.tsx) shows a "Has an end time" checkbox for a Time field, when adding it and when editing it. The slot editor (SlotEditor.tsx) shows two time boxes for that field, "From" and "To", and saves them as one value.
  • The AI connector. FIELD_GUIDE says that a Time field with an end time takes HH:MM-HH:MM. The tools already accept the config and the values through the shared schemas.

Watch for

Choices this makes

  • One field with an end, not a new field type or two linked fields. Emails print each field as one "Label: value" line (CLAUDE.md, Email), and the code avoids rules that guess which fields go together. Two linked fields would need such a rule. A new type would double every fieldType === 'time' check, including the reminder pairing in pairedTimeField.
  • Stored as "HH:MM-HH:MM", a string like every other value. The Build tab's rows hold strings, and the download and grouping keys keep working. An object, { start, end }, is the alternative.
  • The end must be later than the start. Overnight shifts (10 PM – 2 AM) are left for later. They would need an end before the start to mean "the next day", in the wording and in the calendar export.
  • Shown as "9:00 AM – 12:00 PM", with an en dash, everywhere.
  • Existing signups keep their two fields. Nothing merges a Starts and Ends pair.
  • The checkbox says "Has an end time".

Done when

  • A Time field with "Has an end time" saves 09:00-12:00 from the Build tab. The slot reads "9:00 AM – 12:00 PM" on the Build tab, the signup page, the confirmation and reminder emails, and the Responses tab.
  • "Add to calendar" for that slot exports 9:00 to 12:00.
  • Unit tests: validateOneValue takes a start alone and a range, and refuses an end at or before the start, and a range in a plain Time field. slotTimeOfDay returns the start. renderFieldValue shows the range.
  • DB tests: a slot with a range gets the same slot_at as a slot with only its start. Turning the end time off while a slot holds a range is refused.
  • pnpm lint && pnpm typecheck && pnpm knip && pnpm test and pnpm test:db pass.

Found by the UX walkthrough (scenario O2) on 2026-10-24, and checked against the code on main at 4ed27a9.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions