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:
Claude Agent
2026-02-11 21:27:05 +00:00
parent 9c2846cf00
commit 0bf3e6a7e2
28 changed files with 1960 additions and 1641 deletions

View File

@@ -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>