diff --git a/apps/portal/pages/admin/scheduling.vue b/apps/portal/pages/admin/scheduling.vue index 55e62a3..01315e6 100644 --- a/apps/portal/pages/admin/scheduling.vue +++ b/apps/portal/pages/admin/scheduling.vue @@ -26,6 +26,8 @@ interface EventType { availabilityScheduleId: string locationType: string ignoreAllDayEvents: boolean + assignment?: 'single' | 'round_robin' + hostPool?: string[] isActive: boolean } interface MinuteInterval { startMinute: number; endMinute: number } @@ -85,6 +87,9 @@ const { data: hosts, refresh: refreshHosts } = await useFetch(() => `${b const selectedHostId = ref(null) const selectedHost = computed(() => hosts.value?.find((h) => h._id === selectedHostId.value) ?? null) +// Round-robin pool candidates: every other host in the tenant (the owning host is +// always implicitly part of the pool, so it is excluded from the selectable list). +const poolCandidates = computed(() => (hosts.value ?? []).filter((h) => h._id !== selectedHostId.value)) const detailTab = ref<'event-types' | 'availability' | 'bookings'>('event-types') const eventTypes = ref([]) @@ -289,6 +294,8 @@ const etForm = reactive({ availabilityScheduleId: '', locationType: 'jitsi', ignoreAllDayEvents: true, + assignment: 'single' as 'single' | 'round_robin', + hostPool: [] as string[], }) const etEditingId = ref(null) const etSlugTouched = ref(false) @@ -303,6 +310,7 @@ function openEt(et?: EventType) { bufferAfterMinutes: et.bufferAfterMinutes, minimumNoticeMinutes: et.minimumNoticeMinutes, maximumDaysInFuture: et.maximumDaysInFuture, availabilityScheduleId: et.availabilityScheduleId, locationType: et.locationType, ignoreAllDayEvents: et.ignoreAllDayEvents ?? true, + assignment: et.assignment ?? 'single', hostPool: [...(et.hostPool ?? [])], }) etSlugTouched.value = true // don't auto-rewrite an existing slug } else { @@ -311,7 +319,7 @@ function openEt(et?: EventType) { title: '', slug: '', durationMinutes: 30, slotIntervalMinutes: 15, bufferBeforeMinutes: 0, bufferAfterMinutes: 0, minimumNoticeMinutes: 60, maximumDaysInFuture: 60, availabilityScheduleId: availability.value[0]?._id ?? '', locationType: 'jitsi', - ignoreAllDayEvents: true, + ignoreAllDayEvents: true, assignment: 'single', hostPool: [], }) etSlugTouched.value = false } @@ -325,13 +333,18 @@ async function submitEt() { if (!selectedHostId.value || !etValid.value) return etBusy.value = true try { + // Only send a host pool for round-robin; single-host keeps the legacy shape. + const payload = { + ...etForm, + hostPool: etForm.assignment === 'round_robin' ? [...etForm.hostPool] : [], + } if (etEditingId.value) { // slug is immutable after creation (public links would break) → omit it. - const { slug: _slug, ...patch } = { ...etForm } + const { slug: _slug, ...patch } = payload await request(`${base.value}/event-types/${etEditingId.value}`, { method: 'PATCH', body: patch }) toast.ok('Event type updated') } else { - await request(`${base.value}/hosts/${selectedHostId.value}/event-types`, { method: 'POST', body: { ...etForm } }) + await request(`${base.value}/hosts/${selectedHostId.value}/event-types`, { method: 'POST', body: payload }) toast.ok('Event type created') } etOpen.value = false @@ -587,7 +600,10 @@ const maskSecret = (s: string) => (s.length > 12 ? `${s.slice(0, 9)}…${s.slice

Create an availability schedule first.

-
{{ et.title }} · {{ et.durationMinutes }} min
+
+ {{ et.title }} · {{ et.durationMinutes }} min + round-robin +
{{ publicUrl(et) }}
@@ -837,6 +853,29 @@ const maskSecret = (s: string) => (s.length > 12 ? `${s.slice(0, 9)}…${s.slice Ignore all-day events when checking availability + +
+ Team pool +

{{ selectedHost?.displayName ?? 'This host' }} is always included. Add other hosts to share bookings with:

+
+ +

No other hosts in this tenant yet.

+
+