bmc_hub/app/modules/planner/backend/configuration.py

137 lines
6.4 KiB
Python

"""Planner settings and per-employee work schedules use the existing tables."""
from datetime import time
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, model_validator
from app.core.auth_dependencies import get_current_user
from app.core.database import atomic_transaction,execute_query,execute_query_single
from .work import require_schema,require_target,audit
router=APIRouter(dependencies=[Depends(require_schema)])
def require_admin(user):
if not (user.get('is_superadmin') or 'planner.admin' in set(user.get('permissions') or [])):
raise HTTPException(403,'Du må ikke ændre planlægningsindstillinger')
class ConfigurationInput(BaseModel):
appointment_warning_minutes:int=Field(default=30,ge=0,le=1440)
deadline_warning_days:int=Field(default=1,ge=0,le=30)
class TeamScopeInput(BaseModel):
user_ids:list[int]=Field(default_factory=list,max_length=1000)
@router.get('/team-scope/{editor}')
def team_scope(editor:int,user:dict=Depends(get_current_user)):
require_admin(user)
return {'user_ids':[r['target_user_id'] for r in execute_query('SELECT target_user_id FROM planner_team_scope WHERE editor_user_id=%s ORDER BY target_user_id',(editor,)) or []]}
@router.put('/team-scope/{editor}')
def update_team_scope(editor:int,payload:TeamScopeInput,user:dict=Depends(get_current_user)):
require_admin(user)
ids=sorted(set(payload.user_ids))
with atomic_transaction():
if not execute_query_single('SELECT user_id FROM users WHERE user_id=%s AND is_active FOR UPDATE',(editor,)):
raise HTTPException(400,'Vælg en aktiv medarbejder')
if len(execute_query('SELECT user_id FROM users WHERE user_id=ANY(%s) AND is_active',(ids,)) or [])!=len(ids):
raise HTTPException(400,'Vælg aktive medarbejdere')
old=team_scope(editor,user)
execute_query('DELETE FROM planner_team_scope WHERE editor_user_id=%s',(editor,),fetch=False)
for uid in ids:
execute_query('INSERT INTO planner_team_scope(editor_user_id,target_user_id) VALUES(%s,%s)',(editor,uid),fetch=False)
audit(user,'team_scope_update','team_scope',editor,old,{'user_ids':ids})
return {'user_ids':ids}
@router.get('/configuration')
def configuration(user:dict=Depends(get_current_user)):
require_target(user,int(user['id']))
row=execute_query_single('SELECT * FROM planner_settings WHERE id=1') or {}
row['can_manage']=bool(user.get('is_superadmin') or 'planner.admin' in set(user.get('permissions') or []))
return row
@router.put('/configuration')
def update_configuration(payload:ConfigurationInput,user:dict=Depends(get_current_user)):
require_admin(user)
with atomic_transaction():
old=execute_query_single('SELECT * FROM planner_settings WHERE id=1 FOR UPDATE')
row=execute_query_single('UPDATE planner_settings SET appointment_warning_minutes=%s,deadline_warning_days=%s,updated_by=%s,updated_at=NOW() WHERE id=1 RETURNING *',(payload.appointment_warning_minutes,payload.deadline_warning_days,user['id']))
audit(user,'settings_update','settings',1,old,row)
return row
class ActivityTypeInput(BaseModel):
label:str=Field(min_length=1,max_length=80)
active:bool=True
is_absence:bool=True
@router.put('/activity-types/{code}')
def update_activity_type(code:str,payload:ActivityTypeInput,user:dict=Depends(get_current_user)):
require_admin(user)
import re
if not code or len(code)>40 or not payload.label.strip():
raise HTTPException(400,'Ugyldig kode eller navn')
with atomic_transaction():
old=execute_query_single('SELECT * FROM planner_activity_types WHERE code=%s FOR UPDATE',(code,))
if not old and not re.fullmatch('[a-z][a-z_]{0,39}',code):
raise HTTPException(400,'Ugyldig kode til ny type')
if old and old['is_absence']!=payload.is_absence and execute_query_single('SELECT id FROM planner_absences WHERE kind=%s LIMIT 1',(code,)):
raise HTTPException(409,'En type med registreret fravær kan ikke ændres til en anden kategori. Opret en ny type.')
row=execute_query_single('''INSERT INTO planner_activity_types(code,label,active,is_absence) VALUES(%s,%s,%s,%s)
ON CONFLICT(code) DO UPDATE SET label=EXCLUDED.label,active=EXCLUDED.active,is_absence=EXCLUDED.is_absence RETURNING *''',
(code,payload.label.strip(),payload.active,payload.is_absence))
audit(user,'type_update','activity_type',0,old,row)
return row
class WorkDay(BaseModel):
weekday:int=Field(ge=0,le=6)
start_time:time
end_time:time
break_minutes:int=Field(default=0,ge=0,le=1440)
@model_validator(mode='after')
def valid(self):
minutes=(self.end_time.hour*60+self.end_time.minute)-(self.start_time.hour*60+self.start_time.minute)
if minutes<=self.break_minutes:
raise ValueError('Arbejdsperioden skal være længere end pausen')
return self
class ScheduleInput(BaseModel):
days:list[WorkDay]=Field(max_length=7)
@model_validator(mode='after')
def unique_days(self):
if len({d.weekday for d in self.days})!=len(self.days):
raise ValueError('Angiv kun én arbejdsperiode pr. ugedag')
return self
@router.get('/work-schedules/{target}')
def work_schedules(target:int,user:dict=Depends(get_current_user)):
require_target(user,target)
return execute_query('SELECT * FROM planner_work_schedules WHERE user_id=%s AND active ORDER BY weekday',(target,)) or []
@router.put('/work-schedules/{target}')
def update_work_schedules(target:int,payload:ScheduleInput,user:dict=Depends(get_current_user)):
require_admin(user)
require_target(user,target,True)
with atomic_transaction():
execute_query('SELECT user_id FROM users WHERE user_id=%s FOR UPDATE',(target,))
old=execute_query('SELECT * FROM planner_work_schedules WHERE user_id=%s AND active FOR UPDATE',(target,)) or []
execute_query('UPDATE planner_work_schedules SET active=false WHERE user_id=%s AND active',(target,),fetch=False)
for day in payload.days:
execute_query('INSERT INTO planner_work_schedules(user_id,weekday,start_time,end_time,break_minutes) VALUES(%s,%s,%s,%s,%s)',
(target,day.weekday,day.start_time,day.end_time,day.break_minutes),fetch=False)
audit(user,'schedule_update','work_schedule',target,old,payload.model_dump(mode='json'))
return work_schedules(target,user)