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:
11
backend/app/models/__init__.py
Normal file
11
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Models package."""
|
||||
from app.models.attachment import Attachment
|
||||
from app.models.audit_log import AuditLog
|
||||
from app.models.booking import Booking
|
||||
from app.models.booking_template import BookingTemplate
|
||||
from app.models.notification import Notification
|
||||
from app.models.settings import Settings
|
||||
from app.models.space import Space
|
||||
from app.models.user import User
|
||||
|
||||
__all__ = ["User", "Space", "Settings", "Booking", "BookingTemplate", "Notification", "AuditLog", "Attachment"]
|
||||
27
backend/app/models/attachment.py
Normal file
27
backend/app/models/attachment.py
Normal file
@@ -0,0 +1,27 @@
|
||||
"""Attachment model."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Attachment(Base):
|
||||
"""Attachment model for booking files."""
|
||||
|
||||
__tablename__ = "attachments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False, index=True)
|
||||
filename = Column(String(255), nullable=False) # Original filename
|
||||
stored_filename = Column(String(255), nullable=False) # UUID-based filename
|
||||
filepath = Column(String(500), nullable=False) # Full path
|
||||
size = Column(BigInteger, nullable=False) # File size in bytes
|
||||
content_type = Column(String(100), nullable=False)
|
||||
uploaded_by = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
booking = relationship("Booking", back_populates="attachments")
|
||||
uploader = relationship("User")
|
||||
24
backend/app/models/audit_log.py
Normal file
24
backend/app/models/audit_log.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""AuditLog model for tracking admin actions."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, JSON, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Audit log for tracking admin actions."""
|
||||
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
action = Column(String(100), nullable=False, index=True) # booking_approved, space_created, etc
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
target_type = Column(String(50), nullable=False, index=True) # booking, space, user, settings
|
||||
target_id = Column(Integer, nullable=False)
|
||||
details = Column(JSON, nullable=True) # Additional info (reasons, changed fields, etc)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="audit_logs")
|
||||
36
backend/app/models/booking.py
Normal file
36
backend/app/models/booking.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Booking model."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Booking(Base):
|
||||
"""Booking model for space reservations."""
|
||||
|
||||
__tablename__ = "bookings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
space_id = Column(Integer, ForeignKey("spaces.id"), nullable=False, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
start_datetime = Column(DateTime, nullable=False, index=True)
|
||||
end_datetime = Column(DateTime, nullable=False, index=True)
|
||||
status = Column(
|
||||
String, nullable=False, default="pending", index=True
|
||||
) # pending/approved/rejected/canceled
|
||||
rejection_reason = Column(String, nullable=True)
|
||||
cancellation_reason = Column(String, nullable=True)
|
||||
approved_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
google_calendar_event_id = Column(String, nullable=True) # Store Google Calendar event ID
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", foreign_keys=[user_id], backref="bookings")
|
||||
space = relationship("Space", foreign_keys=[space_id], backref="bookings")
|
||||
approver = relationship("User", foreign_keys=[approved_by], backref="approved_bookings")
|
||||
notifications = relationship("Notification", back_populates="booking")
|
||||
attachments = relationship("Attachment", back_populates="booking", cascade="all, delete-orphan")
|
||||
24
backend/app/models/booking_template.py
Normal file
24
backend/app/models/booking_template.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Booking template model."""
|
||||
from sqlalchemy import Column, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class BookingTemplate(Base):
|
||||
"""Booking template model for reusable booking configurations."""
|
||||
|
||||
__tablename__ = "booking_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
name = Column(String(200), nullable=False) # Template name
|
||||
space_id = Column(Integer, ForeignKey("spaces.id"), nullable=True) # Optional default space
|
||||
duration_minutes = Column(Integer, nullable=False) # Default duration
|
||||
title = Column(String(200), nullable=False) # Default title
|
||||
description = Column(Text, nullable=True) # Default description
|
||||
usage_count = Column(Integer, default=0) # Track usage
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="booking_templates")
|
||||
space = relationship("Space")
|
||||
26
backend/app/models/google_calendar_token.py
Normal file
26
backend/app/models/google_calendar_token.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Google Calendar Token model."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class GoogleCalendarToken(Base):
|
||||
"""Google Calendar OAuth token storage."""
|
||||
|
||||
__tablename__ = "google_calendar_tokens"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), unique=True, nullable=False)
|
||||
access_token = Column(Text, nullable=False)
|
||||
refresh_token = Column(Text, nullable=False)
|
||||
token_expiry = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(
|
||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
||||
)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="google_calendar_token")
|
||||
26
backend/app/models/notification.py
Normal file
26
backend/app/models/notification.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Notification model."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
"""Notification model for in-app notifications."""
|
||||
|
||||
__tablename__ = "notifications"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
type = Column(String(50), nullable=False) # booking_created, booking_approved, etc
|
||||
title = Column(String(200), nullable=False)
|
||||
message = Column(Text, nullable=False)
|
||||
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=True)
|
||||
is_read = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="notifications")
|
||||
booking = relationship("Booking", back_populates="notifications")
|
||||
18
backend/app/models/settings.py
Normal file
18
backend/app/models/settings.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Settings model."""
|
||||
from sqlalchemy import Column, Integer
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Settings(Base):
|
||||
"""Global settings model (singleton - only one row with id=1)."""
|
||||
|
||||
__tablename__ = "settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, default=1)
|
||||
min_duration_minutes = Column(Integer, nullable=False, default=30)
|
||||
max_duration_minutes = Column(Integer, nullable=False, default=480) # 8 hours
|
||||
working_hours_start = Column(Integer, nullable=False, default=8) # 8 AM
|
||||
working_hours_end = Column(Integer, nullable=False, default=20) # 8 PM
|
||||
max_bookings_per_day_per_user = Column(Integer, nullable=False, default=3)
|
||||
min_hours_before_cancel = Column(Integer, nullable=False, default=2)
|
||||
17
backend/app/models/space.py
Normal file
17
backend/app/models/space.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Space model."""
|
||||
from sqlalchemy import Boolean, Column, Integer, String
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Space(Base):
|
||||
"""Space model for offices and meeting rooms."""
|
||||
|
||||
__tablename__ = "spaces"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, nullable=False, index=True)
|
||||
type = Column(String, nullable=False) # "sala" or "birou"
|
||||
capacity = Column(Integer, nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
28
backend/app/models/user.py
Normal file
28
backend/app/models/user.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""User model."""
|
||||
from sqlalchemy import Boolean, Column, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""User model."""
|
||||
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
full_name = Column(String, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
role = Column(String, nullable=False, default="user") # "admin" or "user"
|
||||
organization = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
timezone = Column(String(50), default="UTC", nullable=False) # IANA timezone
|
||||
|
||||
# Relationships
|
||||
notifications = relationship("Notification", back_populates="user")
|
||||
audit_logs = relationship("AuditLog", back_populates="user")
|
||||
booking_templates = relationship("BookingTemplate", back_populates="user")
|
||||
google_calendar_token = relationship(
|
||||
"GoogleCalendarToken", back_populates="user", uselist=False
|
||||
)
|
||||
Reference in New Issue
Block a user