Files
space-booking/frontend/src/views/Settings.vue
Claude Agent 0bf3e6a7e2 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 <noreply@anthropic.com>
2026-02-11 21:27:05 +00:00

365 lines
8.9 KiB
Vue

<template>
<div class="settings">
<h2>Global Booking Settings</h2>
<!-- Settings Form -->
<CollapsibleSection title="Booking Rules Configuration" :icon="Sliders">
<div v-if="loadingSettings" class="loading">Loading settings...</div>
<form v-else @submit.prevent="handleSubmit" class="settings-form">
<div class="form-row">
<div class="form-group">
<label for="minDuration">Minimum Duration (minutes)</label>
<input
id="minDuration"
v-model.number="formData.min_duration_minutes"
type="number"
required
min="15"
max="480"
placeholder="30"
/>
<small>Between 15 and 480 minutes</small>
</div>
<div class="form-group">
<label for="maxDuration">Maximum Duration (minutes)</label>
<input
id="maxDuration"
v-model.number="formData.max_duration_minutes"
type="number"
required
min="30"
max="1440"
placeholder="480"
/>
<small>Between 30 and 1440 minutes (24h)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="workStart">Working Hours Start (hour)</label>
<input
id="workStart"
v-model.number="formData.working_hours_start"
type="number"
required
min="0"
max="23"
placeholder="8"
/>
<small>0-23 (e.g., 8 = 8:00 AM)</small>
</div>
<div class="form-group">
<label for="workEnd">Working Hours End (hour)</label>
<input
id="workEnd"
v-model.number="formData.working_hours_end"
type="number"
required
min="1"
max="24"
placeholder="20"
/>
<small>1-24 (e.g., 20 = 8:00 PM)</small>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="maxBookings">Max Bookings per Day per User</label>
<input
id="maxBookings"
v-model.number="formData.max_bookings_per_day_per_user"
type="number"
required
min="1"
max="20"
placeholder="3"
/>
<small>Between 1 and 20</small>
</div>
<div class="form-group">
<label for="minCancel">Min Hours Before Cancel</label>
<input
id="minCancel"
v-model.number="formData.min_hours_before_cancel"
type="number"
required
min="0"
max="72"
placeholder="2"
/>
<small>Between 0 and 72 hours</small>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="loading">
Save Settings
</button>
</div>
</form>
<div v-if="error" class="error">{{ error }}</div>
<div v-if="success" class="success">{{ success }}</div>
</CollapsibleSection>
<!-- Info Card -->
<CollapsibleSection title="About These Settings" :icon="Info">
<ul class="info-list">
<li><strong>Duration:</strong> Controls minimum and maximum booking length</li>
<li><strong>Working Hours:</strong> Bookings outside these hours will be rejected</li>
<li><strong>Max Bookings:</strong> Limits how many bookings a user can make per day</li>
<li><strong>Cancel Policy:</strong> Users cannot cancel bookings too close to start time</li>
</ul>
<p class="note">These rules apply to all new booking requests.</p>
</CollapsibleSection>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { settingsApi, handleApiError } from '@/services/api'
import CollapsibleSection from '@/components/CollapsibleSection.vue'
import { Sliders, Info } from 'lucide-vue-next'
import type { Settings } from '@/types'
const loadingSettings = ref(true)
const loading = ref(false)
const error = ref('')
const success = ref('')
const formData = ref<Omit<Settings, 'id'>>({
min_duration_minutes: 30,
max_duration_minutes: 480,
working_hours_start: 8,
working_hours_end: 20,
max_bookings_per_day_per_user: 3,
min_hours_before_cancel: 2
})
const loadSettings = async () => {
try {
loadingSettings.value = true
const settings = await settingsApi.get()
formData.value = {
min_duration_minutes: settings.min_duration_minutes,
max_duration_minutes: settings.max_duration_minutes,
working_hours_start: settings.working_hours_start,
working_hours_end: settings.working_hours_end,
max_bookings_per_day_per_user: settings.max_bookings_per_day_per_user,
min_hours_before_cancel: settings.min_hours_before_cancel
}
} catch (err) {
error.value = handleApiError(err)
} finally {
loadingSettings.value = false
}
}
const validateForm = (): boolean => {
if (
formData.value.min_duration_minutes <= 0 ||
formData.value.max_duration_minutes <= 0 ||
formData.value.working_hours_start < 0 ||
formData.value.working_hours_end < 0 ||
formData.value.max_bookings_per_day_per_user <= 0 ||
formData.value.min_hours_before_cancel < 0
) {
error.value = 'All fields must be positive values'
return false
}
if (formData.value.min_duration_minutes >= formData.value.max_duration_minutes) {
error.value = 'Minimum duration must be less than maximum duration'
return false
}
if (formData.value.working_hours_start >= formData.value.working_hours_end) {
error.value = 'Working hours start must be less than working hours end'
return false
}
if (formData.value.working_hours_start < 0 || formData.value.working_hours_start > 23) {
error.value = 'Working hours start must be between 0 and 23'
return false
}
if (formData.value.working_hours_end < 1 || formData.value.working_hours_end > 24) {
error.value = 'Working hours end must be between 1 and 24'
return false
}
return true
}
const handleSubmit = async () => {
error.value = ''
success.value = ''
if (!validateForm()) {
return
}
loading.value = true
try {
await settingsApi.update(formData.value)
success.value = 'Settings updated successfully!'
setTimeout(() => {
success.value = ''
}, 3000)
} catch (err) {
error.value = handleApiError(err)
} finally {
loading.value = false
}
}
onMounted(() => {
loadSettings()
})
</script>
<style scoped>
.settings {
max-width: 900px;
margin: 0 auto;
}
h2 {
margin-bottom: 1.5rem;
color: var(--color-text-primary);
}
.settings-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-weight: 500;
color: var(--color-text-primary);
font-size: 14px;
}
.form-group input {
padding: 0.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
font-size: 1rem;
background: var(--color-surface);
color: var(--color-text-primary);
}
.form-group input:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent);
}
.form-group small {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.form-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
}
.btn {
padding: 0.6rem 1.2rem;
border: none;
border-radius: var(--radius-sm);
font-size: 1rem;
cursor: pointer;
transition: all var(--transition-fast);
}
.btn-primary {
background: var(--color-accent);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-hover);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.loading {
text-align: center;
padding: 2rem;
color: var(--color-text-secondary);
}
.error {
padding: 0.75rem;
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
border-radius: var(--radius-sm);
color: var(--color-danger);
margin-top: 1rem;
}
.success {
padding: 0.75rem;
background: color-mix(in srgb, var(--color-success) 10%, transparent);
border-radius: var(--radius-sm);
color: var(--color-success);
margin-top: 1rem;
}
.info-list {
margin: 0;
padding-left: 1.5rem;
}
.info-list li {
margin-bottom: 0.5rem;
color: var(--color-text-secondary);
}
.info-list strong {
color: var(--color-text-primary);
}
.note {
margin-top: 1rem;
margin-bottom: 0;
font-style: italic;
color: var(--color-text-secondary);
}
.collapsible-section + .collapsible-section {
margin-top: 16px;
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
}
}
</style>