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>
365 lines
8.9 KiB
Vue
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>
|