+
-
+
+
+
+
{{ confirmTitle }}
+
{{ confirmMessage }}
+
+ Cancel
+
+ {{ confirmLoading ? 'Processing...' : confirmLabel }}
+
+
+
+
+
+
+
+
+
Reject Booking
+
Rejecting "{{ rejectBooking?.title }}"
+
+ Reason (optional)
+
+
+
+ Cancel
+
+ {{ processing !== null ? 'Rejecting...' : 'Reject' }}
+
+
+
+
+
{{ toastMsg }}
@@ -266,7 +303,7 @@ import type { Booking, Space } from '@/types'
const route = useRoute()
const authStore = useAuthStore()
-const isAdmin = computed(() => authStore.user?.role === 'admin')
+const isAdmin = computed(() => ['admin', 'superadmin', 'manager'].includes(authStore.user?.role || ''))
const userTimezone = computed(() => authStore.user?.timezone || 'UTC')
const breadcrumbItems = computed(() => [
@@ -325,6 +362,62 @@ const showToast = (msg: string, type: 'success' | 'error') => {
setTimeout(() => { toastMsg.value = '' }, type === 'success' ? 3000 : 5000)
}
+// Confirm modal (for cancel / approve)
+const showConfirmModal = ref(false)
+const confirmTitle = ref('')
+const confirmMessage = ref('')
+const confirmDanger = ref(false)
+const confirmLabel = ref('Yes')
+const confirmLoading = ref(false)
+const onConfirm = ref<(() => Promise
) | null>(null)
+
+const openConfirm = (opts: { title: string; message: string; danger?: boolean; label?: string; action: () => Promise }) => {
+ confirmTitle.value = opts.title
+ confirmMessage.value = opts.message
+ confirmDanger.value = opts.danger ?? false
+ confirmLabel.value = opts.label ?? 'Yes'
+ onConfirm.value = opts.action
+ confirmLoading.value = false
+ showConfirmModal.value = true
+}
+
+const executeConfirm = async () => {
+ if (!onConfirm.value) return
+ confirmLoading.value = true
+ try {
+ await onConfirm.value()
+ } finally {
+ confirmLoading.value = false
+ showConfirmModal.value = false
+ }
+}
+
+// Reject modal
+const showRejectModal = ref(false)
+const rejectBooking = ref(null)
+const rejectReason = ref('')
+
+const openRejectModal = (booking: Booking) => {
+ rejectBooking.value = booking
+ rejectReason.value = ''
+ showRejectModal.value = true
+}
+
+const doReject = async () => {
+ if (!rejectBooking.value) return
+ processing.value = rejectBooking.value.id
+ try {
+ await adminBookingsApi.reject(rejectBooking.value.id, rejectReason.value || undefined)
+ showToast(`Booking "${rejectBooking.value.title}" rejected.`, 'success')
+ showRejectModal.value = false
+ await loadBookings()
+ } catch (err) {
+ showToast(handleApiError(err), 'error')
+ } finally {
+ processing.value = null
+ }
+}
+
const hasActiveFilters = computed(() =>
filters.value.space_id !== null ||
filters.value.status !== null ||
@@ -413,50 +506,53 @@ const clearAllFilters = () => {
}
// Actions
-const handleCancel = async (booking: Booking) => {
- if (!confirm(`Cancel booking "${booking.title}"?`)) return
-
- processing.value = booking.id
- try {
- await bookingsApi.update(booking.id, { status: 'canceled' } as any)
- showToast(`Booking "${booking.title}" canceled.`, 'success')
- await loadBookings()
- } catch (err) {
- showToast(handleApiError(err), 'error')
- } finally {
- processing.value = null
- }
+const handleCancel = (booking: Booking) => {
+ openConfirm({
+ title: 'Cancel Booking',
+ message: `Cancel booking "${booking.title}"?`,
+ danger: true,
+ label: 'Cancel Booking',
+ action: async () => {
+ processing.value = booking.id
+ try {
+ if (isAdmin.value) {
+ await adminBookingsApi.cancel(booking.id)
+ } else {
+ await bookingsApi.cancel(booking.id)
+ }
+ showToast(`Booking "${booking.title}" canceled.`, 'success')
+ await loadBookings()
+ } catch (err) {
+ showToast(handleApiError(err), 'error')
+ } finally {
+ processing.value = null
+ }
+ }
+ })
}
-const handleApprove = async (booking: Booking) => {
- if (!confirm(`Approve booking "${booking.title}"?`)) return
-
- processing.value = booking.id
- try {
- await adminBookingsApi.approve(booking.id)
- showToast(`Booking "${booking.title}" approved!`, 'success')
- await loadBookings()
- } catch (err) {
- showToast(handleApiError(err), 'error')
- } finally {
- processing.value = null
- }
+const handleApprove = (booking: Booking) => {
+ openConfirm({
+ title: 'Approve Booking',
+ message: `Approve booking "${booking.title}"?`,
+ label: 'Approve',
+ action: async () => {
+ processing.value = booking.id
+ try {
+ await adminBookingsApi.approve(booking.id)
+ showToast(`Booking "${booking.title}" approved!`, 'success')
+ await loadBookings()
+ } catch (err) {
+ showToast(handleApiError(err), 'error')
+ } finally {
+ processing.value = null
+ }
+ }
+ })
}
-const handleReject = async (booking: Booking) => {
- const reason = prompt('Rejection reason (optional):')
- if (reason === null) return // User clicked cancel
-
- processing.value = booking.id
- try {
- await adminBookingsApi.reject(booking.id, reason || undefined)
- showToast(`Booking "${booking.title}" rejected.`, 'success')
- await loadBookings()
- } catch (err) {
- showToast(handleApiError(err), 'error')
- } finally {
- processing.value = null
- }
+const handleReject = (booking: Booking) => {
+ openRejectModal(booking)
}
// Edit modal
@@ -1009,6 +1105,21 @@ onMounted(() => {
background: var(--color-border);
}
+.btn-danger {
+ background: var(--color-danger);
+ color: white;
+}
+
+.btn-danger:hover:not(:disabled) {
+ background: color-mix(in srgb, var(--color-danger) 85%, black);
+}
+
+.confirm-message {
+ color: var(--color-text-secondary);
+ margin-bottom: 20px;
+ line-height: 1.5;
+}
+
/* Toast */
.toast {
position: fixed;
diff --git a/frontend/src/views/Organization.vue b/frontend/src/views/Organization.vue
new file mode 100644
index 0000000..a74267e
--- /dev/null
+++ b/frontend/src/views/Organization.vue
@@ -0,0 +1,469 @@
+
+
+
+
+
+
+
+
Loading organizations...
+
+
+
+
+
No organizations found
+
+
+
+
+
+
+
+
+
Loading members...
+
+
+
No members yet
+
+
+
+ Name
+ Email
+ Role
+ Actions
+
+
+
+
+ {{ member.user_name }}
+ {{ member.user_email }}
+
+
+ {{ member.role }}
+
+
+
+
+ Remove
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Create Organization
+
+
+
+
+
{{ toast }}
+
+
+
+
+
+
diff --git a/frontend/src/views/Properties.vue b/frontend/src/views/Properties.vue
new file mode 100644
index 0000000..f592ec5
--- /dev/null
+++ b/frontend/src/views/Properties.vue
@@ -0,0 +1,630 @@
+
+
+
+
+
+
+
+
+
Loading properties...
+
+
+
+
+
+
No properties yet
+
Create your first property
+
+
+
+
+
+
+
{{ prop.description }}
+
{{ prop.address }}
+
+
Managed by:
+
+
+ {{ mgr.full_name.charAt(0).toUpperCase() }}
+ {{ mgr.full_name }}
+
+
+
+
+
+
+
+
+
+
+
Create New Property
+
+
+ Name *
+
+
+
+ Description
+
+
+
+ Address
+
+
+
+
+
+ Public (allows anonymous bookings)
+
+
+
+ {{ error }}
+
+
+
+ {{ submitting ? 'Creating...' : 'Create' }}
+
+ Cancel
+
+
+
+
+
+
+
+
+
{{ confirmTitle }}
+
{{ confirmMessage }}
+
{{ error }}
+
+
+ {{ submitting ? 'Processing...' : confirmAction }}
+
+ Cancel
+
+
+
+
+
+
{{ successMsg }}
+
+
+
+
+
+
diff --git a/frontend/src/views/PropertyDetail.vue b/frontend/src/views/PropertyDetail.vue
new file mode 100644
index 0000000..ae15c4d
--- /dev/null
+++ b/frontend/src/views/PropertyDetail.vue
@@ -0,0 +1,1096 @@
+
+
+
+
+
+
+
Loading property...
+
+
+
+
Error
+
{{ error }}
+
Back to Properties
+
+
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
No spaces in this property yet. Click "Add Space" to create one.
+
+
+
+
+ {{ sp.type === 'sala' ? 'Sala' : 'Birou' }}
+ Capacity: {{ sp.capacity }}
+
+
{{ sp.description }}
+
+
+
+
+
+
+
Bookings for this Property
+
No bookings found.
+
+
+
+
{{ bk.title }}
+
+ {{ bk.space?.name }}
+ {{ formatDate(bk.start_datetime) }} - {{ formatTime(bk.end_datetime) }}
+
+
Guest: {{ bk.guest_name }}
+
{{ bk.user.full_name }}
+
+
{{ bk.status }}
+
+
+
+
+
+
+
Access Control
+
+
This property is public . All users (including anonymous guests) can view spaces and create bookings.
+
This property is private . Only users with explicit access below can view spaces and create bookings.
+
Users with access can: browse available spaces, check availability, and submit booking requests. They still need approval if "Require approval" is enabled in Settings.
+
+
+
+
+
+
No access grants yet.
+
+
+
+ {{ acc.user_name }}
+ {{ acc.organization_name }} (Org)
+ {{ acc.user_email }}
+
+
Revoke
+
+
+
+
+
+
+
Property Settings
+
Override global defaults for this property. Leave blank to use global settings.
+
+
+
+
+
+
+ Require approval for bookings
+
+
+
+
+ {{ savingSettings ? 'Saving...' : 'Save Settings' }}
+
+
+
+
+
+
+
+
+
+
Edit Property
+
+
+ Name *
+
+
+
+ Description
+
+
+
+ Address
+
+
+
+
+
+ Public
+
+
+ {{ editError }}
+
+ Save
+ Cancel
+
+
+
+
+
+
+
+
+
Add Space to {{ property?.name }}
+
+
+ Name *
+
+
+
+ Type *
+
+ Sala
+ Birou
+
+
+
+ Capacity *
+
+
+
+ Description
+
+
+ {{ spaceFormError }}
+
+
+ {{ creatingSpace ? 'Creating...' : 'Create Space' }}
+
+ Cancel
+
+
+
+
+
+
{{ toast }}
+
+
+
+
+
+
diff --git a/frontend/src/views/PublicBooking.vue b/frontend/src/views/PublicBooking.vue
new file mode 100644
index 0000000..74b6ed3
--- /dev/null
+++ b/frontend/src/views/PublicBooking.vue
@@ -0,0 +1,493 @@
+
+
+
+
Book a Space
+
Reserve a meeting room or workspace without an account
+
+
+
+
Loading properties...
+
No public properties available.
+
+
+
{{ prop.name }}
+
{{ prop.description }}
+
{{ prop.address }}
+
{{ prop.space_count || 0 }} spaces
+
+
+
+
+
+
+
Back to properties
+
{{ selectedProperty?.name }} - Choose a Space
+
Loading spaces...
+
No spaces available.
+
+
+
{{ sp.name }}
+
+ {{ formatType(sp.type) }}
+ Capacity: {{ sp.capacity }}
+
+
+
+
+
+
+
+
+
+
+
✓
+
Booking Request Sent!
+
Your booking request has been submitted. You will receive updates at {{ form.guest_email }} .
+
Book Another
+
+
+
+ Already have an account? Sign in
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/SpaceDetail.vue b/frontend/src/views/SpaceDetail.vue
index 8471396..6bfb8cb 100644
--- a/frontend/src/views/SpaceDetail.vue
+++ b/frontend/src/views/SpaceDetail.vue
@@ -66,7 +66,82 @@
Availability Calendar
View existing bookings and available time slots
-
+
+
+
+
+
+
+
+
Loading bookings...
+
No bookings found for this space.
+
+
+
+
+ User
+ Date
+ Time
+ Title
+ Status
+ Actions
+
+
+
+
+ {{ b.user?.full_name || b.guest_name || 'Unknown' }}
+ {{ formatBookingDate(b.start_datetime) }}
+ {{ formatBookingTime(b.start_datetime) }} - {{ formatBookingTime(b.end_datetime) }}
+ {{ b.title }}
+ {{ b.status }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+