Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/workplace/e2e/local/desk-times.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ test.describe('desk booking times', () => {
test('a maximum length and bookable hours limit what can be chosen', async ({
staffPage,
}) => {
const min = LIMITED_HOURS['app.bookings.min_duration'];
const max = LIMITED_HOURS['app.bookings.max_duration'];
const hours = LIMITED_HOURS['app.bookings.bookable_hours'];

Expand Down Expand Up @@ -145,10 +146,12 @@ test.describe('desk booking times', () => {
`no start time before ${hours.start}:00 may be offered, got ` +
`${JSON.stringify(times)}`,
).toBeGreaterThanOrEqual(hours.start * 60);
// The last start must leave the minimum length before closing time.
// A start at closing time has no valid length (PPT-2872).
expect(
Math.max(...minutes),
`no start time after ${hours.end}:00 may be offered, got ` +
`${JSON.stringify(times)}`,
).toBeLessThanOrEqual(hours.end * 60);
`no start time later than ${min} minutes before ${hours.end}:00 ` +
`may be offered, got ${JSON.stringify(times)}`,
).toBeLessThanOrEqual(hours.end * 60 - min);
});
});
7 changes: 4 additions & 3 deletions apps/workplace/e2e/local/room-times.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const DAY = 86_400;
const window_from = () => Math.floor(Date.now() / 1000) - 2 * DAY;
const window_to = () => Math.floor(Date.now() / 1000) + 7 * DAY;

const MIN_MINUTES = LIMITED_HOURS['app.events.min_duration'];
const MAX_MINUTES = LIMITED_HOURS['app.events.max_duration'];
const HOURS = LIMITED_HOURS['app.events.bookable_hours'];

Expand Down Expand Up @@ -147,9 +148,9 @@ test.describe('room booking times', () => {
).toBeGreaterThanOrEqual(HOURS.start * 60);
expect(
Math.max(...times.map(minutesOfDay)),
`nor after ${HOURS.end}:00 — an option outside the window is one a user can ` +
'pick and then be refused for',
).toBeLessThanOrEqual(HOURS.end * 60);
`nor later than ${MIN_MINUTES} minutes before ${HOURS.end}:00 — a later ` +
'start leaves no meeting length that ends by closing time (PPT-2872)',
).toBeLessThanOrEqual(HOURS.end * 60 - MIN_MINUTES);
});

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export type DeskFormType = 'single' | 'group' | 'other';
"
[ngModelOptions]="{ standalone: true }"
[range]="bookable_hours()"
[min_duration]="effective_min_duration()"
[use_24hr]="use_24hr()"
[timezone]="timezone"
/>
Expand Down Expand Up @@ -449,6 +450,11 @@ export class DeskFlowDetailsComponent {
settingSignal('desks.custom_duration_options')() ||
settingSignal('bookings.custom_duration_options', [] as number[])(),
);
/** Shortest length the duration field offers. The start-time field uses it
* so that the last start leaves room for a booking before closing time. */
public readonly effective_min_duration = computed(() =>
Math.min(this.min_duration(), ...this.custom_duration_options()),
);
public readonly can_book_lockers = settingSignal(
'desks.can_book_lockers',
false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ import { DeskFlowSelectMapComponent } from './desk-flow-select-map.component';
"
[ngModelOptions]="{ standalone: true }"
[range]="bookable_hours()"
[min_duration]="effective_min_duration()"
[use_24hr]="use_24hr()"
[timezone]="timezone"
/>
Expand Down Expand Up @@ -489,6 +490,7 @@ import { DeskFlowSelectMapComponent } from './desk-flow-select-map.component';
"
[ngModelOptions]="{ standalone: true }"
[range]="bookable_hours()"
[min_duration]="effective_min_duration()"
[use_24hr]="use_24hr()"
[timezone]="timezone"
/>
Expand Down Expand Up @@ -639,6 +641,11 @@ export class DeskFlowSelectComponent extends AsyncHandler implements OnInit {
settingSignal('desks.custom_duration_options')() ||
settingSignal('bookings.custom_duration_options', [] as number[])(),
);
/** Shortest length the duration field offers. The start-time field uses it
* so that the last start leaves room for a booking before closing time. */
public readonly effective_min_duration = computed(() =>
Math.min(this.min_duration(), ...this.custom_duration_options()),
);
public readonly bookable_hours = computed<
{ start: number; end: number } | undefined
>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import { ROOM_CAPACITY_OPTIONS, roomCapacityFilters } from './room-capacity';
"
[ngModelOptions]="{ standalone: true }"
[range]="bookable_hours()"
[min_duration]="effective_min_duration()"
[use_24hr]="use_24hr()"
[timezone]="timezone"
[disabled]="start_time_disabled()"
Expand Down Expand Up @@ -317,6 +318,11 @@ export class MeetingFlowDetailsComponent {
'events.custom_duration_options',
[],
);
/** Shortest length the duration field offers. The start-time field uses it
* so that the last start leaves room for a booking before closing time. */
public readonly effective_min_duration = computed(() =>
Math.min(this.min_duration(), ...this.custom_duration_options()),
);
/** Number of days ahead that the user can book a meeting */
public readonly allowed_future_days = settingSignal(
'events.allowed_future_days',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ import { ROOM_CAPACITY_OPTIONS, roomCapacityFilters } from './room-capacity';
[ngModelOptions]="{ standalone: true }"
[disabled]="form.date().disabled()"
[range]="bookable_hours()"
[min_duration]="effective_min_duration()"
[use_24hr]="use_24hr()"
[timezone]="timezone"
/>
Expand Down Expand Up @@ -586,6 +587,7 @@ import { ROOM_CAPACITY_OPTIONS, roomCapacityFilters } from './room-capacity';
[ngModelOptions]="{ standalone: true }"
[disabled]="form.date().disabled()"
[range]="bookable_hours()"
[min_duration]="effective_min_duration()"
[use_24hr]="use_24hr()"
[timezone]="timezone"
/>
Expand Down Expand Up @@ -776,6 +778,11 @@ export class MeetingFlowSpaceSelectComponent {
'events.custom_duration_options',
[],
);
/** Shortest length the duration field offers. The start-time field uses it
* so that the last start leaves room for a booking before closing time. */
public readonly effective_min_duration = computed(() =>
Math.min(this.min_duration(), ...this.custom_duration_options()),
);
public readonly allow_all_day = settingSignal(
'events.allow_all_day',
false,
Expand Down
4 changes: 2 additions & 2 deletions docs/settings/workplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ These apply to all resource booking flows (desks, parking, lockers). Most can be
| `bookings.allowed_daily_parking_count` | number | – | Number of parking bookings a user can make for a single day. Use `0` for unlimited. Falls back to `1`. |
| `bookings.allowed_daily_locker_count` | number | – | Number of locker bookings a user can make for a single day. Use `0` for unlimited. Falls back to `1`. |
| `bookings.all_day_period` | object | – | Time window used for "all day" resource bookings, as hours of the day, e.g. `{ "start": 8, "end": 18 }`. Override per type under `desks.*`, `parking.*` or `lockers.*`. |
| `bookings.bookable_hours` | object | – | Time window in which bookings can be made, as hours of the day, e.g. `{ "start": 8, "end": 19 }`. |
| `bookings.bookable_hours` | object | – | Time window in which bookings can be made, as hours of the day, e.g. `{ "start": 8, "end": 19 }`. A booking must end by `end`, so the last start time is `end` minus the shortest duration option. |
| `bookings.can_book_for_others` | boolean | – | Allow users to make bookings on behalf of other users. |
| `bookings.can_book_for_anyone` | boolean | – | Allow users to make bookings on behalf of anyone, including people outside the organisation. |
| `bookings.custom_duration_options` | number[] | – | Custom list of duration options, in minutes, offered when selecting a booking duration. |
Expand Down Expand Up @@ -203,7 +203,7 @@ These settings override the matching `bookings.*` settings for visitor invites.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `events.bookable_hours` | object | – | Time window in which meetings can be booked, as hours of the day, e.g. `{ "start": 8, "end": 19 }`. |
| `events.bookable_hours` | object | – | Time window in which meetings can be booked, as hours of the day, e.g. `{ "start": 8, "end": 19 }`. A meeting must end by `end`, so the last start time is `end` minus the shortest duration option. |
| `events.all_day_period` | object | – | Time window used for "all day" meetings, as hours of the day. |
| `events.multiple_spaces` | boolean | `false` | Allow users to book multiple spaces in a single meeting. |
| `events.allow_multiple_spaces` | boolean | – | Legacy alias for `events.multiple_spaces`. Either key set to `true` enables multiple room selection. Use `events.multiple_spaces` for new configuration. |
Expand Down
4 changes: 3 additions & 1 deletion e2e/support/desk/desk.settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export async function useSettings(
}

/**
* Cap the booking length and confine it to office hours.
* Cap the booking length and confine it to office hours. The minimum length
* sets the last start time: a booking must end by closing time.
*
* Both unset by default, so nothing else in the suite sees them, and both fail
* quietly in the same way: an out-of-range option is simply ABSENT from the
* picker rather than refused. That is why the times spec asserts on what is
* offered.
*/
export const LIMITED_HOURS = {
'app.bookings.min_duration': 30,
'app.bookings.max_duration': 60,
'app.bookings.bookable_hours': { start: 9, end: 17 },
};
Expand Down
4 changes: 3 additions & 1 deletion e2e/support/room/room.settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export const NO_APPROVAL = {
};

/**
* Cap the meeting length and confine it to office hours.
* Cap the meeting length and confine it to office hours. The minimum length
* sets the last start time: a meeting must end by closing time.
*
* `app.events.*`, NOT `app.bookings.*`. The meeting form reads its limits from
* the EVENT settings (`event-form.service.ts`: `app.events.max_duration`,
Expand All @@ -117,6 +118,7 @@ export const NO_APPROVAL = {
* the first run of `room-times.spec.ts`.
*/
export const LIMITED_HOURS = {
'app.events.min_duration': 30,
'app.events.max_duration': 60,
'app.events.bookable_hours': { start: 9, end: 17 },
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ import { BookingFormService } from '../booking-form.service';
[use_24hr]="use_24hr()"
[timezone]="timezone()"
[range]="bookable_hours()"
[min_duration]="60"
></a-time-field>
</div>
<div class="w-1/3 flex-1">
Expand Down
Loading