Refactor to manager-centric model, add team calendar for all users
Key changes: - Removed office-centric model (deleted offices.py, office-rules) - Renamed to team-rules, managers are part of their own team - Team calendar visible to all (read-only for employees) - Admins can have a manager assigned
This commit is contained in:
@@ -61,14 +61,14 @@ def authenticate_user(db: Session, email: str, password: str) -> User | None:
|
||||
return user
|
||||
|
||||
|
||||
def create_user(db: Session, email: str, password: str, name: str, office_id: str = None, role: str = "employee") -> User:
|
||||
def create_user(db: Session, email: str, password: str, name: str, manager_id: str = None, role: str = "employee") -> User:
|
||||
"""Create a new user"""
|
||||
user = User(
|
||||
id=str(uuid.uuid4()),
|
||||
email=email,
|
||||
password_hash=hash_password(password),
|
||||
name=name,
|
||||
office_id=office_id,
|
||||
manager_id=manager_id,
|
||||
role=role,
|
||||
created_at=datetime.utcnow().isoformat(),
|
||||
updated_at=datetime.utcnow().isoformat()
|
||||
|
||||
@@ -21,7 +21,7 @@ import uuid
|
||||
|
||||
from database.models import (
|
||||
User, UserPresence, DailyParkingAssignment,
|
||||
NotificationLog, NotificationQueue, OfficeMembership
|
||||
NotificationLog, NotificationQueue
|
||||
)
|
||||
from services.parking import get_spot_display_name
|
||||
|
||||
|
||||
@@ -6,31 +6,19 @@ Key concepts:
|
||||
- Managers own parking spots (defined by manager_parking_quota)
|
||||
- Each manager has a spot prefix (A, B, C...) for display names
|
||||
- Spots are named like A1, A2, B1, B2 based on manager prefix
|
||||
- Fairness: users with lowest parking_days/office_days ratio get priority
|
||||
- Fairness: users with lowest parking_days/presence_days ratio get priority
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import func, or_
|
||||
|
||||
from database.models import (
|
||||
DailyParkingAssignment, User, OfficeMembership, UserPresence,
|
||||
DailyParkingAssignment, User, UserPresence,
|
||||
ParkingGuarantee, ParkingExclusion, ManagerClosingDay, ManagerWeeklyClosingDay
|
||||
)
|
||||
|
||||
|
||||
def get_manager_for_office(office_id: str, db: Session) -> User | None:
|
||||
"""Find the manager responsible for an office"""
|
||||
membership = db.query(OfficeMembership).filter(
|
||||
OfficeMembership.office_id == office_id
|
||||
).first()
|
||||
|
||||
if not membership:
|
||||
return None
|
||||
|
||||
return db.query(User).filter(User.id == membership.user_id).first()
|
||||
|
||||
|
||||
def get_spot_prefix(manager: User, db: Session) -> str:
|
||||
"""Get the spot prefix for a manager (from manager_spot_prefix or auto-assign)"""
|
||||
if manager.manager_spot_prefix:
|
||||
@@ -136,23 +124,16 @@ def initialize_parking_pool(manager_id: str, quota: int, date: str, db: Session)
|
||||
|
||||
def get_user_parking_ratio(user_id: str, manager_id: str, db: Session) -> float:
|
||||
"""
|
||||
Calculate user's parking ratio: parking_days / office_days
|
||||
Calculate user's parking ratio: parking_days / presence_days
|
||||
Lower ratio = higher priority for next parking spot
|
||||
"""
|
||||
# Get offices managed by this manager
|
||||
managed_office_ids = [
|
||||
m.office_id for m in db.query(OfficeMembership).filter(
|
||||
OfficeMembership.user_id == manager_id
|
||||
).all()
|
||||
]
|
||||
|
||||
# Count days user was present (office_days)
|
||||
office_days = db.query(UserPresence).filter(
|
||||
# Count days user was present
|
||||
presence_days = db.query(UserPresence).filter(
|
||||
UserPresence.user_id == user_id,
|
||||
UserPresence.status == "present"
|
||||
).count()
|
||||
|
||||
if office_days == 0:
|
||||
if presence_days == 0:
|
||||
return 0.0 # New user, highest priority
|
||||
|
||||
# Count days user got parking
|
||||
@@ -161,7 +142,7 @@ def get_user_parking_ratio(user_id: str, manager_id: str, db: Session) -> float:
|
||||
DailyParkingAssignment.manager_id == manager_id
|
||||
).count()
|
||||
|
||||
return parking_days / office_days
|
||||
return parking_days / presence_days
|
||||
|
||||
|
||||
def is_user_excluded(user_id: str, manager_id: str, date: str, db: Session) -> bool:
|
||||
@@ -206,19 +187,16 @@ def get_users_wanting_parking(manager_id: str, date: str, db: Session) -> list[d
|
||||
"""
|
||||
Get all users who want parking for this date, sorted by fairness priority.
|
||||
Returns list of {user_id, has_guarantee, ratio}
|
||||
"""
|
||||
# Get offices managed by this manager
|
||||
managed_office_ids = [
|
||||
m.office_id for m in db.query(OfficeMembership).filter(
|
||||
OfficeMembership.user_id == manager_id
|
||||
).all()
|
||||
]
|
||||
|
||||
# Get users who marked "present" for this date and belong to managed offices
|
||||
Note: Manager is part of their own team and can get parking from their pool.
|
||||
"""
|
||||
# Get users who marked "present" for this date:
|
||||
# - Users managed by this manager (User.manager_id == manager_id)
|
||||
# - The manager themselves (User.id == manager_id)
|
||||
present_users = db.query(UserPresence).join(User).filter(
|
||||
UserPresence.date == date,
|
||||
UserPresence.status == "present",
|
||||
User.office_id.in_(managed_office_ids)
|
||||
or_(User.manager_id == manager_id, User.id == manager_id)
|
||||
).all()
|
||||
|
||||
candidates = []
|
||||
@@ -316,18 +294,19 @@ def release_user_spot(manager_id: str, user_id: str, date: str, db: Session) ->
|
||||
return True
|
||||
|
||||
|
||||
def handle_presence_change(user_id: str, date: str, old_status: str, new_status: str, office_id: str, db: Session):
|
||||
def handle_presence_change(user_id: str, date: str, old_status: str, new_status: str, manager_id: str, db: Session):
|
||||
"""
|
||||
Handle presence status change and update parking accordingly.
|
||||
Uses fairness algorithm for assignment.
|
||||
manager_id is the user's manager (from User.manager_id).
|
||||
"""
|
||||
# Don't process past dates
|
||||
target_date = datetime.strptime(date, "%Y-%m-%d").date()
|
||||
if target_date < datetime.now().date():
|
||||
return
|
||||
|
||||
# Find manager for this office
|
||||
manager = get_manager_for_office(office_id, db)
|
||||
# Get manager
|
||||
manager = db.query(User).filter(User.id == manager_id, User.role == "manager").first()
|
||||
if not manager or not manager.manager_parking_quota:
|
||||
return
|
||||
|
||||
|
||||
Reference in New Issue
Block a user