- 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>
218 lines
5.0 KiB
Vue
218 lines
5.0 KiB
Vue
<template>
|
||
<div class="booking-row" :class="[`booking-row-${booking.status}`]">
|
||
<div class="row-time">
|
||
{{ formatTimeRange(booking.start_datetime, booking.end_datetime) }}
|
||
</div>
|
||
<div class="row-space">{{ booking.space?.name || 'Space' }}</div>
|
||
<div v-if="showUser && booking.user" class="row-user">
|
||
{{ booking.user.full_name }}
|
||
</div>
|
||
<div class="row-title" :title="booking.title">{{ booking.title }}</div>
|
||
<span :class="['row-badge', `row-badge-${booking.status}`]">
|
||
{{ statusLabel }}
|
||
</span>
|
||
<ActionMenu
|
||
v-if="rowActions.length > 0"
|
||
:actions="rowActions"
|
||
@select="handleAction"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { Check, XCircle, Pencil, Ban } from 'lucide-vue-next'
|
||
import { formatTime as formatTimeUtil } from '@/utils/datetime'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import ActionMenu from '@/components/ActionMenu.vue'
|
||
import type { ActionItem } from '@/components/ActionMenu.vue'
|
||
import type { Booking } from '@/types'
|
||
|
||
const props = defineProps<{
|
||
booking: Booking
|
||
isAdmin: boolean
|
||
showUser?: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
approve: [booking: Booking]
|
||
reject: [booking: Booking]
|
||
cancel: [booking: Booking]
|
||
edit: [booking: Booking]
|
||
}>()
|
||
|
||
const authStore = useAuthStore()
|
||
const userTimezone = computed(() => authStore.user?.timezone || 'UTC')
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
pending: 'Pending',
|
||
approved: 'Approved',
|
||
rejected: 'Rejected',
|
||
canceled: 'Canceled'
|
||
}
|
||
|
||
const statusLabel = computed(() => STATUS_LABELS[props.booking.status] || props.booking.status)
|
||
|
||
const rowActions = computed<ActionItem[]>(() => {
|
||
const actions: ActionItem[] = []
|
||
const status = props.booking.status
|
||
|
||
if (props.isAdmin && status === 'pending') {
|
||
actions.push({ key: 'approve', label: 'Approve', icon: Check, color: 'var(--color-success)' })
|
||
actions.push({ key: 'reject', label: 'Reject', icon: XCircle, color: 'var(--color-danger)' })
|
||
}
|
||
|
||
if (status === 'pending' || status === 'approved') {
|
||
actions.push({ key: 'edit', label: 'Edit', icon: Pencil })
|
||
}
|
||
|
||
if (status === 'pending' || status === 'approved') {
|
||
actions.push({ key: 'cancel', label: 'Cancel', icon: Ban, color: 'var(--color-danger)' })
|
||
}
|
||
|
||
return actions
|
||
})
|
||
|
||
const handleAction = (key: string) => {
|
||
switch (key) {
|
||
case 'approve': emit('approve', props.booking); break
|
||
case 'reject': emit('reject', props.booking); break
|
||
case 'edit': emit('edit', props.booking); break
|
||
case 'cancel': emit('cancel', props.booking); break
|
||
}
|
||
}
|
||
|
||
const formatTimeRange = (start: string, end: string): string => {
|
||
return `${formatTimeUtil(start, userTimezone.value)}–${formatTimeUtil(end, userTimezone.value)}`
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.booking-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 14px;
|
||
min-height: 44px;
|
||
background: var(--color-bg-secondary);
|
||
border-radius: var(--radius-sm);
|
||
border-left: 3px solid transparent;
|
||
transition: background var(--transition-fast);
|
||
}
|
||
|
||
.booking-row:hover {
|
||
background: var(--color-bg-tertiary);
|
||
}
|
||
|
||
.booking-row-pending {
|
||
border-left-color: var(--color-warning);
|
||
}
|
||
|
||
.booking-row-approved {
|
||
border-left-color: var(--color-success);
|
||
}
|
||
|
||
.booking-row-rejected {
|
||
border-left-color: var(--color-danger);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.booking-row-canceled {
|
||
border-left-color: var(--color-text-muted);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.row-time {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-text-primary);
|
||
white-space: nowrap;
|
||
min-width: 100px;
|
||
}
|
||
|
||
.row-space {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--color-accent);
|
||
white-space: nowrap;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.row-user {
|
||
font-size: 13px;
|
||
color: var(--color-text-secondary);
|
||
white-space: nowrap;
|
||
min-width: 80px;
|
||
max-width: 120px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.row-title {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
color: var(--color-text-primary);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.row-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 10px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
border-radius: 10px;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.row-badge-pending {
|
||
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
||
color: var(--color-warning);
|
||
}
|
||
|
||
.row-badge-approved {
|
||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||
color: var(--color-success);
|
||
}
|
||
|
||
.row-badge-rejected {
|
||
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
||
color: var(--color-danger);
|
||
}
|
||
|
||
.row-badge-canceled {
|
||
background: color-mix(in srgb, var(--color-text-muted) 15%, transparent);
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.booking-row {
|
||
flex-wrap: wrap;
|
||
gap: 6px 10px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
.row-time {
|
||
min-width: auto;
|
||
}
|
||
|
||
.row-space {
|
||
min-width: auto;
|
||
}
|
||
|
||
.row-user {
|
||
min-width: auto;
|
||
max-width: none;
|
||
}
|
||
|
||
.row-title {
|
||
flex-basis: 100%;
|
||
order: 10;
|
||
}
|
||
|
||
}
|
||
</style>
|