From 0bf3e6a7e2e150919dc0a806ceba997e81fd3433 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 11 Feb 2026 21:27:05 +0000 Subject: [PATCH] feat: complete UI redesign with dark mode, sidebar navigation, and modern design system Implemented comprehensive UI overhaul with three-layer architecture: Layer 1 - Theme System: - CSS variables for light/dark themes (theme.css) - Theme composable with light/dark/auto mode (useTheme.ts) - Sidebar state management composable (useSidebar.ts) - Refactored main.css to use CSS variables throughout Layer 2 - Core Components: - AppSidebar with collapsible navigation (desktop) and overlay (mobile) - CollapsibleSection reusable component for expandable cards - Restructured App.vue with new sidebar layout - Integrated Lucide icons library (lucide-vue-next) Layer 3 - Views & Components: - Updated all 14 views with CSS variables and responsive design - Replaced inline SVG with Lucide icon components - Added collapsible sections to Dashboard, Admin pages, UserProfile - Updated 3 shared components (BookingForm, SpaceCalendar, AttachmentsList) Features: - Dark/light/auto theme with persistent preference - Collapsible sidebar (icons-only on desktop, overlay on mobile) - Consistent color palette using CSS variables - Full responsive design across all pages - Modern minimalist aesthetic with Indigo accent color Co-Authored-By: Claude Sonnet 4.5 --- IMPLEMENTATION_SUMMARY.md | 142 ------ frontend/package-lock.json | 10 + frontend/package.json | 1 + frontend/src/App.vue | 270 ++++++----- frontend/src/assets/main.css | 57 ++- frontend/src/assets/theme.css | 69 +++ frontend/src/components/AppSidebar.vue | 320 +++++++++++++ frontend/src/components/AttachmentsList.vue | 34 +- frontend/src/components/BookingForm.vue | 53 ++- .../src/components/CollapsibleSection.vue | 94 ++++ frontend/src/components/SpaceCalendar.vue | 77 +-- frontend/src/composables/useSidebar.ts | 43 ++ frontend/src/composables/useTheme.ts | 52 ++ frontend/src/main.ts | 1 + frontend/src/views/Admin.vue | 228 ++++----- frontend/src/views/AdminPending.vue | 238 ++++----- frontend/src/views/AdminReports.vue | 263 +++++----- frontend/src/views/AuditLog.vue | 205 ++++---- frontend/src/views/Dashboard.vue | 450 ++++++------------ frontend/src/views/Login.vue | 21 +- frontend/src/views/MyBookings.vue | 155 +++--- frontend/src/views/Register.vue | 26 +- frontend/src/views/Settings.vue | 97 ++-- frontend/src/views/SpaceDetail.vue | 113 ++--- frontend/src/views/Spaces.vue | 145 +++--- frontend/src/views/UserProfile.vue | 134 ++---- frontend/src/views/Users.vue | 266 ++++++----- frontend/src/views/VerifyEmail.vue | 37 +- 28 files changed, 1960 insertions(+), 1641 deletions(-) delete mode 100644 IMPLEMENTATION_SUMMARY.md create mode 100644 frontend/src/assets/theme.css create mode 100644 frontend/src/components/AppSidebar.vue create mode 100644 frontend/src/components/CollapsibleSection.vue create mode 100644 frontend/src/composables/useSidebar.ts create mode 100644 frontend/src/composables/useTheme.ts diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 17d555c..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,142 +0,0 @@ -# Implementation Summary: Timezone Fix + Per-Space Settings - -## Overview -Fixed timezone display issues and added per-space scheduling settings to the space booking system. - -## Changes Implemented - -### Part A: Frontend Timezone Fix ✅ - -**File: `frontend/src/utils/datetime.ts`** -- Added `ensureUTC()` function to handle naive datetime strings from backend -- Applied `ensureUTC()` to all `new Date()` calls in: - - `formatDateTime()` - - `formatTime()` - - `formatDateTimeWithTZ()` - - `isoToLocalDateTime()` - -**File: `frontend/src/views/Dashboard.vue`** -- Imported `ensureUTC` from utils -- Applied to booking date comparisons in `upcomingBookings` computed property - -**Impact:** MyBookings now correctly shows times in user's timezone (e.g., 10:00-17:00 Bucharest instead of 08:00-15:00 UTC). - ---- - -### Part B: Per-Space Scheduling Settings (Database) ✅ - -**File: `backend/app/models/space.py`** -- Added 4 nullable columns: - - `working_hours_start` (Integer) - - `working_hours_end` (Integer) - - `min_duration_minutes` (Integer) - - `max_duration_minutes` (Integer) - -**File: `backend/app/schemas/space.py`** -- Added 4 optional fields to `SpaceBase` and `SpaceResponse` - -**File: `backend/migrations/004_add_per_space_settings.sql`** -- Created migration (already applied to database) - -**Impact:** Spaces can now override global settings. NULL = use global default. - ---- - -### Part C: Timezone-Aware Validation ✅ - -**File: `backend/app/services/booking_service.py`** -- Imported `Space` model and timezone utilities -- Added `user_timezone` parameter to `validate_booking_rules()` -- Load per-space settings with fallback to global -- Convert UTC times to user timezone for working hours validation -- Fix max bookings per day to use local date boundaries - -**File: `backend/app/api/bookings.py`** -Updated 6 endpoints to pass `user_timezone`: - -1. **POST /bookings (create)** - Line ~251 -2. **POST /bookings/recurring** - Line ~376 - - Also fixed: Now converts local times to UTC before storage -3. **PUT /bookings/{id} (update)** - Line ~503 -4. **PUT /admin/.../approve** - Line ~682 - - Uses booking owner's timezone -5. **PUT /admin/.../update** - Line ~865 - - Uses booking owner's timezone -6. **PUT /admin/.../reschedule** - Line ~1013 - - Uses booking owner's timezone - -**Impact:** -- Working hours validation now uses user's local time (9:00 Bucharest is valid, not rejected as 7:00 UTC) -- Per-space settings are respected -- Recurring bookings now store correct UTC times - ---- - -### Part D: Admin UI for Per-Space Settings ✅ - -**File: `frontend/src/views/Admin.vue`** -- Added form section "Per-Space Scheduling Settings" -- Added 4 input fields with placeholders indicating global defaults -- Updated `formData` reactive object -- Updated `startEdit()` to load space settings -- Updated `resetForm()` to clear settings -- Added CSS for form-section-header, form-row, and help-text - -**File: `frontend/src/types/index.ts`** -- Extended `Space` interface with 4 optional fields - -**Impact:** Admins can now configure per-space scheduling rules via UI. - ---- - -## Testing Checklist - -### Timezone Display -- [ ] User with `Europe/Bucharest` timezone sees correct local times in MyBookings -- [ ] Booking created at 09:00 Bucharest shows as 09:00 (not 07:00) - -### Working Hours Validation -- [ ] Booking at 09:00 Bucharest (07:00 UTC) is accepted (not rejected by 8-20 rule) -- [ ] User sees error message with correct timezone-aware hours - -### Per-Space Settings -- [ ] Create space with custom working hours (e.g., 10-18) -- [ ] Booking outside custom hours is rejected -- [ ] Space without settings uses global defaults -- [ ] Admin UI displays and saves per-space settings correctly - -### Recurring Bookings -- [ ] Recurring bookings now store correct UTC times -- [ ] Bookings created for multiple occurrences have consistent timezone handling - ---- - -## Files Modified - -| # | File | Changes | -|---|------|---------| -| 1 | `frontend/src/utils/datetime.ts` | Added `ensureUTC()` + applied to 4 functions | -| 2 | `frontend/src/views/Dashboard.vue` | Import and use `ensureUTC` | -| 3 | `backend/app/models/space.py` | Added 4 nullable columns | -| 4 | `backend/app/schemas/space.py` | Added 4 optional fields | -| 5 | `backend/migrations/004_add_per_space_settings.sql` | DB migration (applied) | -| 6 | `backend/app/services/booking_service.py` | Timezone-aware validation | -| 7 | `backend/app/api/bookings.py` | Updated 6 callers + recurring fix | -| 8 | `frontend/src/views/Admin.vue` | Admin UI for per-space settings | -| 9 | `frontend/src/types/index.ts` | Extended Space interface | - ---- - -## Known Limitations - -1. **SQLite**: COMMENT ON COLUMN not supported (documented in migration file) -2. **Backward Compatibility**: Existing bookings created before fix may have incorrect times (data migration not included) - ---- - -## Next Steps - -1. Run frontend ESLint: `cd frontend && npx eslint src/utils/datetime.ts src/views/Dashboard.vue src/views/Admin.vue` -2. Run backend tests: `cd backend && pytest` (if tests exist) -3. Manual testing per checklist above -4. Consider data migration for existing bookings if needed diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d8dc730..f8d6c4b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@fullcalendar/vue3": "^6.1.0", "axios": "^1.6.0", "chart.js": "^4.5.1", + "lucide-vue-next": "^0.563.0", "pinia": "^2.1.0", "vue": "^3.4.0", "vue-router": "^4.2.0" @@ -2769,6 +2770,15 @@ "dev": true, "license": "MIT" }, + "node_modules/lucide-vue-next": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.563.0.tgz", + "integrity": "sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA==", + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index 84ca054..29a44b1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@fullcalendar/vue3": "^6.1.0", "axios": "^1.6.0", "chart.js": "^4.5.1", + "lucide-vue-next": "^0.563.0", "pinia": "^2.1.0", "vue": "^3.4.0", "vue-router": "^4.2.0" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3521bd8..4e6196c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,34 +1,29 @@ @@ -67,10 +61,14 @@ import { ref, computed, onMounted, onUnmounted } from 'vue' import { useAuthStore } from '@/stores/auth' import { useRouter } from 'vue-router' import { notificationsApi } from '@/services/api' +import { useSidebar } from '@/composables/useSidebar' import type { Notification } from '@/types' +import AppSidebar from '@/components/AppSidebar.vue' +import { Menu, Bell, X } from 'lucide-vue-next' const authStore = useAuthStore() const router = useRouter() +const { collapsed, toggleMobile } = useSidebar() const notifications = ref([]) const showNotifications = ref(false) @@ -83,17 +81,11 @@ const unreadCount = computed(() => { return notifications.value.filter((n) => !n.is_read).length }) -const logout = () => { - authStore.logout() - router.push('/login') -} - const fetchNotifications = async () => { if (!authStore.isAuthenticated) return try { loading.value = true - // Get all notifications, sorted by created_at DESC (from API) notifications.value = await notificationsApi.getAll() } catch (error) { console.error('Failed to fetch notifications:', error) @@ -114,18 +106,15 @@ const closeNotifications = () => { } const handleNotificationClick = async (notification: Notification) => { - // Mark as read if (!notification.is_read) { try { await notificationsApi.markAsRead(notification.id) - // Update local state notification.is_read = true } catch (error) { console.error('Failed to mark notification as read:', error) } } - // Navigate to booking if available if (notification.booking_id) { closeNotifications() router.push('/my-bookings') @@ -147,25 +136,20 @@ const formatTime = (dateStr: string): string => { return date.toLocaleDateString() } -// Click outside to close const handleClickOutside = (event: MouseEvent) => { + const target = event.target as HTMLElement if ( dropdownRef.value && - !dropdownRef.value.contains(event.target as Node) && - !(event.target as HTMLElement).closest('.notification-bell') + !dropdownRef.value.contains(target) && + !target.closest('.notification-bell') ) { closeNotifications() } } onMounted(() => { - // Initial fetch fetchNotifications() - - // Auto-refresh every 30 seconds refreshInterval = window.setInterval(fetchNotifications, 30000) - - // Add click outside listener document.addEventListener('click', handleClickOutside) }) @@ -178,64 +162,68 @@ onUnmounted(() => { diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index ad8e30a..40fe954 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -9,8 +9,8 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; line-height: 1.6; - color: #2c3e50; - background: #f5f5f5; + color: var(--color-text-primary); + background: var(--color-bg-primary); } #app { @@ -21,37 +21,37 @@ body { .btn { padding: 0.75rem 1.5rem; border: none; - border-radius: 4px; + border-radius: var(--radius-sm); cursor: pointer; font-size: 1rem; - transition: all 0.2s; + transition: all var(--transition-fast); } .btn-primary { - background: #3498db; + background: var(--color-accent); color: white; } .btn-primary:hover { - background: #2980b9; + background: var(--color-accent-hover); } .btn-success { - background: #27ae60; + background: var(--color-success); color: white; } .btn-success:hover { - background: #229954; + filter: brightness(0.9); } .btn-danger { - background: #e74c3c; + background: var(--color-danger); color: white; } .btn-danger:hover { - background: #c0392b; + filter: brightness(0.9); } /* Forms */ @@ -63,6 +63,7 @@ body { display: block; margin-bottom: 0.5rem; font-weight: 500; + color: var(--color-text-primary); } .form-group input, @@ -70,57 +71,61 @@ body { .form-group textarea { width: 100%; padding: 0.75rem; - border: 1px solid #ddd; - border-radius: 4px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); font-size: 1rem; + background: var(--color-surface); + color: var(--color-text-primary); + transition: border-color var(--transition-fast); } .form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; - border-color: #3498db; + border-color: var(--color-accent); } .error { - color: #e74c3c; + color: var(--color-danger); font-size: 0.9rem; margin-top: 0.25rem; } /* Cards */ .card { - background: white; - border-radius: 8px; + background: var(--color-surface); + border-radius: var(--radius-md); padding: 1.5rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: var(--shadow-sm); + border: 1px solid var(--color-border); } /* Status badges */ .badge { display: inline-block; padding: 0.25rem 0.75rem; - border-radius: 12px; + border-radius: var(--radius-lg); font-size: 0.85rem; font-weight: 500; } .badge-pending { - background: #fff3cd; - color: #856404; + background: color-mix(in srgb, var(--color-warning) 15%, transparent); + color: var(--color-warning); } .badge-approved { - background: #d4edda; - color: #155724; + background: color-mix(in srgb, var(--color-success) 15%, transparent); + color: var(--color-success); } .badge-rejected { - background: #f8d7da; - color: #721c24; + background: color-mix(in srgb, var(--color-danger) 15%, transparent); + color: var(--color-danger); } .badge-canceled { - background: #e2e3e5; - color: #383d41; + background: var(--color-bg-tertiary); + color: var(--color-text-secondary); } diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css new file mode 100644 index 0000000..c9b12ff --- /dev/null +++ b/frontend/src/assets/theme.css @@ -0,0 +1,69 @@ +/* Design Tokens - Light Theme (default) */ +:root { + /* Base colors */ + --color-bg-primary: #ffffff; + --color-bg-secondary: #f8f9fa; + --color-bg-tertiary: #f1f3f5; + --color-surface: #ffffff; + --color-surface-hover: #f8f9fa; + + /* Text */ + --color-text-primary: #1a1a2e; + --color-text-secondary: #6b7280; + --color-text-muted: #9ca3af; + + /* Accent */ + --color-accent: #6366f1; + --color-accent-hover: #4f46e5; + --color-accent-light: #eef2ff; + + /* Semantic */ + --color-success: #10b981; + --color-warning: #f59e0b; + --color-danger: #ef4444; + --color-info: #3b82f6; + + /* Borders */ + --color-border: #e5e7eb; + --color-border-light: #f3f4f6; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + + /* Sidebar */ + --sidebar-width: 260px; + --sidebar-collapsed-width: 68px; + --sidebar-bg: #1a1a2e; + --sidebar-text: #a1a1b5; + --sidebar-text-active: #ffffff; + --sidebar-hover-bg: rgba(255, 255, 255, 0.08); + + /* Spacing */ + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; +} + +/* Dark Theme */ +[data-theme="dark"] { + --color-bg-primary: #0f0f1a; + --color-bg-secondary: #1a1a2e; + --color-bg-tertiary: #232340; + --color-surface: #1a1a2e; + --color-surface-hover: #232340; + --color-text-primary: #e5e5ef; + --color-text-secondary: #9ca3af; + --color-text-muted: #6b7280; + --color-accent-light: #1e1b4b; + --color-border: #2d2d4a; + --color-border-light: #232340; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.4); +} diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue new file mode 100644 index 0000000..5f72d0e --- /dev/null +++ b/frontend/src/components/AppSidebar.vue @@ -0,0 +1,320 @@ + + + + + diff --git a/frontend/src/components/AttachmentsList.vue b/frontend/src/components/AttachmentsList.vue index 4499a54..05f4fc7 100644 --- a/frontend/src/components/AttachmentsList.vue +++ b/frontend/src/components/AttachmentsList.vue @@ -126,7 +126,7 @@ onMounted(() => { .attachments-title { font-size: 16px; font-weight: 600; - color: #111827; + color: var(--color-text-primary); margin: 0 0 12px 0; } @@ -134,19 +134,19 @@ onMounted(() => { .error, .no-attachments { padding: 12px; - border-radius: 6px; + border-radius: var(--radius-sm); font-size: 14px; } .loading, .no-attachments { - background: #f3f4f6; - color: #6b7280; + background: var(--color-bg-tertiary); + color: var(--color-text-secondary); } .error { - background: #fee2e2; - color: #991b1b; + background: color-mix(in srgb, var(--color-danger) 10%, transparent); + color: var(--color-danger); } .attachment-items { @@ -160,9 +160,9 @@ onMounted(() => { align-items: center; justify-content: space-between; padding: 12px; - background: #f9fafb; - border: 1px solid #e5e7eb; - border-radius: 6px; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); margin-bottom: 8px; gap: 12px; } @@ -189,7 +189,7 @@ onMounted(() => { .attachment-name { font-size: 14px; - color: #3b82f6; + color: var(--color-accent); text-decoration: none; font-weight: 500; overflow: hidden; @@ -203,23 +203,23 @@ onMounted(() => { .attachment-meta { font-size: 12px; - color: #6b7280; + color: var(--color-text-secondary); } .btn-delete { padding: 6px 12px; - background: white; - color: #ef4444; - border: 1px solid #ef4444; - border-radius: 4px; + background: var(--color-surface); + color: var(--color-danger); + border: 1px solid var(--color-danger); + border-radius: var(--radius-sm); font-size: 13px; cursor: pointer; - transition: all 0.2s; + transition: all var(--transition-fast); flex-shrink: 0; } .btn-delete:hover:not(:disabled) { - background: #fef2f2; + background: color-mix(in srgb, var(--color-danger) 5%, transparent); } .btn-delete:disabled { diff --git a/frontend/src/components/BookingForm.vue b/frontend/src/components/BookingForm.vue index bae8a1b..9c767f1 100644 --- a/frontend/src/components/BookingForm.vue +++ b/frontend/src/components/BookingForm.vue @@ -437,7 +437,7 @@ onMounted(() => { display: block; margin-bottom: 6px; font-weight: 500; - color: #374151; + color: var(--color-text-primary); font-size: 14px; } @@ -445,7 +445,7 @@ onMounted(() => { display: block; margin-bottom: 4px; font-weight: 400; - color: #6b7280; + color: var(--color-text-secondary); font-size: 12px; } @@ -464,26 +464,28 @@ onMounted(() => { .form-textarea { width: 100%; padding: 8px 12px; - border: 1px solid #d1d5db; - border-radius: 4px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); font-size: 14px; font-family: inherit; - transition: border-color 0.2s; + transition: border-color var(--transition-fast); + background: var(--color-surface); + color: var(--color-text-primary); } .form-input:focus, .form-textarea:focus { outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + border-color: var(--color-accent); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent); } .form-input-error { - border-color: #ef4444; + border-color: var(--color-danger); } .form-input:disabled { - background-color: #f3f4f6; + background-color: var(--color-bg-tertiary); cursor: not-allowed; } @@ -494,24 +496,24 @@ onMounted(() => { .form-error { display: block; margin-top: 4px; - color: #ef4444; + color: var(--color-danger); font-size: 13px; } .api-error { padding: 12px; - background: #fee2e2; - color: #991b1b; - border-radius: 4px; + 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; } .success-message { padding: 12px; - background: #d1fae5; - color: #065f46; - border-radius: 4px; + background: color-mix(in srgb, var(--color-success) 10%, transparent); + color: var(--color-success); + border-radius: var(--radius-sm); margin-bottom: 16px; font-size: 14px; } @@ -525,11 +527,11 @@ onMounted(() => { .btn { padding: 10px 20px; border: none; - border-radius: 4px; + border-radius: var(--radius-sm); font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s; + transition: all var(--transition-fast); } .btn:disabled { @@ -538,33 +540,34 @@ onMounted(() => { } .btn-primary { - background: #3b82f6; + background: var(--color-accent); color: white; } .btn-primary:hover:not(:disabled) { - background: #2563eb; + background: var(--color-accent-hover); } .btn-secondary { - background: #6b7280; + background: var(--color-text-secondary); color: white; } .btn-secondary:hover:not(:disabled) { - background: #4b5563; + background: var(--color-text-primary); } .warning-banner { - background: #fff3cd; - border: 1px solid #ffc107; - border-radius: 4px; + background: color-mix(in srgb, var(--color-warning) 15%, transparent); + border: 1px solid var(--color-warning); + border-radius: var(--radius-sm); padding: 12px; margin-bottom: 16px; display: flex; gap: 12px; align-items: flex-start; font-size: 14px; + color: var(--color-text-primary); } .warning-icon { diff --git a/frontend/src/components/CollapsibleSection.vue b/frontend/src/components/CollapsibleSection.vue new file mode 100644 index 0000000..9cb01b4 --- /dev/null +++ b/frontend/src/components/CollapsibleSection.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/components/SpaceCalendar.vue b/frontend/src/components/SpaceCalendar.vue index 7bd4731..9148bcf 100644 --- a/frontend/src/components/SpaceCalendar.vue +++ b/frontend/src/components/SpaceCalendar.vue @@ -313,32 +313,32 @@ defineExpose({ refresh }) diff --git a/frontend/src/views/AdminPending.vue b/frontend/src/views/AdminPending.vue index b20bc48..21b5101 100644 --- a/frontend/src/views/AdminPending.vue +++ b/frontend/src/views/AdminPending.vue @@ -3,8 +3,7 @@

Admin Dashboard - Pending Booking Requests

-
-

Filters

+
@@ -16,7 +15,7 @@
-
+
@@ -32,65 +31,66 @@
-
-

Pending Requests ({{ bookings.length }})

- - - - - - - - - - - - - - - + + + + + + + + +
UserSpaceDateTimeTitleDescriptionActions
- +
+
{{ booking.space?.name || 'Unknown Space' }}
+
{{ formatType(booking.space?.type || '') }}
+
+
{{ formatDate(booking.start_datetime) }}{{ formatTime(booking.start_datetime, booking.end_datetime) }}{{ booking.title }} +
+ {{ truncateText(booking.description || '-', 40) }} +
+
+ + +
+
+ @@ -144,6 +144,8 @@ import { ref, computed, onMounted } from 'vue' import { adminBookingsApi, spacesApi, handleApiError } from '@/services/api' import { useAuthStore } from '@/stores/auth' import { formatDate as formatDateUtil, formatTime as formatTimeUtil } from '@/utils/datetime' +import CollapsibleSection from '@/components/CollapsibleSection.vue' +import { Filter, ClipboardCheck } from 'lucide-vue-next' import type { Booking, Space } from '@/types' const authStore = useAuthStore() @@ -279,17 +281,24 @@ onMounted(() => { diff --git a/frontend/src/views/AdminReports.vue b/frontend/src/views/AdminReports.vue index af66e53..1df57dd 100644 --- a/frontend/src/views/AdminReports.vue +++ b/frontend/src/views/AdminReports.vue @@ -3,20 +3,22 @@

Booking Reports

-
- + +
+ - + - - -
+ + +
+
Loading reports...
@@ -53,63 +55,67 @@

Space Usage Report

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SpaceTotalApprovedPendingRejectedCanceledHours
{{ item.space_name }}{{ item.total_bookings }}{{ item.approved_bookings }}{{ item.pending_bookings }}{{ item.rejected_bookings }}{{ item.canceled_bookings }}{{ item.total_hours.toFixed(1) }}h
Total{{ usageReport?.total_bookings }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SpaceTotalApprovedPendingRejectedCanceledHours
{{ item.space_name }}{{ item.total_bookings }}{{ item.approved_bookings }}{{ item.pending_bookings }}{{ item.rejected_bookings }}{{ item.canceled_bookings }}{{ item.total_hours.toFixed(1) }}h
Total{{ usageReport?.total_bookings }}
+

Top Users Report

- - - - - - - - - - - - - - - - - - - -
UserEmailTotal BookingsApprovedTotal Hours
{{ item.user_name }}{{ item.user_email }}{{ item.total_bookings }}{{ item.approved_bookings }}{{ item.total_hours.toFixed(1) }}h
+
+ + + + + + + + + + + + + + + + + + + +
UserEmailTotal BookingsApprovedTotal Hours
{{ item.user_name }}{{ item.user_email }}{{ item.total_bookings }}{{ item.approved_bookings }}{{ item.total_hours.toFixed(1) }}h
+
@@ -144,6 +150,8 @@ import { ref, onMounted, watch, nextTick } from 'vue' import { reportsApi } from '@/services/api' import Chart from 'chart.js/auto' +import CollapsibleSection from '@/components/CollapsibleSection.vue' +import { CalendarDays } from 'lucide-vue-next' import type { SpaceUsageReport, TopUsersReport, ApprovalRateReport } from '@/types' const activeTab = ref('usage') @@ -295,25 +303,16 @@ onMounted(() => { diff --git a/frontend/src/views/AuditLog.vue b/frontend/src/views/AuditLog.vue index 14f5c06..c58dd76 100644 --- a/frontend/src/views/AuditLog.vue +++ b/frontend/src/views/AuditLog.vue @@ -19,55 +19,57 @@ - - + + -

Se încarcă...

+

Se încarcă...

{{ error }}

- - - - - - - - - - - - - - - - - - - - - -
TimestampUtilizatorAcțiuneTip TargetID TargetDetalii
{{ formatDate(log.created_at) }} -
{{ log.user_name }}
- {{ log.user_email }} -
{{ formatAction(log.action) }}{{ log.target_type }}{{ log.target_id }} -
{{
-              formatDetails(log.details)
-            }}
- - -
+
+ + + + + + + + + + + + + + + + + + + + + +
TimestampUtilizatorAcțiuneTip TargetID TargetDetalii
{{ formatDate(log.created_at) }} +
{{ log.user_name }}
+ {{ log.user_email }} +
{{ formatAction(log.action) }}{{ log.target_type }}{{ log.target_id }} +
{{
+                formatDetails(log.details)
+              }}
+ - +
+
-

Nu există înregistrări în jurnal.

+

Nu există înregistrări în jurnal.

@@ -158,14 +160,9 @@ onMounted(() => { diff --git a/frontend/src/views/Dashboard.vue b/frontend/src/views/Dashboard.vue index c0a6f9b..bdc8595 100644 --- a/frontend/src/views/Dashboard.vue +++ b/frontend/src/views/Dashboard.vue @@ -11,126 +11,72 @@
-
-
-
- - - + +
+
+
+ +
+
+

{{ stats.total }}

+

Total Bookings

+
-
-

{{ stats.total }}

-

Total Bookings

-
-
-
-
- - - +
+
+ +
+
+

{{ stats.pending }}

+

Pending

+
-
-

{{ stats.pending }}

-

Pending

-
-
-
-
- - - +
+
+ +
+
+

{{ stats.approved }}

+

Approved

+
-
-

{{ stats.approved }}

-

Approved

-
-
- -
-
- - - -
-
-

{{ adminStats.pendingRequests }}

-

Pending Requests

+ +
+
+ +
+
+

{{ adminStats.pendingRequests }}

+

Pending Requests

+
-
+

Quick Actions

- - - + Book a Space - - - + My Bookings - - - + Manage Bookings - - - + Manage Spaces
@@ -138,21 +84,13 @@
-
-
-

Upcoming Bookings

+ +
- - - +

No upcoming bookings

@@ -163,14 +101,7 @@ class="booking-item" >
- - - +

{{ booking.title }}

@@ -184,24 +115,16 @@
-
+ -
-
-

Available Spaces

+ +
- - - +

No available spaces

@@ -213,14 +136,7 @@ class="space-item" >
- - - +

{{ space.name }}

@@ -228,25 +144,17 @@ {{ formatType(space.type) }} · Capacity: {{ space.capacity }}

- - - +
-
+
-
-
-

Recent Activity

+ +

No recent activity

@@ -255,14 +163,7 @@
- - - +

{{ log.action }}

@@ -271,7 +172,7 @@
-
+
@@ -288,6 +189,22 @@ import { } from '@/services/api' import { useAuthStore } from '@/stores/auth' import { formatDateTime as formatDateTimeUtil, ensureUTC } from '@/utils/datetime' +import CollapsibleSection from '@/components/CollapsibleSection.vue' +import { + Calendar, + CalendarDays, + Clock, + CheckCircle, + Users, + Search, + ClipboardList, + ClipboardCheck, + Building2, + ChevronRight, + FileText, + ScrollText, + BarChart3 +} from 'lucide-vue-next' import type { Space, Booking, AuditLog, User } from '@/types' const authStore = useAuthStore() @@ -402,15 +319,10 @@ onMounted(() => { diff --git a/frontend/src/views/MyBookings.vue b/frontend/src/views/MyBookings.vue index 6f47dff..58e4b2c 100644 --- a/frontend/src/views/MyBookings.vue +++ b/frontend/src/views/MyBookings.vue @@ -297,11 +297,9 @@ const formatStatus = (status: string): string => { const openEditModal = (booking: Booking) => { editingBooking.value = booking - // Extract date and time from ISO datetime const startLocal = isoToLocalDateTime(booking.start_datetime, userTimezone.value) const endLocal = isoToLocalDateTime(booking.end_datetime, userTimezone.value) - // Split YYYY-MM-DDTHH:mm into date and time parts const [startDate, startTime] = startLocal.split('T') const [endDate, endTime] = endLocal.split('T') @@ -330,7 +328,6 @@ const saveEdit = async () => { editError.value = '' try { - // Combine date and time, then convert to ISO const startDateTime = `${editForm.value.start_date}T${editForm.value.start_time}` const endDateTime = `${editForm.value.end_date}T${editForm.value.end_time}` @@ -352,7 +349,6 @@ const saveEdit = async () => { } const canCancel = (booking: Booking): boolean => { - // Can only cancel pending or approved bookings return booking.status === 'pending' || booking.status === 'approved' } @@ -365,9 +361,6 @@ const handleCancel = async (booking: Booking) => { error.value = '' try { - // TODO: Implement cancel endpoint when available - // await bookingsApi.cancel(booking.id) - // For now, just show a message alert('Cancel functionality will be implemented in a future update.') await loadBookings() } catch (err) { @@ -383,17 +376,18 @@ onMounted(() => { diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue index f5ba373..efd724f 100644 --- a/frontend/src/views/Register.vue +++ b/frontend/src/views/Register.vue @@ -145,12 +145,12 @@ const handleRegister = async () => { h2 { text-align: center; margin-bottom: 0.5rem; - color: #2c3e50; + color: var(--color-text-primary); } .subtitle { text-align: center; - color: #7f8c8d; + color: var(--color-text-secondary); margin-bottom: 2rem; } @@ -158,7 +158,7 @@ h2 { display: block; margin-top: 0.25rem; font-size: 0.85rem; - color: #7f8c8d; + color: var(--color-text-secondary); } .btn-block { @@ -169,11 +169,11 @@ h2 { .login-link { text-align: center; margin-top: 1.5rem; - color: #7f8c8d; + color: var(--color-text-secondary); } .login-link a { - color: #3498db; + color: var(--color-accent); text-decoration: none; } @@ -184,18 +184,18 @@ h2 { .error { margin-top: 1rem; padding: 0.75rem; - background: #fee; - border-left: 3px solid #e74c3c; - border-radius: 4px; - color: #c0392b; + background: color-mix(in srgb, var(--color-danger) 10%, transparent); + border-left: 3px solid var(--color-danger); + border-radius: var(--radius-sm); + color: var(--color-danger); } .success { margin-top: 1rem; padding: 0.75rem; - background: #d4edda; - border-left: 3px solid #28a745; - border-radius: 4px; - color: #155724; + background: color-mix(in srgb, var(--color-success) 10%, transparent); + border-left: 3px solid var(--color-success); + border-radius: var(--radius-sm); + color: var(--color-success); } diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index c714d6d..b0047cc 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -3,9 +3,7 @@

Global Booking Settings

-
-

Booking Rules Configuration

- +
Loading settings...
@@ -108,25 +106,26 @@
{{ error }}
{{ success }}
-
+ -
-

About These Settings

-
    + +
    • Duration: Controls minimum and maximum booking length
    • Working Hours: Bookings outside these hours will be rejected
    • Max Bookings: Limits how many bookings a user can make per day
    • Cancel Policy: Users cannot cancel bookings too close to start time

    These rules apply to all new booking requests.

    -
+
diff --git a/frontend/src/views/Users.vue b/frontend/src/views/Users.vue index 34cf233..d6a675b 100644 --- a/frontend/src/views/Users.vue +++ b/frontend/src/views/Users.vue @@ -3,13 +3,13 @@ -
-

Filters

+
@@ -31,68 +31,69 @@ />
-
+ -
-

All Users

+
Loading users...
No users found. {{ filterRole || filterOrganization ? 'Try different filters.' : 'Create one above!' }}
- - - - - - - - - - - - - - - - - - - - - -
EmailFull NameRoleOrganizationStatusActions
{{ user.email }}{{ user.full_name }} - - {{ user.role }} - - {{ user.organization || '-' }} - - {{ user.is_active ? 'Active' : 'Inactive' }} - - - - - -
-
+
+ + + + + + + + + + + + + + + + + + + + + +
EmailFull NameRoleOrganizationStatusActions
{{ user.email }}{{ user.full_name }} + + {{ user.role }} + + {{ user.organization || '-' }} + + {{ user.is_active ? 'Active' : 'Inactive' }} + + + + + +
+
+