339 lines
8.0 KiB
Vue
339 lines
8.0 KiB
Vue
<template>
|
|
<Transition name="modal-fade">
|
|
<div v-if="show && booking" class="edit-overlay" @click.self="$emit('close')">
|
|
<div class="edit-modal">
|
|
<h3>Edit Booking</h3>
|
|
<form @submit.prevent="saveEdit">
|
|
<div class="form-group">
|
|
<label for="edit-space">Space</label>
|
|
<input
|
|
id="edit-space"
|
|
type="text"
|
|
:value="booking.space?.name || 'Unknown'"
|
|
readonly
|
|
disabled
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="edit-title">Title *</label>
|
|
<input
|
|
id="edit-title"
|
|
v-model="editForm.title"
|
|
type="text"
|
|
required
|
|
maxlength="200"
|
|
placeholder="Booking title"
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="edit-description">Description (optional)</label>
|
|
<textarea
|
|
id="edit-description"
|
|
v-model="editForm.description"
|
|
rows="3"
|
|
placeholder="Additional details..."
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Start *</label>
|
|
<div class="datetime-row">
|
|
<div class="datetime-field">
|
|
<label for="edit-start-date" class="sublabel">Date</label>
|
|
<input id="edit-start-date" v-model="editForm.start_date" type="date" required />
|
|
</div>
|
|
<div class="datetime-field">
|
|
<label for="edit-start-time" class="sublabel">Time</label>
|
|
<input id="edit-start-time" v-model="editForm.start_time" type="time" required />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>End *</label>
|
|
<div class="datetime-row">
|
|
<div class="datetime-field">
|
|
<label for="edit-end-date" class="sublabel">Date</label>
|
|
<input id="edit-end-date" v-model="editForm.end_date" type="date" required />
|
|
</div>
|
|
<div class="datetime-field">
|
|
<label for="edit-end-time" class="sublabel">Time</label>
|
|
<input id="edit-end-time" v-model="editForm.end_time" type="time" required />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="editError" class="error-msg">{{ editError }}</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" @click="$emit('close')">Cancel</button>
|
|
<button type="submit" class="btn btn-primary" :disabled="saving">
|
|
{{ saving ? 'Saving...' : 'Save Changes' }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, watch, computed } from 'vue'
|
|
import { bookingsApi, adminBookingsApi, handleApiError } from '@/services/api'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { isoToLocalDateTime, localDateTimeToISO } from '@/utils/datetime'
|
|
import type { Booking } from '@/types'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
booking: Booking | null
|
|
show: boolean
|
|
isAdmin?: boolean
|
|
}>(), {
|
|
isAdmin: false
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
close: []
|
|
saved: []
|
|
}>()
|
|
|
|
const authStore = useAuthStore()
|
|
const userTimezone = computed(() => authStore.user?.timezone || 'UTC')
|
|
|
|
const editForm = ref({
|
|
title: '',
|
|
description: '',
|
|
start_date: '',
|
|
start_time: '',
|
|
end_date: '',
|
|
end_time: ''
|
|
})
|
|
const editError = ref('')
|
|
const saving = ref(false)
|
|
|
|
// Populate form when booking changes or modal opens
|
|
watch(() => [props.booking, props.show], () => {
|
|
if (props.show && props.booking) {
|
|
const startLocal = isoToLocalDateTime(props.booking.start_datetime, userTimezone.value)
|
|
const endLocal = isoToLocalDateTime(props.booking.end_datetime, userTimezone.value)
|
|
|
|
const [startDate, startTime] = startLocal.split('T')
|
|
const [endDate, endTime] = endLocal.split('T')
|
|
|
|
editForm.value = {
|
|
title: props.booking.title,
|
|
description: props.booking.description || '',
|
|
start_date: startDate,
|
|
start_time: startTime,
|
|
end_date: endDate,
|
|
end_time: endTime
|
|
}
|
|
editError.value = ''
|
|
}
|
|
}, { immediate: true })
|
|
|
|
const saveEdit = async () => {
|
|
if (!props.booking) return
|
|
|
|
saving.value = true
|
|
editError.value = ''
|
|
|
|
try {
|
|
const startDateTime = `${editForm.value.start_date}T${editForm.value.start_time}`
|
|
const endDateTime = `${editForm.value.end_date}T${editForm.value.end_time}`
|
|
|
|
const updateData = {
|
|
title: editForm.value.title,
|
|
description: editForm.value.description,
|
|
start_datetime: localDateTimeToISO(startDateTime),
|
|
end_datetime: localDateTimeToISO(endDateTime)
|
|
}
|
|
|
|
if (props.isAdmin) {
|
|
await adminBookingsApi.update(props.booking.id, updateData)
|
|
} else {
|
|
await bookingsApi.update(props.booking.id, updateData)
|
|
}
|
|
emit('saved')
|
|
} catch (err) {
|
|
editError.value = handleApiError(err)
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.edit-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.45);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.edit-modal {
|
|
background: var(--color-surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: 24px;
|
|
max-width: 500px;
|
|
width: 90%;
|
|
max-height: 90vh;
|
|
overflow-y: auto;
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
.edit-modal h3 {
|
|
margin: 0 0 20px;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.form-group > label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.sublabel {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
font-weight: 400;
|
|
font-size: 12px;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.datetime-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 12px;
|
|
}
|
|
|
|
.datetime-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 14px;
|
|
font-family: inherit;
|
|
background: var(--color-surface);
|
|
color: var(--color-text-primary);
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group textarea:focus {
|
|
outline: none;
|
|
border-color: var(--color-accent);
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent);
|
|
}
|
|
|
|
.form-group input:disabled {
|
|
background: var(--color-bg-tertiary);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.form-group textarea {
|
|
resize: vertical;
|
|
}
|
|
|
|
.error-msg {
|
|
padding: 12px;
|
|
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
|
color: var(--color-danger);
|
|
border-radius: var(--radius-sm);
|
|
margin-bottom: 16px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
font-family: inherit;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--color-accent);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: var(--color-accent-hover);
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--color-bg-tertiary);
|
|
color: var(--color-text-primary);
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: var(--color-border);
|
|
}
|
|
|
|
/* Modal transition */
|
|
.modal-fade-enter-active,
|
|
.modal-fade-leave-active {
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.modal-fade-enter-active .edit-modal,
|
|
.modal-fade-leave-active .edit-modal {
|
|
transition: transform 0.2s ease, opacity 0.2s ease;
|
|
}
|
|
|
|
.modal-fade-enter-from,
|
|
.modal-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
.modal-fade-enter-from .edit-modal,
|
|
.modal-fade-leave-to .edit-modal {
|
|
transform: scale(0.95);
|
|
opacity: 0;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.edit-modal {
|
|
max-width: none;
|
|
width: calc(100% - 32px);
|
|
margin: 16px;
|
|
}
|
|
}
|
|
</style>
|