Files
space-booking/frontend/src/components/ActiveBookings.vue
Claude Agent d245c72757 feat: complete UI/UX overhaul - dashboard unification, calendar UX, mobile optimization
- Dashboard redesign as command center with filters, quick actions, inline approve/reject
- Reusable components: BookingRow, BookingFilters, ActionMenu, BookingPreviewModal, BookingEditModal
- Calendar: drag & drop reschedule, eventClick preview modal, grid/list toggle
- Mobile: segmented control bookings/calendar toggle, compact pills, responsive layout
- Collapsible filters with active count badge
- Smart menu positioning with Teleport
- Calendar/list bidirectional data sync
- Navigation: unified History page, removed AdminPending
- Google Calendar OAuth integration
- Dark mode contrast improvements, breadcrumb navigation
- useLocalStorage composable for state persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:34:47 +00:00

259 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-if="activeBookings.length > 0" class="active-bookings">
<div class="active-header">
<Zap :size="20" class="active-icon" />
<h3>Active Now</h3>
</div>
<div class="active-list">
<div
v-for="booking in activeBookings"
:key="booking.id"
class="active-card"
>
<div class="active-card-top">
<div class="active-info">
<h4>{{ booking.title }}</h4>
<p class="active-space">{{ booking.space?.name || 'Space' }}</p>
<p class="active-time">
{{ formatTime(booking.start_datetime) }} {{ formatTime(booking.end_datetime) }}
</p>
</div>
<span class="active-remaining">{{ getRemainingTime(booking.end_datetime) }}</span>
</div>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: getProgress(booking.start_datetime, booking.end_datetime) + '%' }"
/>
</div>
<div class="active-actions">
<router-link :to="`/history`" class="action-btn action-btn-view" title="View bookings">
<Eye :size="16" />
</router-link>
<button
v-if="booking.status === 'pending'"
class="action-btn action-btn-edit"
title="Edit booking"
@click="$emit('refresh')"
>
<Pencil :size="16" />
</button>
<button
class="action-btn action-btn-cancel"
title="Cancel booking"
@click="$emit('cancel', booking)"
>
<X :size="16" />
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { Zap, Eye, Pencil, X } from 'lucide-vue-next'
import { isBookingActive, getBookingProgress, formatRemainingTime } from '@/utils/datetime'
import { formatTime as formatTimeUtil } from '@/utils/datetime'
import { useAuthStore } from '@/stores/auth'
import type { Booking } from '@/types'
const props = defineProps<{
bookings: Booking[]
}>()
defineEmits<{
cancel: [booking: Booking]
refresh: []
}>()
const authStore = useAuthStore()
const userTimezone = computed(() => authStore.user?.timezone || 'UTC')
const now = ref(new Date())
const activeBookings = computed(() => {
void now.value
return props.bookings.filter(
(b) => b.status === 'approved' && isBookingActive(b.start_datetime, b.end_datetime)
)
})
const formatTime = (datetime: string): string => {
return formatTimeUtil(datetime, userTimezone.value)
}
const getProgress = (start: string, end: string): number => {
void now.value
return getBookingProgress(start, end)
}
const getRemainingTime = (end: string): string => {
void now.value
return formatRemainingTime(end)
}
let refreshInterval: ReturnType<typeof setInterval> | null = null
onMounted(() => {
refreshInterval = setInterval(() => {
now.value = new Date()
}, 60000)
})
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval)
refreshInterval = null
}
})
</script>
<style scoped>
.active-bookings {
background: var(--color-surface);
border: 1px solid var(--color-accent);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
.active-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 1.25rem;
background: var(--color-accent-light);
border-bottom: 1px solid var(--color-border-light);
}
.active-icon {
color: var(--color-accent);
}
.active-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
}
.active-list {
display: flex;
flex-direction: column;
gap: 1px;
background: var(--color-border-light);
}
.active-card {
padding: 1rem 1.25rem;
background: var(--color-surface);
}
.active-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.active-info h4 {
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 4px;
}
.active-space {
font-size: 13px;
color: var(--color-text-secondary);
margin: 0 0 2px;
}
.active-time {
font-size: 12px;
color: var(--color-text-muted);
margin: 0;
}
.active-remaining {
font-size: 13px;
font-weight: 600;
color: var(--color-accent);
white-space: nowrap;
flex-shrink: 0;
}
.progress-bar {
height: 6px;
background: var(--color-bg-tertiary);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-hover));
border-radius: 3px;
transition: width 1s ease;
}
.active-actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.action-btn-view:hover {
color: var(--color-info);
border-color: var(--color-info);
background: color-mix(in srgb, var(--color-info) 10%, transparent);
}
.action-btn-edit:hover {
color: var(--color-warning);
border-color: var(--color-warning);
background: color-mix(in srgb, var(--color-warning) 10%, transparent);
}
.action-btn-cancel:hover {
color: var(--color-danger);
border-color: var(--color-danger);
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
}
/* Mobile responsive */
@media (max-width: 768px) {
.active-card-top {
flex-direction: column;
gap: 0.5rem;
}
.active-remaining {
align-self: flex-start;
}
}
</style>