Skip to content

Commit b1b17fc

Browse files
devmountrwood-mozdavinotdaviddependabot[bot]
authored
Fix custom availabilities when switching dates (#1336)
* 🔨 Fix custom availabilities when switching dates * 💚 Implement current design changes * 🔨 Fix availability validation * 🔨 Clean up * Disable deactivation of default calendar and sync availability (#1334) * 🔨 Disable deactivation of default calendar, sync availability * 🔨 Fix calendar row alignment * 🔨 Only show dropdown if at least one active button exists * 🔨 Fix CSS grid * Fix overlapping hold events and auto confirm setting (#1338) * 🔨 Fix the auto-confirm settings to align with db * 🔨 Prevent overlapping events in the week calendar * Migrate the E2E tests to the new URLs and use TB Accounts for sign-in (#1314) * Migrate the E2E tests to the new URLs and use TB Accounts for sign-in * Remove unused const and import * npm audit fix (#1350) * Bump the npm_and_yarn group across 1 directory with 1 update (#1351) Bumps the npm_and_yarn group with 1 update in the /test/e2e directory: [js-yaml](https://github.com/nodeca/js-yaml). Updates `js-yaml` from 4.1.0 to 4.1.1 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.1.0...4.1.1) Updates `js-yaml` from 3.14.1 to 3.14.2 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.1.0...4.1.1) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect dependency-group: npm_and_yarn - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 🔨 Replace favicon, add webmanifest, respect color scheme (#1353) * Remove VITE_LANDING_PAGE_URL env var and redirect to login if not logged in (#1356) * Fix schedule public availability test (#1357) * Remove email as login_hint from GoogleOAuth (#1358) * ➕ Add unit tests for schedule availability --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Rob Wood <rwood@thunderbird.net> Co-authored-by: Davi Nakano <114549747+davinotdavid@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent c3a4379 commit b1b17fc

File tree

6 files changed

+176
-34
lines changed

6 files changed

+176
-34
lines changed

backend/src/appointment/database/repo/schedule.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,23 @@ def all_availability_is_valid(schedule: schemas.ScheduleValidationIn):
9898

9999
# Make sure, that the availabilities are sorted by weekday AND by start time. This is important for checking
100100
# validity of times of adjacent availabilities at the same day
101-
for i, a in enumerate(sorted(schedule.availabilities, key=lambda x: (x.day_of_week.value, x.start_time))):
101+
availabilities = sorted(schedule.availabilities, key=lambda x: (x.day_of_week.value, x.start_time))
102+
for i, a in enumerate(availabilities):
102103
# Check valid times (start time before end time) and duration (end time at least x minutes after start)
103104
if not utils.is_valid_time_range(a.start_time, a.end_time, schedule.slot_duration):
104105
return False
105106
# If a previous slot exists on that day, fail if the times overlap
106107
if (
107108
i > 0
108-
and a.day_of_week == schedule.availabilities[i - 1].day_of_week
109-
and not utils.is_valid_time_range(schedule.availabilities[i - 1].end_time, a.start_time)
109+
and (a.day_of_week.value == availabilities[i - 1].day_of_week.value)
110+
and not utils.is_valid_time_range(availabilities[i - 1].end_time, a.start_time)
110111
):
111112
return False
112113
# If a next slot exists on that day, fail if the times overlap
113114
if (
114115
i < size - 1
115-
and a.day_of_week == schedule.availabilities[i + 1].day_of_week
116-
and not utils.is_valid_time_range(a.end_time, schedule.availabilities[i + 1].start_time)
116+
and (a.day_of_week.value == availabilities[i + 1].day_of_week.value)
117+
and not utils.is_valid_time_range(a.end_time, availabilities[i + 1].start_time)
117118
):
118119
return False
119120

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from appointment.database.repo.schedule import all_availability_is_valid
2+
from appointment.database import schemas
3+
from datetime import date, time
4+
5+
6+
class TestScheduleAvailability:
7+
8+
def test_empty_availability_is_valid(self):
9+
# Test empty availability is valid
10+
schedule = schemas.ScheduleValidationIn(
11+
name='test',
12+
calendar_id=1,
13+
slot_duration=30,
14+
start_date=date.today(),
15+
start_time=time(9, 0),
16+
end_time=time(17, 0),
17+
)
18+
assert all_availability_is_valid(schedule)
19+
20+
21+
def test_all_availability_is_valid(self):
22+
# Test already sorted availabilities
23+
schedule = schemas.ScheduleValidationIn(
24+
name='test',
25+
calendar_id=1,
26+
slot_duration=30,
27+
start_date=date.today(),
28+
start_time=time(9, 0),
29+
end_time=time(17, 0),
30+
availabilities=[
31+
schemas.AvailabilityValidationIn(
32+
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(10, 0)
33+
),
34+
schemas.AvailabilityValidationIn(
35+
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(11, 0)
36+
),
37+
schemas.AvailabilityValidationIn(
38+
schedule_id=1, day_of_week=2, start_time=time(9, 0), end_time=time(10, 0)
39+
),
40+
]
41+
)
42+
assert all_availability_is_valid(schedule)
43+
44+
# Test unordered availabilities
45+
schedule.availabilities = [
46+
schemas.AvailabilityValidationIn(
47+
schedule_id=1, day_of_week=1, start_time=time(15, 0), end_time=time(16, 0)
48+
),
49+
schemas.AvailabilityValidationIn(
50+
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(11, 0)
51+
),
52+
schemas.AvailabilityValidationIn(
53+
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(10, 0)
54+
),
55+
schemas.AvailabilityValidationIn(
56+
schedule_id=1, day_of_week=1, start_time=time(17, 0), end_time=time(18, 0)
57+
),
58+
]
59+
assert all_availability_is_valid(schedule)
60+
61+
62+
def test_all_availability_is_invalid(self):
63+
# Test overlapping end-start times
64+
schedule = schemas.ScheduleValidationIn(
65+
name='test',
66+
calendar_id=1,
67+
slot_duration=30,
68+
start_date=date.today(),
69+
start_time=time(9, 0),
70+
end_time=time(17, 0),
71+
availabilities=[
72+
schemas.AvailabilityValidationIn(
73+
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(11, 0)
74+
),
75+
schemas.AvailabilityValidationIn(
76+
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(12, 0)
77+
),
78+
]
79+
)
80+
assert not all_availability_is_valid(schedule)
81+
82+
# Test completely overlapping slots
83+
schedule.availabilities = [
84+
schemas.AvailabilityValidationIn(
85+
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(12, 0)
86+
),
87+
schemas.AvailabilityValidationIn(
88+
schedule_id=1, day_of_week=1, start_time=time(10, 0), end_time=time(11, 0)
89+
),
90+
]
91+
assert not all_availability_is_valid(schedule)
92+
93+
# Test slots with invalid start/end time
94+
schedule.availabilities = [
95+
schemas.AvailabilityValidationIn(
96+
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(12, 0)
97+
),
98+
schemas.AvailabilityValidationIn(
99+
schedule_id=1, day_of_week=1, start_time=time(14, 0), end_time=time(13, 0)
100+
),
101+
]
102+
assert not all_availability_is_valid(schedule)
103+
104+
# Test slots that are too small for the defined duration
105+
schedule.availabilities = [
106+
schemas.AvailabilityValidationIn(
107+
schedule_id=1, day_of_week=1, start_time=time(9, 0), end_time=time(9, 15)
108+
),
109+
]
110+
assert not all_availability_is_valid(schedule)

frontend/src/views/AvailabilityView/components/AvailabilitySettings/components/AvailabilityBookingWindowPill.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,18 @@ const bookingWindowOptions: SelectOption[] = [1, 2, 3, 4].map((d) => ({
3030
<segmented-control
3131
v-model="bookingWindow"
3232
name="booking-window"
33+
class="booking-window-segmented-control"
3334
:required="false"
3435
:legend="t('label.bookingWindow')"
3536
:options="bookingWindowOptions"
3637
:disabled="!currentState.active"
3738
>
3839
{{ t('label.bookingWindow') }}
3940
</segmented-control>
40-
</template>
41+
</template>
42+
43+
<style>
44+
.booking-window-segmented-control ul {
45+
font-size: 0.875rem;
46+
}
47+
</style>

frontend/src/views/AvailabilityView/components/AvailabilitySettings/components/AvailabilityMinimumNoticePill.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const earliestOptions: SelectOption[] = [0, 0.5, 1, 2, 3, 4, 5].map((d) => {
3838

3939
<template>
4040
<segmented-control
41-
class="minimum-notice-segment-control"
41+
class="minimum-notice-segmented-control"
4242
v-model="minimumNotice"
4343
name="minimum-notice"
4444
:required="false"
@@ -49,3 +49,13 @@ const earliestOptions: SelectOption[] = [0, 0.5, 1, 2, 3, 4, 5].map((d) => {
4949
{{ t('label.minimumNotice') }}
5050
</segmented-control>
5151
</template>
52+
53+
<style>
54+
.minimum-notice-segmented-control ul {
55+
font-size: 0.875rem;
56+
57+
li:first-child button {
58+
text-transform: capitalize;
59+
}
60+
}
61+
</style>

frontend/src/views/AvailabilityView/components/AvailabilitySettings/components/AvailabilitySelect.vue

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,11 @@ const initialAvailabilitySet = Object.fromEntries(
4646
isoWeekdays.map((d) => [d.iso, [defaultAvailability(d.iso)]]
4747
));
4848
const availabilitySet = ref<AvailabilitySet>(initialAvailabilitySet);
49-
const validationErrors = ref<string[][]>(Array.from({length: props.options.length}, () => []));
50-
const validationErrorsExist = computed(() => validationErrors.value.some(e => e.filter(d => d == '').length));
49+
const validationErrors = ref<{[k: string]: string[]}>(Object.fromEntries(isoWeekdays.map((d) => [d.iso, []])));
50+
const validationErrorsExist = computed(
51+
() => Object.values(validationErrors.value).some(e => e.filter(d => d == '').length)
52+
);
53+
5154
const durationHumanized = computed(() => dj.duration(props.slotDuration, "minutes").humanize());
5255
const disabledWeekdays = computed(() => isoWeekdays.map(d => d.iso).filter(d => !model.value.includes(d)));
5356
const validationAlert = { title: t('error.invalidTimeConfiguration', { value: durationHumanized.value }) } as Alert;
@@ -71,7 +74,7 @@ watch(
7174
() => props.availabilities,
7275
() => {
7376
availabilitySet.value = defaultAvailabilitySet();
74-
validationErrors.value = Array.from({length: props.options.length}, () => []);
77+
validationErrors.value = Object.fromEntries(isoWeekdays.map((d) => [d.iso, []]));
7578
},
7679
);
7780

frontend/src/views/AvailabilityView/components/AvailabilitySettings/index.vue

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,42 @@ export default {
130130
<div class="available-days-and-times-container">
131131
<div>
132132
<h3>{{ t('label.availableDaysAndTimes') }}</h3>
133+
134+
<div class="custom-availability-toggle">
135+
<checkbox-input
136+
name="customizePerDay"
137+
:label="t('label.customizePerDay')"
138+
v-model="useCustomAvailabilities"
139+
:disabled="!currentState.active"
140+
/>
141+
</div>
142+
133143
<bubble-select
144+
v-if="!useCustomAvailabilities"
145+
v-model="weekDays"
134146
:options="scheduleDayOptions"
135147
:required="false"
136-
v-model="weekDays"
137148
:disabled="!currentState.active"
138149
/>
150+
151+
<!-- Availability with customization -->
152+
<template v-if="useCustomAvailabilities">
153+
<availability-select
154+
:options="scheduleDayOptions"
155+
:availabilities="currentState.availabilities"
156+
:start-time="startTime"
157+
:end-time="endTime"
158+
:slot-duration="slotDuration"
159+
:required="true"
160+
v-model="weekDays"
161+
@update="onAvailabilitySelectUpdated"
162+
:disabled="!currentState.active"
163+
/>
164+
</template>
139165
</div>
140166

141167
<!-- Availability without customization -->
142-
<div class="availability-times-container">
168+
<div v-if="!useCustomAvailabilities" class="availability-times-container">
143169
<text-input
144170
type="time"
145171
name="start_time"
@@ -163,29 +189,8 @@ export default {
163189
</text-input>
164190
</div>
165191

166-
<checkbox-input
167-
name="customizePerDay"
168-
:label="t('label.customizePerDay')"
169-
v-model="useCustomAvailabilities"
170-
:disabled="!currentState.active"
171-
/>
172192
</div>
173193

174-
<!-- Availability with customization -->
175-
<template v-if="useCustomAvailabilities">
176-
<availability-select
177-
:options="scheduleDayOptions"
178-
:availabilities="currentState.availabilities"
179-
:start-time="startTime"
180-
:end-time="endTime"
181-
:slot-duration="slotDuration"
182-
:required="true"
183-
v-model="weekDays"
184-
@update="onAvailabilitySelectUpdated"
185-
:disabled="!currentState.active"
186-
/>
187-
</template>
188-
189194
<div class="segmented-controls-container">
190195
<!-- Minimum notice -->
191196
<availability-minimum-notice-pill />
@@ -238,6 +243,7 @@ h3 {
238243
display: inline-flex;
239244
gap: 1rem;
240245
margin-block-end: 1.5rem;
246+
width: 100%;
241247
}
242248
243249
.available-days-and-times-container {
@@ -246,6 +252,10 @@ h3 {
246252
gap: 1.5rem;
247253
width: 100%;
248254
max-width: 356px;
255+
256+
.custom-availability-toggle {
257+
margin-bottom: 1rem;
258+
}
249259
}
250260
251261
.user-timezone-container {
@@ -273,7 +283,8 @@ h3 {
273283
274284
.segmented-controls-container {
275285
display: flex;
276-
flex-direction: column;
286+
flex-direction: row;
287+
flex-wrap: wrap;
277288
gap: 1.5rem;
278289
}
279290
}

0 commit comments

Comments
 (0)