"""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)