feat: Space Booking System - MVP complet

Sistem web pentru rezervarea de birouri și săli de ședință
cu flux de aprobare administrativă.

Stack: FastAPI + Vue.js 3 + SQLite + TypeScript

Features implementate:
- Autentificare JWT + Self-registration cu email verification
- CRUD Spații, Utilizatori, Settings (Admin)
- Calendar interactiv (FullCalendar) cu drag-and-drop
- Creare rezervări cu validare (durată, program, overlap, max/zi)
- Rezervări recurente (săptămânal)
- Admin: aprobare/respingere/anulare cereri
- Admin: creare directă rezervări (bypass approval)
- Admin: editare orice rezervare
- User: editare/anulare rezervări proprii
- Notificări in-app (bell icon + dropdown)
- Notificări email (async SMTP cu BackgroundTasks)
- Jurnal acțiuni administrative (audit log)
- Rapoarte avansate (utilizare, top users, approval rate)
- Șabloane rezervări (booking templates)
- Atașamente fișiere (upload/download)
- Conflict warnings (verificare disponibilitate real-time)
- Integrare Google Calendar (OAuth2)
- Suport timezone (UTC storage + user preference)
- 225+ teste backend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-02-09 17:51:29 +00:00
commit df4031d99c
113 changed files with 24491 additions and 0 deletions

View File

@@ -0,0 +1,370 @@
import axios, { AxiosError } from 'axios'
import type {
LoginRequest,
TokenResponse,
UserRegister,
RegistrationResponse,
EmailVerificationRequest,
VerificationResponse,
Space,
User,
Settings,
Booking,
BookingCreate,
BookingUpdate,
BookingTemplate,
BookingTemplateCreate,
Notification,
AuditLog,
Attachment,
RecurringBookingCreate,
RecurringBookingResult,
SpaceUsageReport,
TopUsersReport,
ApprovalRateReport
} from '@/types'
const api = axios.create({
baseURL: '/api',
headers: {
'Content-Type': 'application/json'
}
})
// Add token to requests
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Auth API
export const authApi = {
login: async (credentials: LoginRequest): Promise<TokenResponse> => {
const response = await api.post<TokenResponse>('/auth/login', credentials)
return response.data
},
register: async (data: UserRegister): Promise<RegistrationResponse> => {
const response = await api.post<RegistrationResponse>('/auth/register', data)
return response.data
},
verifyEmail: async (data: EmailVerificationRequest): Promise<VerificationResponse> => {
const response = await api.post<VerificationResponse>('/auth/verify', data)
return response.data
},
resendVerification: async (email: string): Promise<{ message: string }> => {
const response = await api.post<{ message: string }>('/auth/resend-verification', null, {
params: { email }
})
return response.data
}
}
// Users API
export const usersApi = {
me: async (): Promise<User> => {
const response = await api.get<User>('/users/me')
return response.data
},
list: async (params?: { role?: string; organization?: string }): Promise<User[]> => {
const response = await api.get<User[]>('/admin/users', { params })
return response.data
},
create: async (
data: Omit<User, 'id' | 'is_active'> & { password: string }
): Promise<User> => {
const response = await api.post<User>('/admin/users', data)
return response.data
},
update: async (
id: number,
data: Partial<Omit<User, 'id' | 'is_active'>>
): Promise<User> => {
const response = await api.put<User>(`/admin/users/${id}`, data)
return response.data
},
updateStatus: async (id: number, is_active: boolean): Promise<User> => {
const response = await api.patch<User>(`/admin/users/${id}/status`, { is_active })
return response.data
},
resetPassword: async (id: number, new_password: string): Promise<User> => {
const response = await api.post<User>(`/admin/users/${id}/reset-password`, {
new_password
})
return response.data
},
getTimezones: async (): Promise<string[]> => {
const response = await api.get<string[]>('/users/timezones')
return response.data
},
updateTimezone: async (timezone: string): Promise<{ message: string; timezone: string }> => {
const response = await api.put<{ message: string; timezone: string }>('/users/me/timezone', {
timezone
})
return response.data
}
}
// Spaces API
export const spacesApi = {
list: async (): Promise<Space[]> => {
const response = await api.get<Space[]>('/spaces')
return response.data
},
create: async (data: Omit<Space, 'id' | 'is_active'>): Promise<Space> => {
const response = await api.post<Space>('/admin/spaces', data)
return response.data
},
update: async (id: number, data: Omit<Space, 'id' | 'is_active'>): Promise<Space> => {
const response = await api.put<Space>(`/admin/spaces/${id}`, data)
return response.data
},
updateStatus: async (id: number, is_active: boolean): Promise<Space> => {
const response = await api.patch<Space>(`/admin/spaces/${id}/status`, { is_active })
return response.data
}
}
// Settings API
export const settingsApi = {
get: async (): Promise<Settings> => {
const response = await api.get<Settings>('/admin/settings')
return response.data
},
update: async (data: Omit<Settings, 'id'>): Promise<Settings> => {
const response = await api.put<Settings>('/admin/settings', data)
return response.data
}
}
// Bookings API
export const bookingsApi = {
getForSpace: async (spaceId: number, start: string, end: string): Promise<Booking[]> => {
const response = await api.get<Booking[]>(`/spaces/${spaceId}/bookings`, {
params: { start, end }
})
return response.data
},
checkAvailability: async (params: {
space_id: number
start_datetime: string
end_datetime: string
}) => {
return api.get('/bookings/check-availability', { params })
},
create: async (data: BookingCreate): Promise<Booking> => {
const response = await api.post<Booking>('/bookings', data)
return response.data
},
getMy: async (status?: string): Promise<Booking[]> => {
const response = await api.get<Booking[]>('/bookings/my', {
params: status ? { status } : {}
})
return response.data
},
update: async (id: number, data: BookingUpdate): Promise<Booking> => {
const response = await api.put<Booking>(`/bookings/${id}`, data)
return response.data
},
createRecurring: async (data: RecurringBookingCreate): Promise<RecurringBookingResult> => {
const response = await api.post<RecurringBookingResult>('/bookings/recurring', data)
return response.data
}
}
// Admin Bookings API
export const adminBookingsApi = {
getPending: async (filters?: { space_id?: number; user_id?: number }): Promise<Booking[]> => {
const response = await api.get<Booking[]>('/admin/bookings/pending', { params: filters })
return response.data
},
approve: async (id: number): Promise<Booking> => {
const response = await api.put<Booking>(`/admin/bookings/${id}/approve`)
return response.data
},
reject: async (id: number, reason?: string): Promise<Booking> => {
const response = await api.put<Booking>(`/admin/bookings/${id}/reject`, { reason })
return response.data
},
update: async (id: number, data: BookingUpdate): Promise<Booking> => {
const response = await api.put<Booking>(`/admin/bookings/${id}`, data)
return response.data
},
reschedule: async (
id: number,
data: { start_datetime: string; end_datetime: string }
): Promise<Booking> => {
const response = await api.put<Booking>(`/admin/bookings/${id}/reschedule`, data)
return response.data
}
}
// Notifications API
export const notificationsApi = {
getAll: async (isRead?: boolean): Promise<Notification[]> => {
const params = isRead !== undefined ? { is_read: isRead } : {}
const response = await api.get<Notification[]>('/notifications', { params })
return response.data
},
markAsRead: async (id: number): Promise<Notification> => {
const response = await api.put<Notification>(`/notifications/${id}/read`)
return response.data
}
}
// Audit Log API
export const auditLogApi = {
getAll: async (params?: {
action?: string
start_date?: string
end_date?: string
page?: number
limit?: number
}): Promise<AuditLog[]> => {
const response = await api.get<AuditLog[]>('/admin/audit-log', { params })
return response.data
}
}
// Booking Templates API
export const bookingTemplatesApi = {
getAll: async (): Promise<BookingTemplate[]> => {
const response = await api.get<BookingTemplate[]>('/booking-templates')
return response.data
},
create: async (data: BookingTemplateCreate): Promise<BookingTemplate> => {
const response = await api.post<BookingTemplate>('/booking-templates', data)
return response.data
},
delete: async (id: number): Promise<void> => {
await api.delete(`/booking-templates/${id}`)
},
createBookingFromTemplate: async (
templateId: number,
startDatetime: string
): Promise<Booking> => {
const response = await api.post<Booking>(
`/booking-templates/from-template/${templateId}`,
null,
{ params: { start_datetime: startDatetime } }
)
return response.data
}
}
// Attachments API
export const attachmentsApi = {
upload: async (bookingId: number, file: File): Promise<Attachment> => {
const formData = new FormData()
formData.append('file', file)
const response = await api.post<Attachment>(`/bookings/${bookingId}/attachments`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
return response.data
},
list: async (bookingId: number): Promise<Attachment[]> => {
const response = await api.get<Attachment[]>(`/bookings/${bookingId}/attachments`)
return response.data
},
download: (attachmentId: number): string => {
return `/api/attachments/${attachmentId}/download`
},
delete: async (attachmentId: number): Promise<void> => {
await api.delete(`/attachments/${attachmentId}`)
}
}
// Reports API
export const reportsApi = {
getUsage: async (params?: {
start_date?: string
end_date?: string
space_id?: number
}): Promise<SpaceUsageReport> => {
const response = await api.get<SpaceUsageReport>('/admin/reports/usage', { params })
return response.data
},
getTopUsers: async (params?: {
start_date?: string
end_date?: string
limit?: number
}): Promise<TopUsersReport> => {
const response = await api.get<TopUsersReport>('/admin/reports/top-users', { params })
return response.data
},
getApprovalRate: async (params?: {
start_date?: string
end_date?: string
}): Promise<ApprovalRateReport> => {
const response = await api.get<ApprovalRateReport>('/admin/reports/approval-rate', {
params
})
return response.data
}
}
// Google Calendar API
export const googleCalendarApi = {
connect: async (): Promise<{ authorization_url: string; state: string }> => {
const response = await api.get<{ authorization_url: string; state: string }>(
'/integrations/google/connect'
)
return response.data
},
disconnect: async (): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>('/integrations/google/disconnect')
return response.data
},
status: async (): Promise<{ connected: boolean; expires_at: string | null }> => {
const response = await api.get<{ connected: boolean; expires_at: string | null }>(
'/integrations/google/status'
)
return response.data
}
}
// Helper to handle API errors
export const handleApiError = (error: unknown): string => {
if (error instanceof AxiosError) {
return error.response?.data?.detail || error.message
}
return 'An unexpected error occurred'
}
export default api