feat(scheduling): date-overrides UI for availability

This commit is contained in:
Ronni Baslund
2026-06-07 08:55:52 +02:00
parent f41475ac3b
commit 851018f481
+53 -2
View File
@@ -30,7 +30,8 @@ interface EventType {
}
interface MinuteInterval { startMinute: number; endMinute: number }
interface WeeklyRule { dayOfWeek: number; intervals: MinuteInterval[] }
interface Availability { _id: string; name: string; timezone: string; weeklyRules: WeeklyRule[] }
interface DateOverride { date: string; isUnavailable: boolean; intervals: MinuteInterval[] }
interface Availability { _id: string; name: string; timezone: string; weeklyRules: WeeklyRule[]; dateOverrides?: DateOverride[] }
interface Booking {
_id: string
status: string
@@ -169,10 +170,14 @@ async function submitHost() {
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
const availOpen = ref(false)
const availBusy = ref(false)
// Date-override editor rows mirror the backend shape but hold times as HH:MM
// for the <input type="time"> controls; we convert to minutes on submit.
interface OverrideRow { date: string; isUnavailable: boolean; start: string; end: string }
const availForm = reactive({
name: 'Working hours',
timezone: 'Europe/Copenhagen',
days: DAYS.map((_, i) => ({ enabled: i >= 1 && i <= 5, start: '09:00', end: '17:00' })),
overrides: [] as OverrideRow[],
})
const availEditingId = ref<string | null>(null)
function openAvail(a?: Availability) {
@@ -186,14 +191,30 @@ function openAvail(a?: Availability) {
? { enabled: true, start: minToTime(intv.startMinute), end: minToTime(intv.endMinute) }
: { enabled: false, start: '09:00', end: '17:00' }
})
availForm.overrides = (a.dateOverrides ?? []).map((o) => {
const intv = o.intervals?.[0]
return {
date: o.date,
isUnavailable: o.isUnavailable,
start: intv ? minToTime(intv.startMinute) : '09:00',
end: intv ? minToTime(intv.endMinute) : '17:00',
}
})
} else {
availEditingId.value = null
availForm.name = 'Working hours'
availForm.timezone = selectedHost.value?.timezone ?? 'Europe/Copenhagen'
availForm.days = DAYS.map((_, i) => ({ enabled: i >= 1 && i <= 5, start: '09:00', end: '17:00' }))
availForm.overrides = []
}
availOpen.value = true
}
function addOverride() {
availForm.overrides.push({ date: '', isUnavailable: false, start: '09:00', end: '17:00' })
}
function removeOverride(idx: number) {
availForm.overrides.splice(idx, 1)
}
const timeToMin = (t: string) => { const [h, m] = t.split(':').map(Number); return h * 60 + m }
async function submitAvail() {
if (!selectedHostId.value) return
@@ -203,7 +224,15 @@ async function submitAvail() {
.map((d, i) => ({ dayOfWeek: i, enabled: d.enabled, start: d.start, end: d.end }))
.filter((d) => d.enabled && timeToMin(d.end) > timeToMin(d.start))
.map((d) => ({ dayOfWeek: d.dayOfWeek, intervals: [{ startMinute: timeToMin(d.start), endMinute: timeToMin(d.end) }] }))
const body = { name: availForm.name, timezone: availForm.timezone, weeklyRules }
const dateOverrides: DateOverride[] = availForm.overrides
.filter((o) => /^\d{4}-\d{2}-\d{2}$/.test(o.date))
.filter((o) => o.isUnavailable || timeToMin(o.end) > timeToMin(o.start))
.map((o) => ({
date: o.date,
isUnavailable: o.isUnavailable,
intervals: o.isUnavailable ? [] : [{ startMinute: timeToMin(o.start), endMinute: timeToMin(o.end) }],
}))
const body = { name: availForm.name, timezone: availForm.timezone, weeklyRules, dateOverrides }
if (availEditingId.value) {
await request(`${base.value}/availability/${availEditingId.value}`, { method: 'PATCH', body })
toast.ok('Availability updated')
@@ -568,6 +597,21 @@ const detailTabs = computed(() => [
<input class="input time" type="time" v-model="d.end" :disabled="!d.enabled" />
</div>
</div>
<div class="overrides">
<div class="overrides-head">
<Eyebrow>Date overrides</Eyebrow>
<UiButton variant="ghost" size="sm" @click="addOverride">Add override</UiButton>
</div>
<p class="mute small" v-if="!availForm.overrides.length">Block specific dates or set custom hours that replace the weekly rules.</p>
<div v-for="(o, i) in availForm.overrides" :key="i" class="overrow">
<input class="input date" type="date" v-model="o.date" />
<label class="daytoggle"><input type="checkbox" v-model="o.isUnavailable" /> Unavailable</label>
<input class="input time" type="time" v-model="o.start" :disabled="o.isUnavailable" />
<span class="dash"></span>
<input class="input time" type="time" v-model="o.end" :disabled="o.isUnavailable" />
<button type="button" class="removeov" aria-label="Remove override" @click="removeOverride(i)"><UiIcon name="x" /></button>
</div>
</div>
</div>
<template #footer>
<UiButton variant="ghost" @click="availOpen = false">Cancel</UiButton>
@@ -719,4 +763,11 @@ const detailTabs = computed(() => [
.daytoggle { display: flex; align-items: center; gap: 6px; font-size: 13px; }
.time { width: 100%; }
.dash { text-align: center; color: var(--text-mute); }
.overrides { display: flex; flex-direction: column; gap: 8px; border-top: 1px solid var(--border); padding-top: 12px; }
.overrides-head { display: flex; align-items: center; justify-content: space-between; }
.small { font-size: 12px; }
.overrow { display: grid; grid-template-columns: 150px auto 1fr auto 1fr auto; align-items: center; gap: 8px; }
.date { width: 100%; }
.removeov { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); color: var(--text-mute); cursor: pointer; }
.removeov:hover { background: rgba(226, 48, 48, 0.08); color: var(--bad); }
</style>