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:
370
frontend/src/services/api.ts
Normal file
370
frontend/src/services/api.ts
Normal 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
|
||||
Reference in New Issue
Block a user