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>
This commit is contained in:
@@ -3,57 +3,59 @@
|
||||
<div class="page-header">
|
||||
<h2>Admin Dashboard - Space Management</h2>
|
||||
<button class="btn btn-primary" @click="openCreateModal">
|
||||
<Plus :size="16" />
|
||||
Create New Space
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Spaces List -->
|
||||
<div class="card">
|
||||
<h3>All Spaces</h3>
|
||||
<CollapsibleSection title="All Spaces" :icon="Building2">
|
||||
<div v-if="loadingSpaces" class="loading">Loading spaces...</div>
|
||||
<div v-else-if="spaces.length === 0" class="empty">
|
||||
No spaces created yet. Create one above!
|
||||
</div>
|
||||
<table v-else class="spaces-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Capacity</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="space in spaces" :key="space.id">
|
||||
<td>{{ space.name }}</td>
|
||||
<td>{{ space.type === 'sala' ? 'Sala' : 'Birou' }}</td>
|
||||
<td>{{ space.capacity }}</td>
|
||||
<td>
|
||||
<span :class="['badge', space.is_active ? 'badge-active' : 'badge-inactive']">
|
||||
{{ space.is_active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="startEdit(space)"
|
||||
:disabled="loading"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
:class="['btn', 'btn-sm', space.is_active ? 'btn-warning' : 'btn-success']"
|
||||
@click="toggleStatus(space)"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ space.is_active ? 'Deactivate' : 'Activate' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Capacity</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="space in spaces" :key="space.id">
|
||||
<td>{{ space.name }}</td>
|
||||
<td>{{ space.type === 'sala' ? 'Sala' : 'Birou' }}</td>
|
||||
<td>{{ space.capacity }}</td>
|
||||
<td>
|
||||
<span :class="['badge', space.is_active ? 'badge-active' : 'badge-inactive']">
|
||||
{{ space.is_active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="startEdit(space)"
|
||||
:disabled="loading"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
:class="['btn', 'btn-sm', space.is_active ? 'btn-warning' : 'btn-success']"
|
||||
@click="toggleStatus(space)"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ space.is_active ? 'Deactivate' : 'Activate' }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Create/Edit Space Modal -->
|
||||
<div v-if="showModal" class="modal" @click.self="closeModal">
|
||||
@@ -179,6 +181,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { spacesApi, handleApiError } from '@/services/api'
|
||||
import CollapsibleSection from '@/components/CollapsibleSection.vue'
|
||||
import { Building2, Plus } from 'lucide-vue-next'
|
||||
import type { Space } from '@/types'
|
||||
|
||||
const spaces = ref<Space[]>([])
|
||||
@@ -305,17 +309,18 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.space-form {
|
||||
@@ -328,20 +333,20 @@ onMounted(() => {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.form-section-header h4 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
@@ -364,7 +369,7 @@ onMounted(() => {
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -372,17 +377,19 @@ onMounted(() => {
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group 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-actions {
|
||||
@@ -392,13 +399,16 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
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 {
|
||||
@@ -407,39 +417,39 @@ 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;
|
||||
color: white;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #4b5563;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #10b981;
|
||||
background: var(--color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #059669;
|
||||
background: color-mix(in srgb, var(--color-success) 85%, black);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
background: var(--color-warning);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #d97706;
|
||||
background: color-mix(in srgb, var(--color-warning) 85%, black);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@@ -449,53 +459,58 @@ onMounted(() => {
|
||||
|
||||
.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-top: 12px;
|
||||
}
|
||||
|
||||
.success {
|
||||
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-top: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.spaces-table {
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.spaces-table th {
|
||||
.data-table th {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
background: var(--color-bg-secondary);
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.spaces-table td {
|
||||
.data-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.spaces-table tr:hover {
|
||||
background: #f9fafb;
|
||||
.data-table tr:hover {
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -507,13 +522,13 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -521,19 +536,6 @@ onMounted(() => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -548,18 +550,30 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<h2>Admin Dashboard - Pending Booking Requests</h2>
|
||||
|
||||
<!-- Filters Card -->
|
||||
<div class="card">
|
||||
<h3>Filters</h3>
|
||||
<CollapsibleSection title="Filters" :icon="Filter">
|
||||
<div class="filters">
|
||||
<div class="form-group">
|
||||
<label for="filter-space">Filter by Space</label>
|
||||
@@ -16,7 +15,7 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="card">
|
||||
@@ -32,65 +31,66 @@
|
||||
</div>
|
||||
|
||||
<!-- Bookings Table -->
|
||||
<div v-else class="card">
|
||||
<h3>Pending Requests ({{ bookings.length }})</h3>
|
||||
<table class="bookings-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Space</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="booking in bookings" :key="booking.id">
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ booking.user?.full_name || 'Unknown' }}</div>
|
||||
<div class="user-email">{{ booking.user?.email || '-' }}</div>
|
||||
<div class="user-org" v-if="booking.user?.organization">
|
||||
{{ booking.user.organization }}
|
||||
<CollapsibleSection v-else :title="`Pending Requests (${bookings.length})`" :icon="ClipboardCheck">
|
||||
<div class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Space</th>
|
||||
<th>Date</th>
|
||||
<th>Time</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="booking in bookings" :key="booking.id">
|
||||
<td>
|
||||
<div class="user-info">
|
||||
<div class="user-name">{{ booking.user?.full_name || 'Unknown' }}</div>
|
||||
<div class="user-email">{{ booking.user?.email || '-' }}</div>
|
||||
<div class="user-org" v-if="booking.user?.organization">
|
||||
{{ booking.user.organization }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="space-info">
|
||||
<div class="space-name">{{ booking.space?.name || 'Unknown Space' }}</div>
|
||||
<div class="space-type">{{ formatType(booking.space?.type || '') }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ formatDate(booking.start_datetime) }}</td>
|
||||
<td>{{ formatTime(booking.start_datetime, booking.end_datetime) }}</td>
|
||||
<td>{{ booking.title }}</td>
|
||||
<td>
|
||||
<div class="description" :title="booking.description || '-'">
|
||||
{{ truncateText(booking.description || '-', 40) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
@click="handleApprove(booking)"
|
||||
:disabled="processing === booking.id"
|
||||
>
|
||||
{{ processing === booking.id ? 'Processing...' : 'Approve' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="showRejectModal(booking)"
|
||||
:disabled="processing === booking.id"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="space-info">
|
||||
<div class="space-name">{{ booking.space?.name || 'Unknown Space' }}</div>
|
||||
<div class="space-type">{{ formatType(booking.space?.type || '') }}</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ formatDate(booking.start_datetime) }}</td>
|
||||
<td>{{ formatTime(booking.start_datetime, booking.end_datetime) }}</td>
|
||||
<td>{{ booking.title }}</td>
|
||||
<td>
|
||||
<div class="description" :title="booking.description || '-'">
|
||||
{{ truncateText(booking.description || '-', 40) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-sm btn-success"
|
||||
@click="handleApprove(booking)"
|
||||
:disabled="processing === booking.id"
|
||||
>
|
||||
{{ processing === booking.id ? 'Processing...' : 'Approve' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="showRejectModal(booking)"
|
||||
:disabled="processing === booking.id"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Reject Modal -->
|
||||
<div v-if="rejectingBooking" class="modal" @click.self="closeRejectModal">
|
||||
@@ -134,7 +134,7 @@
|
||||
|
||||
<!-- Success Message -->
|
||||
<div v-if="success" class="card">
|
||||
<div class="success">{{ success }}</div>
|
||||
<div class="success-msg">{{ success }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-pending {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
h2 {
|
||||
margin-bottom: 24px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 16px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.collapsible-section + .collapsible-section,
|
||||
.card + .collapsible-section,
|
||||
.collapsible-section + .card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
@@ -306,22 +315,25 @@ onMounted(() => {
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-group select:focus,
|
||||
.form-group 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-actions {
|
||||
@@ -332,53 +344,58 @@ onMounted(() => {
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.success {
|
||||
.success-msg {
|
||||
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);
|
||||
}
|
||||
|
||||
.bookings-table {
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.bookings-table th {
|
||||
.data-table th {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
background: var(--color-bg-secondary);
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bookings-table td {
|
||||
.data-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
vertical-align: top;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.bookings-table tr:hover {
|
||||
background: #f9fafb;
|
||||
.data-table tr:hover {
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.user-info,
|
||||
@@ -391,14 +408,14 @@ onMounted(() => {
|
||||
.user-name,
|
||||
.space-name {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.user-email,
|
||||
.user-org,
|
||||
.space-type {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -416,11 +433,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 {
|
||||
@@ -434,30 +451,30 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #10b981;
|
||||
background: var(--color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #059669;
|
||||
background: color-mix(in srgb, var(--color-success) 85%, black);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
background: color-mix(in srgb, var(--color-danger) 85%, black);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #4b5563;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.modal {
|
||||
@@ -474,22 +491,23 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.booking-summary {
|
||||
background: #f9fafb;
|
||||
border-radius: 4px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -497,10 +515,10 @@ onMounted(() => {
|
||||
.booking-summary p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.booking-summary strong {
|
||||
color: #1f2937;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,20 +3,22 @@
|
||||
<h2>Booking Reports</h2>
|
||||
|
||||
<!-- Date Range Filter -->
|
||||
<div class="filters">
|
||||
<label>
|
||||
Start Date:
|
||||
<input type="date" v-model="startDate" />
|
||||
</label>
|
||||
<CollapsibleSection title="Date Range Filter" :icon="CalendarDays">
|
||||
<div class="filters">
|
||||
<label>
|
||||
Start Date:
|
||||
<input type="date" v-model="startDate" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
End Date:
|
||||
<input type="date" v-model="endDate" />
|
||||
</label>
|
||||
<label>
|
||||
End Date:
|
||||
<input type="date" v-model="endDate" />
|
||||
</label>
|
||||
|
||||
<button @click="loadReports" class="btn-primary">Refresh</button>
|
||||
<button @click="clearFilters" class="btn-secondary">Clear Filters</button>
|
||||
</div>
|
||||
<button @click="loadReports" class="btn btn-primary">Refresh</button>
|
||||
<button @click="clearFilters" class="btn btn-secondary">Clear Filters</button>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="loading">Loading reports...</div>
|
||||
@@ -53,63 +55,67 @@
|
||||
<div v-if="activeTab === 'usage' && !loading" class="report-content">
|
||||
<h3>Space Usage Report</h3>
|
||||
<canvas ref="usageChart"></canvas>
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Space</th>
|
||||
<th>Total</th>
|
||||
<th>Approved</th>
|
||||
<th>Pending</th>
|
||||
<th>Rejected</th>
|
||||
<th>Canceled</th>
|
||||
<th>Hours</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in usageReport?.items" :key="item.space_id">
|
||||
<td>{{ item.space_name }}</td>
|
||||
<td>{{ item.total_bookings }}</td>
|
||||
<td class="status-approved">{{ item.approved_bookings }}</td>
|
||||
<td class="status-pending">{{ item.pending_bookings }}</td>
|
||||
<td class="status-rejected">{{ item.rejected_bookings }}</td>
|
||||
<td class="status-canceled">{{ item.canceled_bookings }}</td>
|
||||
<td>{{ item.total_hours.toFixed(1) }}h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td><strong>{{ usageReport?.total_bookings }}</strong></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Space</th>
|
||||
<th>Total</th>
|
||||
<th>Approved</th>
|
||||
<th>Pending</th>
|
||||
<th>Rejected</th>
|
||||
<th>Canceled</th>
|
||||
<th>Hours</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in usageReport?.items" :key="item.space_id">
|
||||
<td>{{ item.space_name }}</td>
|
||||
<td>{{ item.total_bookings }}</td>
|
||||
<td class="status-approved">{{ item.approved_bookings }}</td>
|
||||
<td class="status-pending">{{ item.pending_bookings }}</td>
|
||||
<td class="status-rejected">{{ item.rejected_bookings }}</td>
|
||||
<td class="status-canceled">{{ item.canceled_bookings }}</td>
|
||||
<td>{{ item.total_hours.toFixed(1) }}h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td><strong>Total</strong></td>
|
||||
<td><strong>{{ usageReport?.total_bookings }}</strong></td>
|
||||
<td colspan="5"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Users Report -->
|
||||
<div v-if="activeTab === 'users' && !loading" class="report-content">
|
||||
<h3>Top Users Report</h3>
|
||||
<canvas ref="usersChart"></canvas>
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Total Bookings</th>
|
||||
<th>Approved</th>
|
||||
<th>Total Hours</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in topUsersReport?.items" :key="item.user_id">
|
||||
<td>{{ item.user_name }}</td>
|
||||
<td>{{ item.user_email }}</td>
|
||||
<td>{{ item.total_bookings }}</td>
|
||||
<td class="status-approved">{{ item.approved_bookings }}</td>
|
||||
<td>{{ item.total_hours.toFixed(1) }}h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-responsive">
|
||||
<table class="report-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Total Bookings</th>
|
||||
<th>Approved</th>
|
||||
<th>Total Hours</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in topUsersReport?.items" :key="item.user_id">
|
||||
<td>{{ item.user_name }}</td>
|
||||
<td>{{ item.user_email }}</td>
|
||||
<td>{{ item.total_bookings }}</td>
|
||||
<td class="status-approved">{{ item.approved_bookings }}</td>
|
||||
<td>{{ item.total_hours.toFixed(1) }}h</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Approval Rate Report -->
|
||||
@@ -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(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-reports {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 30px;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filters label {
|
||||
@@ -321,58 +320,67 @@ h2 {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filters input[type='date'] {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4caf50;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #9e9e9e;
|
||||
color: white;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #757575;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffebee;
|
||||
color: #c62828;
|
||||
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #ddd;
|
||||
margin: 20px 0;
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
@@ -382,29 +390,30 @@ h2 {
|
||||
border-bottom: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
transition: all 0.3s;
|
||||
color: var(--color-text-secondary);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
color: #4caf50;
|
||||
border-bottom-color: #4caf50;
|
||||
color: var(--color-accent);
|
||||
border-bottom-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.report-content {
|
||||
background: white;
|
||||
background: var(--color-surface);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.report-content h3 {
|
||||
margin-bottom: 20px;
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
canvas {
|
||||
@@ -412,6 +421,10 @@ canvas {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.report-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -422,38 +435,38 @@ canvas {
|
||||
.report-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.report-table th {
|
||||
background: #f5f5f5;
|
||||
background: var(--color-bg-secondary);
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.report-table tbody tr:hover {
|
||||
background: #f9f9f9;
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.report-table tfoot {
|
||||
font-weight: bold;
|
||||
background: #f5f5f5;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
color: #4caf50;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: #ffa500;
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: #f44336;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.status-canceled {
|
||||
color: #9e9e9e;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.stats {
|
||||
@@ -465,8 +478,8 @@ canvas {
|
||||
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
@@ -474,42 +487,54 @@ canvas {
|
||||
.stat-card h3 {
|
||||
font-size: 2em;
|
||||
margin: 0 0 10px 0;
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.stat-card p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.stat-card.approved {
|
||||
background: #e8f5e9;
|
||||
border-color: #4caf50;
|
||||
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.stat-card.approved h3 {
|
||||
color: #4caf50;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.stat-card.rejected {
|
||||
background: #ffebee;
|
||||
border-color: #f44336;
|
||||
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.stat-card.rejected h3 {
|
||||
color: #f44336;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.breakdown {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.breakdown p {
|
||||
margin: 8px 0;
|
||||
font-size: 1.1em;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.breakdown strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,55 +19,57 @@
|
||||
<input type="date" v-model="filters.start_date" placeholder="Data început" />
|
||||
<input type="date" v-model="filters.end_date" placeholder="Data sfârșit" />
|
||||
|
||||
<button @click="loadLogs">Filtrează</button>
|
||||
<button @click="resetFilters">Resetează</button>
|
||||
<button @click="loadLogs" class="btn btn-primary">Filtrează</button>
|
||||
<button @click="resetFilters" class="btn btn-secondary">Resetează</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<p v-if="loading">Se încarcă...</p>
|
||||
<p v-if="loading" class="loading-text">Se încarcă...</p>
|
||||
|
||||
<!-- Error state -->
|
||||
<p v-else-if="error" class="error">{{ error }}</p>
|
||||
|
||||
<!-- Table -->
|
||||
<table v-else-if="logs.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Utilizator</th>
|
||||
<th>Acțiune</th>
|
||||
<th>Tip Target</th>
|
||||
<th>ID Target</th>
|
||||
<th>Detalii</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs" :key="log.id">
|
||||
<td>{{ formatDate(log.created_at) }}</td>
|
||||
<td>
|
||||
<div>{{ log.user_name }}</div>
|
||||
<small>{{ log.user_email }}</small>
|
||||
</td>
|
||||
<td>{{ formatAction(log.action) }}</td>
|
||||
<td>{{ log.target_type }}</td>
|
||||
<td>{{ log.target_id }}</td>
|
||||
<td>
|
||||
<pre v-if="log.details && Object.keys(log.details).length > 0">{{
|
||||
formatDetails(log.details)
|
||||
}}</pre>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-else-if="logs.length > 0" class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Utilizator</th>
|
||||
<th>Acțiune</th>
|
||||
<th>Tip Target</th>
|
||||
<th>ID Target</th>
|
||||
<th>Detalii</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="log in logs" :key="log.id">
|
||||
<td>{{ formatDate(log.created_at) }}</td>
|
||||
<td>
|
||||
<div>{{ log.user_name }}</div>
|
||||
<small>{{ log.user_email }}</small>
|
||||
</td>
|
||||
<td>{{ formatAction(log.action) }}</td>
|
||||
<td>{{ log.target_type }}</td>
|
||||
<td>{{ log.target_id }}</td>
|
||||
<td>
|
||||
<pre v-if="log.details && Object.keys(log.details).length > 0">{{
|
||||
formatDetails(log.details)
|
||||
}}</pre>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p v-else>Nu există înregistrări în jurnal.</p>
|
||||
<p v-else class="empty-text">Nu există înregistrări în jurnal.</p>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination" v-if="logs.length > 0">
|
||||
<button @click="prevPage" :disabled="page === 1">Anterior</button>
|
||||
<button @click="prevPage" :disabled="page === 1" class="btn btn-secondary">Anterior</button>
|
||||
<span>Pagina {{ page }}</span>
|
||||
<button @click="nextPage" :disabled="logs.length < limit">Următor</button>
|
||||
<button @click="nextPage" :disabled="logs.length < limit" class="btn btn-secondary">Următor</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -158,14 +160,9 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.audit-log {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filters {
|
||||
@@ -179,81 +176,114 @@ h2 {
|
||||
.filters select,
|
||||
.filters input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filters button {
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.filters button:hover {
|
||||
background-color: #45a049;
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.filters button:last-child {
|
||||
background-color: #9e9e9e;
|
||||
.btn-primary {
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filters button:last-child:hover {
|
||||
background-color: #757575;
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f44336;
|
||||
color: var(--color-danger);
|
||||
padding: 10px;
|
||||
background-color: #ffebee;
|
||||
border-radius: 4px;
|
||||
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
table {
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: #f5f5f5;
|
||||
.data-table thead {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
th {
|
||||
.data-table th {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
td {
|
||||
.data-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
td small {
|
||||
color: #757575;
|
||||
.data-table td small {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
overflow-x: auto;
|
||||
max-width: 300px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
@@ -264,25 +294,20 @@ pre {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
padding: 8px 16px;
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination button:hover:not(:disabled) {
|
||||
background-color: #1976d2;
|
||||
}
|
||||
|
||||
.pagination button:disabled {
|
||||
background-color: #e0e0e0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pagination span {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.filters {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filters select,
|
||||
.filters input,
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,126 +11,72 @@
|
||||
<!-- Dashboard Content -->
|
||||
<div v-else class="dashboard-content">
|
||||
<!-- Quick Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-total">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<CollapsibleSection title="Quick Stats" :icon="BarChart3">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-total">
|
||||
<Calendar :size="28" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.total }}</h3>
|
||||
<p>Total Bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.total }}</h3>
|
||||
<p>Total Bookings</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-pending">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-pending">
|
||||
<Clock :size="28" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.pending }}</h3>
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.pending }}</h3>
|
||||
<p>Pending</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-approved">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon stat-icon-approved">
|
||||
<CheckCircle :size="28" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.approved }}</h3>
|
||||
<p>Approved</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ stats.approved }}</h3>
|
||||
<p>Approved</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin: Pending Requests -->
|
||||
<div v-if="isAdmin" class="stat-card">
|
||||
<div class="stat-icon stat-icon-admin">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ adminStats.pendingRequests }}</h3>
|
||||
<p>Pending Requests</p>
|
||||
<!-- Admin: Pending Requests -->
|
||||
<div v-if="isAdmin" class="stat-card">
|
||||
<div class="stat-icon stat-icon-admin">
|
||||
<Users :size="28" />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ adminStats.pendingRequests }}</h3>
|
||||
<p>Pending Requests</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card quick-actions">
|
||||
<h3>Quick Actions</h3>
|
||||
<div class="actions-grid">
|
||||
<router-link to="/spaces" class="action-btn">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Search :size="24" class="action-icon" />
|
||||
<span>Book a Space</span>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/my-bookings" class="action-btn">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
|
||||
/>
|
||||
</svg>
|
||||
<ClipboardList :size="24" class="action-icon" />
|
||||
<span>My Bookings</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="isAdmin" to="/admin/bookings" class="action-btn">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||
/>
|
||||
</svg>
|
||||
<ClipboardCheck :size="24" class="action-icon" />
|
||||
<span>Manage Bookings</span>
|
||||
</router-link>
|
||||
|
||||
<router-link v-if="isAdmin" to="/admin/spaces" class="action-btn">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<Building2 :size="24" class="action-icon" />
|
||||
<span>Manage Spaces</span>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -138,21 +84,13 @@
|
||||
|
||||
<div class="content-grid">
|
||||
<!-- Upcoming Bookings -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Upcoming Bookings</h3>
|
||||
<CollapsibleSection title="Upcoming Bookings" :icon="CalendarDays">
|
||||
<template #header-actions>
|
||||
<router-link to="/my-bookings" class="link">View All</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="upcomingBookings.length === 0" class="empty-state-small">
|
||||
<svg class="icon-empty" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<Calendar :size="48" class="icon-empty" />
|
||||
<p>No upcoming bookings</p>
|
||||
</div>
|
||||
|
||||
@@ -163,14 +101,7 @@
|
||||
class="booking-item"
|
||||
>
|
||||
<div class="booking-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<Calendar :size="20" />
|
||||
</div>
|
||||
<div class="booking-info">
|
||||
<h4>{{ booking.title }}</h4>
|
||||
@@ -184,24 +115,16 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Available Spaces -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3>Available Spaces</h3>
|
||||
<CollapsibleSection title="Available Spaces" :icon="Building2">
|
||||
<template #header-actions>
|
||||
<router-link to="/spaces" class="link">View All</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="availableSpaces.length === 0" class="empty-state-small">
|
||||
<svg class="icon-empty" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<Building2 :size="48" class="icon-empty" />
|
||||
<p>No available spaces</p>
|
||||
</div>
|
||||
|
||||
@@ -213,14 +136,7 @@
|
||||
class="space-item"
|
||||
>
|
||||
<div class="space-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<Building2 :size="20" />
|
||||
</div>
|
||||
<div class="space-info">
|
||||
<h4>{{ space.name }}</h4>
|
||||
@@ -228,25 +144,17 @@
|
||||
{{ formatType(space.type) }} · Capacity: {{ space.capacity }}
|
||||
</p>
|
||||
</div>
|
||||
<svg class="icon-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronRight :size="20" class="icon-chevron" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
|
||||
<!-- Admin: Recent Audit Logs -->
|
||||
<div v-if="isAdmin" class="card">
|
||||
<div class="card-header">
|
||||
<h3>Recent Activity</h3>
|
||||
<CollapsibleSection v-if="isAdmin" title="Recent Activity" :icon="ScrollText">
|
||||
<template #header-actions>
|
||||
<router-link to="/admin/audit-log" class="link">View All</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="recentAuditLogs.length === 0" class="empty-state-small">
|
||||
<p>No recent activity</p>
|
||||
@@ -255,14 +163,7 @@
|
||||
<div v-else class="audit-list">
|
||||
<div v-for="log in recentAuditLogs" :key="log.id" class="audit-item">
|
||||
<div class="audit-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<FileText :size="20" />
|
||||
</div>
|
||||
<div class="audit-info">
|
||||
<p class="audit-action">{{ log.action }}</p>
|
||||
@@ -271,7 +172,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
@@ -421,14 +333,14 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
@@ -444,21 +356,21 @@ onMounted(() => {
|
||||
.dashboard-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
@@ -467,53 +379,49 @@ onMounted(() => {
|
||||
.stat-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.stat-icon-total {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.stat-icon-pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.stat-icon-approved {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.stat-icon-admin {
|
||||
background: #e0e7ff;
|
||||
color: #3730a3;
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.stat-content h3 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-content p {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Quick Actions */
|
||||
.quick-actions h3 {
|
||||
margin-bottom: 20px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.actions-grid {
|
||||
@@ -527,23 +435,21 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
color: #374151;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #e5e7eb;
|
||||
background: var(--color-bg-tertiary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.action-btn .icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #3b82f6;
|
||||
.action-icon {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Content Grid */
|
||||
@@ -555,35 +461,23 @@ onMounted(() => {
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: #3b82f6;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: #2563eb;
|
||||
color: var(--color-accent-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -591,18 +485,17 @@ onMounted(() => {
|
||||
.empty-state-small {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.icon-empty {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 12px;
|
||||
color: #d1d5db;
|
||||
color: var(--color-border);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state-small p {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -610,38 +503,33 @@ onMounted(() => {
|
||||
.bookings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.booking-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
padding: 14px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.booking-item:hover {
|
||||
background: #f3f4f6;
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.booking-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.booking-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #3b82f6;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.booking-info {
|
||||
@@ -651,86 +539,53 @@ onMounted(() => {
|
||||
.booking-info h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.booking-space {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.booking-time {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.badge-rejected {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.badge-canceled {
|
||||
background: #e5e7eb;
|
||||
color: #4b5563;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Spaces List */
|
||||
.spaces-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.space-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
text-decoration: none;
|
||||
transition: background 0.2s;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
|
||||
.space-item:hover {
|
||||
background: #f3f4f6;
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
.space-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.space-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #3b82f6;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.space-info {
|
||||
@@ -740,51 +595,44 @@ onMounted(() => {
|
||||
.space-info h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.space-meta {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.icon-chevron {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Audit List */
|
||||
.audit-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.audit-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.audit-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.audit-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.audit-info {
|
||||
@@ -794,19 +642,19 @@ onMounted(() => {
|
||||
.audit-action {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.audit-user {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.audit-time {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
|
||||
@@ -100,12 +100,12 @@ const handleLogin = 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;
|
||||
}
|
||||
|
||||
@@ -117,11 +117,11 @@ h2 {
|
||||
.register-link {
|
||||
text-align: center;
|
||||
margin-top: 1.5rem;
|
||||
color: #7f8c8d;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.register-link a {
|
||||
color: #3498db;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -132,15 +132,15 @@ h2 {
|
||||
.demo-accounts {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 0.9rem;
|
||||
color: #7f8c8d;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.demo-accounts p {
|
||||
@@ -150,8 +150,9 @@ h2 {
|
||||
.error {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #fee;
|
||||
border-left: 3px solid #e74c3c;
|
||||
border-radius: 4px;
|
||||
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);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.my-bookings {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
h2 {
|
||||
margin-bottom: 24px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
@@ -408,34 +402,35 @@ onMounted(() => {
|
||||
|
||||
.filter-group label {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
background: white;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.filter-group select: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);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -445,9 +440,9 @@ onMounted(() => {
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.bookings-table {
|
||||
@@ -459,20 +454,21 @@ onMounted(() => {
|
||||
.bookings-table th {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
background: var(--color-bg-secondary);
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.bookings-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
vertical-align: middle;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.bookings-table tr:hover {
|
||||
background: #f9fafb;
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.space-info {
|
||||
@@ -483,12 +479,12 @@ onMounted(() => {
|
||||
|
||||
.space-name {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.space-type {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -500,23 +496,23 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.badge-approved {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-rejected {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.badge-canceled {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -525,17 +521,17 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.no-action {
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.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);
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -546,21 +542,30 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
background: color-mix(in srgb, var(--color-danger) 85%, black);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@@ -591,18 +596,19 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@@ -613,14 +619,14 @@ onMounted(() => {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -628,7 +634,7 @@ onMounted(() => {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 400;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -647,17 +653,24 @@ onMounted(() => {
|
||||
.form-group 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;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group 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-group input:disabled {
|
||||
background: var(--color-bg-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
@@ -666,9 +679,9 @@ onMounted(() => {
|
||||
|
||||
.error-message {
|
||||
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;
|
||||
}
|
||||
@@ -679,15 +692,6 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6b7280;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* Mobile Cards */
|
||||
.bookings-cards {
|
||||
display: none;
|
||||
@@ -705,10 +709,10 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.booking-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
.booking-header {
|
||||
@@ -717,13 +721,13 @@ onMounted(() => {
|
||||
align-items: start;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.booking-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.booking-details {
|
||||
@@ -737,16 +741,17 @@ onMounted(() => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.booking-row .label {
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.booking-actions {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
<h2>Global Booking Settings</h2>
|
||||
|
||||
<!-- Settings Form -->
|
||||
<div class="card">
|
||||
<h3>Booking Rules Configuration</h3>
|
||||
|
||||
<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">
|
||||
@@ -108,25 +106,26 @@
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="success" class="success">{{ success }}</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="card info-card">
|
||||
<h4>About These Settings</h4>
|
||||
<ul>
|
||||
<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>
|
||||
</div>
|
||||
</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)
|
||||
@@ -163,7 +162,6 @@ const loadSettings = async () => {
|
||||
}
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
// Validate all fields are positive
|
||||
if (
|
||||
formData.value.min_duration_minutes <= 0 ||
|
||||
formData.value.max_duration_minutes <= 0 ||
|
||||
@@ -176,19 +174,16 @@ const validateForm = (): boolean => {
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate min < max duration
|
||||
if (formData.value.min_duration_minutes >= formData.value.max_duration_minutes) {
|
||||
error.value = 'Minimum duration must be less than maximum duration'
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate working hours start < end
|
||||
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
|
||||
}
|
||||
|
||||
// Validate working hours are within 0-23 for start and 1-24 for end
|
||||
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
|
||||
@@ -206,7 +201,6 @@ const handleSubmit = async () => {
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
|
||||
// Client-side validation
|
||||
if (!validateForm()) {
|
||||
return
|
||||
}
|
||||
@@ -239,21 +233,7 @@ onMounted(() => {
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h3, h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #444;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
@@ -276,18 +256,27 @@ h3, h4 {
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
padding: 0.5rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.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: #666;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@@ -300,19 +289,19 @@ h3, h4 {
|
||||
.btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4caf50;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #45a049;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -323,46 +312,48 @@ h3, h4 {
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: 0.75rem;
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 4px;
|
||||
color: #c33;
|
||||
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: #efe;
|
||||
border: 1px solid #cfc;
|
||||
border-radius: 4px;
|
||||
color: #3c3;
|
||||
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-success);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.info-card ul {
|
||||
.info-list {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card li {
|
||||
.info-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-list strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.note {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.collapsible-section + .collapsible-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -33,14 +33,7 @@
|
||||
<div class="space-meta">
|
||||
<span class="badge badge-type">{{ formatType(space.type) }}</span>
|
||||
<span class="badge badge-capacity">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Users :size="16" />
|
||||
Capacity: {{ space.capacity }}
|
||||
</span>
|
||||
<span :class="['badge', space.is_active ? 'badge-active' : 'badge-inactive']">
|
||||
@@ -53,14 +46,7 @@
|
||||
:disabled="!space.is_active"
|
||||
@click="handleReserve"
|
||||
>
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||
/>
|
||||
</svg>
|
||||
<Plus :size="18" />
|
||||
{{ showBookingForm ? 'Cancel Reservation' : 'Reserve Space' }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -99,6 +85,7 @@ import { useRoute } from 'vue-router'
|
||||
import { spacesApi, handleApiError } from '@/services/api'
|
||||
import SpaceCalendar from '@/components/SpaceCalendar.vue'
|
||||
import BookingForm from '@/components/BookingForm.vue'
|
||||
import { Users, Plus } from 'lucide-vue-next'
|
||||
import type { Space } from '@/types'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -132,7 +119,6 @@ const loadSpace = async () => {
|
||||
}
|
||||
|
||||
// Fetch all spaces and filter by ID
|
||||
// (Could be optimized with a dedicated GET /api/spaces/:id endpoint in the future)
|
||||
const spaces = await spacesApi.list()
|
||||
const foundSpace = spaces.find((s) => s.id === spaceId)
|
||||
|
||||
@@ -170,11 +156,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.space-detail {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Breadcrumbs */
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
@@ -182,26 +163,26 @@ onMounted(() => {
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.breadcrumbs a {
|
||||
color: #3b82f6;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.breadcrumbs a:hover {
|
||||
color: #2563eb;
|
||||
color: var(--color-accent-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumbs .separator {
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.breadcrumbs .current {
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -212,14 +193,14 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
@@ -239,21 +220,22 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.error-card h3 {
|
||||
color: #991b1b;
|
||||
color: var(--color-danger);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-card p {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -281,7 +263,7 @@ onMounted(() => {
|
||||
.header-info h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@@ -303,42 +285,37 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.badge-type {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
background: var(--color-accent-light);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.badge-capacity {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Reserve Button */
|
||||
/* Buttons */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -348,14 +325,14 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
background: var(--color-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.btn-reserve {
|
||||
@@ -365,33 +342,29 @@ onMounted(() => {
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Description Card */
|
||||
.description-card p {
|
||||
color: #4b5563;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Booking Card */
|
||||
.booking-card h3 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Calendar Card */
|
||||
.calendar-subtitle {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@@ -411,14 +384,14 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
|
||||
@@ -42,14 +42,7 @@
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="filteredSpaces.length === 0" class="empty-state">
|
||||
<svg class="empty-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||||
/>
|
||||
</svg>
|
||||
<Building2 :size="80" class="empty-icon" />
|
||||
<h3>No Spaces Found</h3>
|
||||
<p>{{ selectedType || selectedStatus ? 'Try adjusting your filters' : 'No spaces are currently available' }}</p>
|
||||
</div>
|
||||
@@ -72,27 +65,13 @@
|
||||
<div class="space-card-body">
|
||||
<div class="space-info">
|
||||
<div class="info-item">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
/>
|
||||
</svg>
|
||||
<Tag :size="18" class="info-icon" />
|
||||
<span class="label">Type:</span>
|
||||
<span class="value">{{ formatType(space.type) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<svg class="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
/>
|
||||
</svg>
|
||||
<Users :size="18" class="info-icon" />
|
||||
<span class="label">Capacity:</span>
|
||||
<span class="value">{{ space.capacity }} {{ space.capacity === 1 ? 'person' : 'people' }}</span>
|
||||
</div>
|
||||
@@ -106,14 +85,7 @@
|
||||
<div class="space-card-footer">
|
||||
<button class="btn btn-secondary">
|
||||
View Details
|
||||
<svg class="icon-small" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronRight :size="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -125,6 +97,7 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { spacesApi, handleApiError } from '@/services/api'
|
||||
import { Building2, Tag, Users, ChevronRight } from 'lucide-vue-next'
|
||||
import type { Space } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -194,11 +167,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.spaces {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.spaces-header {
|
||||
display: flex;
|
||||
@@ -210,12 +178,12 @@ onMounted(() => {
|
||||
.spaces-header h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -236,28 +204,27 @@ onMounted(() => {
|
||||
.filter-group label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.filter-select:hover {
|
||||
border-color: #9ca3af;
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
@@ -267,14 +234,14 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80px 20px;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
@@ -288,22 +255,23 @@ onMounted(() => {
|
||||
|
||||
/* Error State */
|
||||
.error-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
.error-card h3 {
|
||||
color: #991b1b;
|
||||
color: var(--color-danger);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.error-card p {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -314,21 +282,20 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #d1d5db;
|
||||
color: var(--color-border);
|
||||
margin: 0 auto 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -341,19 +308,20 @@ onMounted(() => {
|
||||
|
||||
/* Space Card */
|
||||
.space-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.space-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.space-card-header {
|
||||
@@ -367,7 +335,7 @@ onMounted(() => {
|
||||
.space-card-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
color: var(--color-text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -375,20 +343,20 @@ onMounted(() => {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.space-card-body {
|
||||
@@ -408,27 +376,25 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #4b5563;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #9ca3af;
|
||||
.info-icon {
|
||||
color: var(--color-text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.space-description {
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-top: 12px;
|
||||
@@ -446,34 +412,29 @@ onMounted(() => {
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3b82f6;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2563eb;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
background: var(--color-bg-secondary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.icon-small {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--color-bg-tertiary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<h2>User Profile</h2>
|
||||
|
||||
<!-- Profile Information Card -->
|
||||
<div class="card">
|
||||
<h3>Profile Information</h3>
|
||||
<CollapsibleSection title="Profile Information" :icon="UserIcon">
|
||||
<div v-if="user" class="profile-info">
|
||||
<div class="info-item">
|
||||
<label>Email:</label>
|
||||
@@ -23,12 +22,10 @@
|
||||
<span>{{ user.role }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Timezone Preferences Card -->
|
||||
<div class="card">
|
||||
<h3>Timezone Preferences</h3>
|
||||
|
||||
<CollapsibleSection title="Timezone Preferences" :icon="Globe">
|
||||
<div v-if="loadingTimezones" class="loading">Loading timezones...</div>
|
||||
|
||||
<div v-else class="timezone-settings">
|
||||
@@ -56,18 +53,16 @@
|
||||
<div v-if="timezoneSuccess" class="success">{{ timezoneSuccess }}</div>
|
||||
<div v-if="timezoneError" class="error">{{ timezoneError }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Google Calendar Integration Card -->
|
||||
<div class="card">
|
||||
<h3>Google Calendar Integration</h3>
|
||||
|
||||
<CollapsibleSection title="Google Calendar Integration" :icon="CalendarDays">
|
||||
<div v-if="loadingGoogleStatus" class="loading">Checking connection status...</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="googleStatus.connected" class="google-connected">
|
||||
<div class="status-indicator">
|
||||
<span class="status-icon">✓</span>
|
||||
<CheckCircle :size="20" class="status-icon-connected" />
|
||||
<span>Connected to Google Calendar</span>
|
||||
</div>
|
||||
|
||||
@@ -103,12 +98,11 @@
|
||||
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-if="success" class="success">{{ success }}</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="card info-card">
|
||||
<h4>About Calendar Integration</h4>
|
||||
<ul>
|
||||
<CollapsibleSection title="About Calendar Integration" :icon="Info">
|
||||
<ul class="info-list">
|
||||
<li>
|
||||
<strong>Automatic Sync:</strong> When your booking is approved, it's automatically added to
|
||||
your Google Calendar
|
||||
@@ -122,7 +116,7 @@
|
||||
above
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -131,6 +125,8 @@ import { ref, computed, onMounted } from 'vue'
|
||||
import { usersApi, googleCalendarApi, handleApiError } from '@/services/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatDateTime as formatDateTimeUtil } from '@/utils/datetime'
|
||||
import CollapsibleSection from '@/components/CollapsibleSection.vue'
|
||||
import { User as UserIcon, Globe, CalendarDays, CheckCircle, Info } from 'lucide-vue-next'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
@@ -184,7 +180,6 @@ const updateTimezone = async () => {
|
||||
try {
|
||||
await usersApi.updateTimezone(selectedTimezone.value)
|
||||
|
||||
// Update auth store
|
||||
if (authStore.user) {
|
||||
authStore.user.timezone = selectedTimezone.value
|
||||
}
|
||||
@@ -195,7 +190,6 @@ const updateTimezone = async () => {
|
||||
}, 5000)
|
||||
} catch (err) {
|
||||
timezoneError.value = handleApiError(err)
|
||||
// Revert selection on error
|
||||
if (user.value) {
|
||||
selectedTimezone.value = user.value.timezone
|
||||
}
|
||||
@@ -223,21 +217,17 @@ const connectGoogle = async () => {
|
||||
try {
|
||||
const response = await googleCalendarApi.connect()
|
||||
|
||||
// Open OAuth URL in popup window
|
||||
const popup = window.open(
|
||||
response.authorization_url,
|
||||
'Google Calendar Authorization',
|
||||
'width=600,height=600,toolbar=no,menubar=no,location=no'
|
||||
)
|
||||
|
||||
// Poll for connection status
|
||||
const pollInterval = setInterval(async () => {
|
||||
// Check if popup was closed
|
||||
if (popup && popup.closed) {
|
||||
clearInterval(pollInterval)
|
||||
connecting.value = false
|
||||
|
||||
// Check status one more time
|
||||
await checkGoogleStatus()
|
||||
|
||||
if (googleStatus.value.connected) {
|
||||
@@ -247,7 +237,6 @@ const connectGoogle = async () => {
|
||||
}, 3000)
|
||||
}
|
||||
} else {
|
||||
// Poll for connection status
|
||||
try {
|
||||
const status = await googleCalendarApi.status()
|
||||
if (status.connected) {
|
||||
@@ -256,7 +245,6 @@ const connectGoogle = async () => {
|
||||
googleStatus.value = status
|
||||
success.value = 'Google Calendar connected successfully!'
|
||||
|
||||
// Close popup
|
||||
if (popup) {
|
||||
popup.close()
|
||||
}
|
||||
@@ -271,7 +259,6 @@ const connectGoogle = async () => {
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
// Stop polling after 5 minutes
|
||||
setTimeout(() => {
|
||||
clearInterval(pollInterval)
|
||||
connecting.value = false
|
||||
@@ -328,22 +315,7 @@ onMounted(() => {
|
||||
|
||||
h2 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h3,
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #444;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.profile-info {
|
||||
@@ -360,17 +332,17 @@ h4 {
|
||||
.info-item label {
|
||||
font-weight: 600;
|
||||
min-width: 120px;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-item span {
|
||||
color: #333;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.google-connected,
|
||||
@@ -386,29 +358,21 @@ h4 {
|
||||
gap: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #4caf50;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 0.9rem;
|
||||
.status-icon-connected {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.expiry-info {
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -420,35 +384,35 @@ h4 {
|
||||
|
||||
.benefits-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.6rem 1.2rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
transition: all var(--transition-fast);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4285f4;
|
||||
background: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #357ae8;
|
||||
background: var(--color-accent-hover);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background-color: #c82333;
|
||||
background: color-mix(in srgb, var(--color-danger) 85%, black);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@@ -458,34 +422,32 @@ h4 {
|
||||
|
||||
.error {
|
||||
padding: 0.75rem;
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 4px;
|
||||
color: #c33;
|
||||
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: #efe;
|
||||
border: 1px solid #cfc;
|
||||
border-radius: 4px;
|
||||
color: #3c3;
|
||||
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-success);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.info-card ul {
|
||||
.info-list {
|
||||
margin: 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card li {
|
||||
.info-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-list strong {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.timezone-settings {
|
||||
@@ -502,16 +464,18 @@ h4 {
|
||||
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.timezone-select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 1rem;
|
||||
max-width: 400px;
|
||||
cursor: pointer;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.timezone-select:disabled {
|
||||
@@ -520,9 +484,13 @@ h4 {
|
||||
}
|
||||
|
||||
.help-text {
|
||||
color: #666;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.collapsible-section + .collapsible-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
<div class="page-header">
|
||||
<h2>Admin Dashboard - User Management</h2>
|
||||
<button class="btn btn-primary" @click="openCreateModal">
|
||||
<UserPlus :size="16" />
|
||||
Create New User
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card">
|
||||
<h3>Filters</h3>
|
||||
<CollapsibleSection title="Filters" :icon="Filter">
|
||||
<div class="filters">
|
||||
<div class="form-group">
|
||||
<label for="filter-role">Filter by Role</label>
|
||||
@@ -31,68 +31,69 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Users List -->
|
||||
<div class="card">
|
||||
<h3>All Users</h3>
|
||||
<CollapsibleSection title="All Users" :icon="UsersIcon">
|
||||
<div v-if="loadingUsers" class="loading">Loading users...</div>
|
||||
<div v-else-if="users.length === 0" class="empty">
|
||||
No users found. {{ filterRole || filterOrganization ? 'Try different filters.' : 'Create one above!' }}
|
||||
</div>
|
||||
<table v-else class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Full Name</th>
|
||||
<th>Role</th>
|
||||
<th>Organization</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.full_name }}</td>
|
||||
<td>
|
||||
<span :class="['badge', user.role === 'admin' ? 'badge-admin' : 'badge-user']">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ user.organization || '-' }}</td>
|
||||
<td>
|
||||
<span :class="['badge', user.is_active ? 'badge-active' : 'badge-inactive']">
|
||||
{{ user.is_active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="startEdit(user)"
|
||||
:disabled="loading"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
:class="['btn', 'btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
|
||||
@click="toggleStatus(user)"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ user.is_active ? 'Deactivate' : 'Activate' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="showResetPassword(user)"
|
||||
:disabled="loading"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div v-else class="table-responsive">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Full Name</th>
|
||||
<th>Role</th>
|
||||
<th>Organization</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.full_name }}</td>
|
||||
<td>
|
||||
<span :class="['badge', user.role === 'admin' ? 'badge-admin' : 'badge-user']">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ user.organization || '-' }}</td>
|
||||
<td>
|
||||
<span :class="['badge', user.is_active ? 'badge-active' : 'badge-inactive']">
|
||||
{{ user.is_active ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="actions">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary"
|
||||
@click="startEdit(user)"
|
||||
:disabled="loading"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
:class="['btn', 'btn-sm', user.is_active ? 'btn-warning' : 'btn-success']"
|
||||
@click="toggleStatus(user)"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ user.is_active ? 'Deactivate' : 'Activate' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="showResetPassword(user)"
|
||||
:disabled="loading"
|
||||
>
|
||||
Reset Password
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
<!-- Create/Edit User Modal -->
|
||||
<div v-if="showFormModal" class="modal" @click.self="closeFormModal">
|
||||
@@ -200,6 +201,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { usersApi, handleApiError } from '@/services/api'
|
||||
import CollapsibleSection from '@/components/CollapsibleSection.vue'
|
||||
import { Users as UsersIcon, UserPlus, Filter } from 'lucide-vue-next'
|
||||
import type { User } from '@/types'
|
||||
|
||||
const users = ref<User[]>([])
|
||||
@@ -367,11 +370,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -383,20 +381,7 @@ onMounted(() => {
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.filters {
|
||||
@@ -405,6 +390,12 @@ onMounted(() => {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -413,28 +404,31 @@ onMounted(() => {
|
||||
|
||||
.form-group label {
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group 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-group input:disabled {
|
||||
background: #f3f4f6;
|
||||
background: var(--color-bg-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -445,13 +439,16 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
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 {
|
||||
@@ -460,48 +457,48 @@ 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;
|
||||
color: white;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #4b5563;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #10b981;
|
||||
background: var(--color-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover:not(:disabled) {
|
||||
background: #059669;
|
||||
background: color-mix(in srgb, var(--color-success) 85%, black);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #f59e0b;
|
||||
background: var(--color-warning);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover:not(:disabled) {
|
||||
background: #d97706;
|
||||
background: color-mix(in srgb, var(--color-warning) 85%, black);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
background: color-mix(in srgb, var(--color-danger) 85%, black);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
@@ -511,53 +508,58 @@ onMounted(() => {
|
||||
|
||||
.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-top: 12px;
|
||||
}
|
||||
|
||||
.success {
|
||||
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-top: 12px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #9ca3af;
|
||||
color: var(--color-text-muted);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.users-table th {
|
||||
.data-table th {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
background: #f9fafb;
|
||||
background: var(--color-bg-secondary);
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
color: var(--color-text-primary);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
.users-table td {
|
||||
.data-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.users-table tr:hover {
|
||||
background: #f9fafb;
|
||||
.data-table tr:hover {
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -569,23 +571,23 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.badge-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
background: color-mix(in srgb, var(--color-success) 15%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
background: color-mix(in srgb, var(--color-accent) 15%, transparent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.badge-user {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -608,15 +610,31 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
margin-bottom: 16px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.collapsible-section + .collapsible-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -129,8 +129,8 @@ const resendVerification = async () => {
|
||||
.spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #3498db;
|
||||
border: 4px solid var(--color-border);
|
||||
border-top: 4px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1.5rem;
|
||||
@@ -153,7 +153,7 @@ const resendVerification = async () => {
|
||||
.icon-success {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #28a745;
|
||||
background: var(--color-success);
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
line-height: 80px;
|
||||
@@ -164,7 +164,7 @@ const resendVerification = async () => {
|
||||
.icon-error {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #e74c3c;
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
line-height: 80px;
|
||||
@@ -173,17 +173,17 @@ const resendVerification = async () => {
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #7f8c8d;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #c0392b;
|
||||
color: var(--color-danger);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ p {
|
||||
}
|
||||
|
||||
.login-link a {
|
||||
color: #3498db;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -203,13 +203,13 @@ p {
|
||||
.resend-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #e1e8ed;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.resend-section > p {
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 500;
|
||||
color: #2c3e50;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.resend-form {
|
||||
@@ -221,22 +221,25 @@ p {
|
||||
.resend-input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.resend-input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 15%, transparent);
|
||||
}
|
||||
|
||||
.resend-message {
|
||||
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);
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user