137 lines
6.4 KiB
Python
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)
|