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>
This commit is contained in:
Claude Agent
2026-02-12 15:34:47 +00:00
parent a4d3f862d2
commit d245c72757
36 changed files with 5275 additions and 1569 deletions

View File

@@ -1,13 +1,7 @@
<template>
<div class="space-detail">
<!-- Breadcrumbs -->
<nav class="breadcrumbs">
<router-link to="/">Home</router-link>
<span class="separator">/</span>
<router-link to="/spaces">Spaces</router-link>
<span class="separator">/</span>
<span class="current">{{ space?.name || 'Loading...' }}</span>
</nav>
<Breadcrumb :items="breadcrumbItems" />
<!-- Loading State -->
<div v-if="loading" class="loading">
@@ -41,14 +35,25 @@
</span>
</div>
</div>
<button
class="btn btn-primary btn-reserve"
:disabled="!space.is_active"
@click="handleReserve"
>
<Plus :size="18" />
{{ showBookingForm ? 'Cancel Reservation' : 'Reserve Space' }}
</button>
<div class="header-actions">
<button
class="btn btn-primary btn-reserve"
:disabled="!space.is_active"
@click="handleReserve"
>
<Plus :size="18" />
{{ showBookingForm ? 'Cancel' : 'Reserve Space' }}
</button>
<button
v-if="isAdmin"
class="btn btn-secondary btn-reserve"
:disabled="!space.is_active"
@click="showAdminBookingForm = true"
>
<UserPlus :size="18" />
Book for User
</button>
</div>
</div>
<!-- Description -->
@@ -76,24 +81,48 @@
/>
</div>
</div>
<!-- Admin Booking Modal -->
<div v-if="showAdminBookingForm && space" class="modal" @click.self="showAdminBookingForm = false">
<div class="modal-content">
<h3>Admin: Book for User</h3>
<AdminBookingForm
:space-id="space.id"
@submit="handleAdminBookingSubmit"
@cancel="showAdminBookingForm = false"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { spacesApi, handleApiError } from '@/services/api'
import Breadcrumb from '@/components/Breadcrumb.vue'
import SpaceCalendar from '@/components/SpaceCalendar.vue'
import BookingForm from '@/components/BookingForm.vue'
import { Users, Plus } from 'lucide-vue-next'
import AdminBookingForm from '@/components/AdminBookingForm.vue'
import { useAuthStore } from '@/stores/auth'
import { Users, Plus, UserPlus } from 'lucide-vue-next'
import type { Space } from '@/types'
const route = useRoute()
const authStore = useAuthStore()
const isAdmin = computed(() => authStore.user?.role === 'admin')
const breadcrumbItems = computed(() => [
{ label: 'Dashboard', to: '/dashboard' },
{ label: 'Spaces', to: '/spaces' },
{ label: space.value?.name || 'Loading...' }
])
const space = ref<Space | null>(null)
const loading = ref(true)
const error = ref('')
const showBookingForm = ref(false)
const showAdminBookingForm = ref(false)
const calendarRef = ref<InstanceType<typeof SpaceCalendar> | null>(null)
// Format space type for display
@@ -150,42 +179,18 @@ const handleBookingSubmit = () => {
calendarRef.value?.refresh()
}
// Handle admin booking form submit
const handleAdminBookingSubmit = () => {
showAdminBookingForm.value = false
calendarRef.value?.refresh()
}
onMounted(() => {
loadSpace()
})
</script>
<style scoped>
/* Breadcrumbs */
.breadcrumbs {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
font-size: 14px;
color: var(--color-text-secondary);
}
.breadcrumbs a {
color: var(--color-accent);
text-decoration: none;
transition: color var(--transition-fast);
}
.breadcrumbs a:hover {
color: var(--color-accent-hover);
text-decoration: underline;
}
.breadcrumbs .separator {
color: var(--color-text-muted);
}
.breadcrumbs .current {
color: var(--color-text-primary);
font-weight: 500;
}
/* Loading State */
.loading {
display: flex;
@@ -335,8 +340,14 @@ onMounted(() => {
box-shadow: var(--shadow-md);
}
.header-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.btn-reserve {
min-width: 180px;
min-width: 160px;
justify-content: center;
}
@@ -413,31 +424,6 @@ onMounted(() => {
font-size: 24px;
}
:deep(.fc .fc-toolbar) {
flex-direction: column;
gap: 8px;
align-items: stretch !important;
}
:deep(.fc .fc-toolbar-chunk) {
width: 100%;
display: flex;
justify-content: center;
}
:deep(.fc .fc-toolbar-title) {
font-size: 1.2em;
margin: 0;
}
:deep(.fc .fc-button) {
padding: 6px 10px;
font-size: 0.85em;
}
:deep(.fc .fc-col-header-cell) {
font-size: 0.75em;
padding: 4px 2px;
}
/* Calendar mobile styles handled by SpaceCalendar component */
}
</style>