feat(scheduling): date-overrides UI for availability
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user