4224 lines
135 KiB
Elm
4224 lines
135 KiB
Elm
port module Main exposing (..)
|
||
|
||
import Browser
|
||
import Dict exposing (Dict)
|
||
import Html exposing (..)
|
||
import Html.Attributes exposing (..)
|
||
import Html.Events exposing (..)
|
||
import Http
|
||
import Json.Decode as Decode exposing (Decoder, bool, field, float, int, list, string)
|
||
import Json.Encode as Encode
|
||
import Task
|
||
import Time
|
||
|
||
|
||
|
||
-- PORTS
|
||
|
||
|
||
port saveToken : Encode.Value -> Cmd msg
|
||
|
||
|
||
port removeToken : () -> Cmd msg
|
||
|
||
|
||
port confirmDelete : String -> Cmd msg
|
||
|
||
|
||
port confirmDeleteResponse : (Bool -> msg) -> Sub msg
|
||
|
||
|
||
|
||
-- MAIN
|
||
|
||
|
||
main : Program Flags Model Msg
|
||
main =
|
||
Browser.element
|
||
{ init = init
|
||
, update = update
|
||
, subscriptions = subscriptions
|
||
, view = view
|
||
}
|
||
|
||
|
||
|
||
-- FLAGS
|
||
|
||
|
||
type alias Flags =
|
||
{ token : Maybe String
|
||
, isAdmin : Bool
|
||
}
|
||
|
||
|
||
|
||
-- MODEL
|
||
|
||
|
||
type alias Model =
|
||
{ page : Page
|
||
, activeTab : AdminTab
|
||
, username : String
|
||
, password : String
|
||
, token : Maybe String
|
||
, isAdmin : Bool
|
||
, schedules : List Schedule
|
||
, users : List User
|
||
, timeEntries : List TimeEntry
|
||
, weeklyHours : List WeeklyHours
|
||
, yearlyHoursSummary : List YearlyHoursSummary
|
||
, selectedEntries : List SelectedEntry
|
||
, currentWeek : Int
|
||
, currentYear : Int
|
||
, weekDates : Maybe WeekDates
|
||
, currentTime : Time.Posix
|
||
, zone : Time.Zone
|
||
, newSchedule : NewSchedule
|
||
, newUser : NewUser
|
||
, error : Maybe String
|
||
, weekEditMode : Bool
|
||
, hasEntriesForCurrentWeek : Bool
|
||
, userWeeklySummary : Maybe WeeklySummary
|
||
, editingTimeEntryId : Maybe Int
|
||
, editingTimeEntry : EditingTimeEntry
|
||
, editingUserId : Maybe Int
|
||
, editingUserWorkHours : String
|
||
, resetPasswordUserId : Maybe Int
|
||
, resetPasswordNew : String
|
||
, pendingDeleteId : Maybe Int
|
||
, selectedUserId : Maybe Int
|
||
, userWorkHoursInput : String
|
||
, userPasswordInput : String
|
||
, isProcessing : Bool
|
||
, mobileMenuOpen : Bool
|
||
, adminManualEntryForm : AdminManualEntry
|
||
, schoolYears : List SchoolYear
|
||
, newSchoolYear : NewSchoolYear
|
||
, activeSchoolYear : Maybe SchoolYear
|
||
, editingSchoolYearId : Maybe Int
|
||
}
|
||
|
||
|
||
type Page
|
||
= LoginPage
|
||
| UserDashboard
|
||
| AdminDashboard
|
||
|
||
|
||
type AdminTab
|
||
= ScheduleTab
|
||
| UsersTab
|
||
| TimeEntriesTab
|
||
| SchoolYearsTab
|
||
|
||
|
||
type alias Schedule =
|
||
{ id : Int
|
||
, dayOfWeek : Int
|
||
, startTime : String
|
||
, endTime : String
|
||
, scheduleType : String
|
||
, title : String
|
||
}
|
||
|
||
|
||
type alias User =
|
||
{ id : Int
|
||
, username : String
|
||
, isAdmin : Bool
|
||
, yearlyWorkHours : Float
|
||
}
|
||
|
||
|
||
type alias TimeEntry =
|
||
{ id : Int
|
||
, userId : Int
|
||
, scheduleId : Int
|
||
, date : String
|
||
, entryType : String
|
||
, username : String
|
||
, startTime : String
|
||
, endTime : String
|
||
}
|
||
|
||
|
||
type alias SelectedEntry =
|
||
{ scheduleId : Int
|
||
, dayOfWeek : Int
|
||
}
|
||
|
||
|
||
type alias NewSchedule =
|
||
{ dayOfWeek : String
|
||
, startTime : String
|
||
, endTime : String
|
||
, scheduleType : String
|
||
, title : String
|
||
}
|
||
|
||
|
||
type alias NewUser =
|
||
{ username : String
|
||
, password : String
|
||
, isAdmin : Bool
|
||
}
|
||
|
||
|
||
type alias WeekDates =
|
||
{ year : Int
|
||
, week : Int
|
||
, dates : List ( String, String )
|
||
, range : String
|
||
}
|
||
|
||
|
||
type alias WeeklySummary =
|
||
{ userId : Int
|
||
, username : String
|
||
, year : Int
|
||
, week : Int
|
||
, totalHours : Float
|
||
, targetHours : Float
|
||
, remainingHours : Float
|
||
}
|
||
|
||
|
||
type alias EditingTimeEntry =
|
||
{ entryId : Int
|
||
, date : String
|
||
, startTime : String
|
||
, endTime : String
|
||
, entryType : String
|
||
}
|
||
|
||
|
||
type alias WeeklyHours =
|
||
{ userId : Int
|
||
, username : String
|
||
, year : Int
|
||
, week : Int
|
||
, totalHours : Float
|
||
, targetHours : Float
|
||
, remainingHours : Float
|
||
}
|
||
|
||
|
||
type alias YearlyHoursSummary =
|
||
{ userId : Int
|
||
, username : String
|
||
, year : Int
|
||
, week : Int
|
||
, totalHours : Float
|
||
, yearlyTarget : Float
|
||
, yearlyActual : Float
|
||
, weeklyTarget : Float
|
||
, remainingYearly : Float
|
||
}
|
||
|
||
|
||
type alias AdminManualEntry =
|
||
{ selectedUserId : Maybe Int
|
||
, date : String
|
||
, startTime : String
|
||
, endTime : String
|
||
, entryType : String
|
||
}
|
||
|
||
|
||
type alias SchoolYear =
|
||
{ id : Int
|
||
, name : String
|
||
, startDate : String
|
||
, endDate : String
|
||
, isActive : Bool
|
||
}
|
||
|
||
|
||
type alias NewSchoolYear =
|
||
{ name : String
|
||
, startDate : String
|
||
, endDate : String
|
||
}
|
||
|
||
|
||
init : Flags -> ( Model, Cmd Msg )
|
||
init flags =
|
||
let
|
||
initialPage =
|
||
case flags.token of
|
||
Just _ ->
|
||
if flags.isAdmin then
|
||
AdminDashboard
|
||
|
||
else
|
||
UserDashboard
|
||
|
||
Nothing ->
|
||
LoginPage
|
||
|
||
model =
|
||
{ page = initialPage
|
||
, activeTab = ScheduleTab
|
||
, username = ""
|
||
, password = ""
|
||
, token = flags.token
|
||
, isAdmin = flags.isAdmin
|
||
, schedules = []
|
||
, users = []
|
||
, timeEntries = []
|
||
, weeklyHours = []
|
||
, yearlyHoursSummary = []
|
||
, selectedEntries = []
|
||
, currentWeek = 1
|
||
, currentYear = 2025
|
||
, currentTime = Time.millisToPosix 0
|
||
, zone = Time.utc
|
||
, newSchedule = NewSchedule "" "" "" "lesson" ""
|
||
, newUser = NewUser "" "" False
|
||
, error = Nothing
|
||
, weekEditMode = False
|
||
, hasEntriesForCurrentWeek = False
|
||
, weekDates = Nothing
|
||
, userWeeklySummary = Nothing
|
||
, editingTimeEntryId = Nothing
|
||
, editingTimeEntry = EditingTimeEntry 0 "" "" "" ""
|
||
, editingUserId = Nothing
|
||
, editingUserWorkHours = ""
|
||
, resetPasswordUserId = Nothing
|
||
, resetPasswordNew = ""
|
||
, pendingDeleteId = Nothing
|
||
, selectedUserId = Nothing
|
||
, userWorkHoursInput = ""
|
||
, userPasswordInput = ""
|
||
, isProcessing = False
|
||
, mobileMenuOpen = False
|
||
, adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson"
|
||
, schoolYears = []
|
||
, newSchoolYear = NewSchoolYear "" "" ""
|
||
, activeSchoolYear = Nothing
|
||
, editingSchoolYearId = Nothing
|
||
}
|
||
|
||
cmd =
|
||
case flags.token of
|
||
Just token ->
|
||
Cmd.batch
|
||
[ Task.perform SetTime Time.now
|
||
, fetchSchedules (Just token)
|
||
, fetchYearlyHoursSummary token
|
||
, if flags.isAdmin then
|
||
fetchSchoolYears token
|
||
|
||
else
|
||
fetchMyInfo token
|
||
]
|
||
|
||
Nothing ->
|
||
Task.perform SetTime Time.now
|
||
in
|
||
( model, cmd )
|
||
|
||
|
||
|
||
-- UPDATE
|
||
|
||
|
||
type Msg
|
||
= UpdateUsername String
|
||
| UpdatePassword String
|
||
| Login
|
||
| LoginResponse (Result Http.Error LoginResult)
|
||
| Logout
|
||
| SetTime Time.Posix
|
||
| FetchSchedules
|
||
| SchedulesReceived (Result Http.Error (List Schedule))
|
||
| ToggleScheduleSelection Int Int
|
||
| SaveTimeEntries
|
||
| TimeEntriesSaved (Result Http.Error ())
|
||
| PreviousWeek
|
||
| NextWeek
|
||
| EnableEditMode
|
||
| DisableEditMode
|
||
| DeleteWeekEntries
|
||
| WeekEntriesDeleted (Result Http.Error ())
|
||
| SwitchTab AdminTab
|
||
| UpdateNewScheduleDay String
|
||
| UpdateNewScheduleStart String
|
||
| UpdateNewScheduleEnd String
|
||
| UpdateNewScheduleType String
|
||
| UpdateNewScheduleTitle String
|
||
| CreateSchedule
|
||
| ScheduleCreated (Result Http.Error ())
|
||
| DeleteSchedule Int
|
||
| ScheduleDeleted (Result Http.Error ())
|
||
| UpdateNewUsername String
|
||
| UpdateNewPassword String
|
||
| UpdateNewUserAdmin Bool
|
||
| CreateUser
|
||
| UserCreated (Result Http.Error ())
|
||
| DeleteUser Int
|
||
| UserDeleted (Result Http.Error ())
|
||
| FetchUsers
|
||
| UsersReceived (Result Http.Error (List User))
|
||
| FetchMyTimeEntries
|
||
| MyTimeEntriesReceived (Result Http.Error (List TimeEntry))
|
||
| FetchAllTimeEntries
|
||
| AllTimeEntriesReceived (Result Http.Error (List TimeEntry))
|
||
| FetchWeeklyHours
|
||
| WeeklyHoursReceived (Result Http.Error (List WeeklyHours))
|
||
| FetchYearlyHoursSummary
|
||
| YearlyHoursSummaryReceived (Result Http.Error (List YearlyHoursSummary))
|
||
| FetchWeekDates
|
||
| WeekDatesReceived (Result Http.Error WeekDates)
|
||
| CheckWeekHasEntries
|
||
| WeekHasEntriesReceived (Result Http.Error Bool)
|
||
| MyWeeklySummaryReceived (Result Http.Error WeeklySummary)
|
||
| EditTimeEntry Int
|
||
| CancelEditTimeEntry
|
||
| UpdateEditTimeEntryDate String
|
||
| UpdateEditTimeEntryStartTime String
|
||
| UpdateEditTimeEntryEndTime String
|
||
| UpdateEditTimeEntryType String
|
||
| SaveEditTimeEntry
|
||
| TimeEntrySaved (Result Http.Error ())
|
||
| TimeEntryDeleted (Result Http.Error ())
|
||
| EditUserWorkHours Int
|
||
| CancelEditUserWorkHours
|
||
| UpdateEditUserWorkHours String
|
||
| SaveUserWorkHours
|
||
| UserWorkHoursSaved (Result Http.Error ())
|
||
| ResetUserPassword Int
|
||
| CancelResetPassword
|
||
| UpdateResetPasswordNew String
|
||
| SaveResetPassword
|
||
| ResetPasswordSaved (Result Http.Error ())
|
||
| ConfirmDeleteTimeEntry Int
|
||
| ConfirmDeleteUser Int
|
||
| DeleteConfirmed Bool
|
||
| StartEditingTimeEntry Int TimeEntry
|
||
| CancelEditingTimeEntry
|
||
| UpdateEditingTimeEntryDate String
|
||
| UpdateEditingTimeEntryStartTime String
|
||
| UpdateEditingTimeEntryEndTime String
|
||
| UpdateEditingTimeEntryType String
|
||
| SaveEditingTimeEntry
|
||
| SelectUserForManagement Int
|
||
| UpdateUserWorkHours String
|
||
| UpdateUserPassword String
|
||
| SaveUserPassword
|
||
| UserPasswordSaved (Result Http.Error ())
|
||
| ToggleMobileMenu
|
||
| CloseMobileMenu
|
||
| SelectUserForManualEntry Int
|
||
| UpdateManualEntryDate String
|
||
| UpdateManualEntryStartTime String
|
||
| UpdateManualEntryEndTime String
|
||
| UpdateManualEntryType String
|
||
| SaveAdminTimeEntry
|
||
| AdminTimeEntrySaved (Result Http.Error ())
|
||
| FetchMyInfo
|
||
| MyInfoReceived (Result Http.Error User)
|
||
| FetchSchoolYears
|
||
| SchoolYearsReceived (Result Http.Error (List SchoolYear))
|
||
| FetchActiveSchoolYear
|
||
| ActiveSchoolYearReceived (Result Http.Error SchoolYear)
|
||
| UpdateNewSchoolYearName String
|
||
| UpdateNewSchoolYearStart String
|
||
| UpdateNewSchoolYearEnd String
|
||
| CreateSchoolYear
|
||
| SchoolYearCreated (Result Http.Error ())
|
||
| ActivateSchoolYear Int
|
||
| SchoolYearActivated (Result Http.Error ())
|
||
| DeleteSchoolYear Int
|
||
| SchoolYearDeleted (Result Http.Error ())
|
||
|
||
|
||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||
update msg model =
|
||
case msg of
|
||
ToggleMobileMenu ->
|
||
( { model | mobileMenuOpen = not model.mobileMenuOpen }, Cmd.none )
|
||
|
||
CloseMobileMenu ->
|
||
( { model | mobileMenuOpen = False }, Cmd.none )
|
||
|
||
UpdateUsername username ->
|
||
( { model | username = username }, Cmd.none )
|
||
|
||
UpdatePassword password ->
|
||
( { model | password = password }, Cmd.none )
|
||
|
||
Login ->
|
||
if model.isProcessing then
|
||
( model, Cmd.none )
|
||
|
||
else
|
||
( { model | isProcessing = True }, loginRequest model.username model.password )
|
||
|
||
LoginResponse (Ok result) ->
|
||
let
|
||
newPage =
|
||
if result.isAdmin then
|
||
AdminDashboard
|
||
|
||
else
|
||
UserDashboard
|
||
|
||
( year, week ) =
|
||
getISOWeekFromPosix model.currentTime
|
||
|
||
tokenData =
|
||
Encode.object
|
||
[ ( "token", Encode.string result.token )
|
||
, ( "isAdmin", Encode.bool result.isAdmin )
|
||
]
|
||
in
|
||
( { model
|
||
| token = Just result.token
|
||
, username = result.username
|
||
, isAdmin = result.isAdmin
|
||
, page = newPage
|
||
, error = Nothing
|
||
, isProcessing = False
|
||
}
|
||
, Cmd.batch
|
||
[ saveToken tokenData
|
||
, fetchSchedules (Just result.token)
|
||
, if not result.isAdmin then
|
||
Cmd.batch
|
||
[ fetchMyTimeEntries result.token
|
||
, fetchWeekDates result.token year week
|
||
, checkWeekHasEntries result.token year week
|
||
, fetchYearlyHoursSummary result.token
|
||
, fetchMyInfo result.token
|
||
]
|
||
|
||
else
|
||
Cmd.batch
|
||
[ fetchMyTimeEntries result.token
|
||
, fetchWeekDates result.token year week
|
||
, checkWeekHasEntries result.token year week
|
||
, fetchYearlyHoursSummary result.token
|
||
]
|
||
]
|
||
)
|
||
|
||
LoginResponse (Err _) ->
|
||
( { model | error = Just "Login fehlgeschlagen", isProcessing = False }, Cmd.none )
|
||
|
||
Logout ->
|
||
( { model
|
||
| page = LoginPage
|
||
, token = Nothing
|
||
, isAdmin = False
|
||
, username = ""
|
||
, password = ""
|
||
, isProcessing = False
|
||
}
|
||
, removeToken ()
|
||
)
|
||
|
||
FetchSchedules ->
|
||
( model, fetchSchedules model.token )
|
||
|
||
SchedulesReceived (Ok schedules) ->
|
||
( { model | schedules = schedules }, Cmd.none )
|
||
|
||
SchedulesReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none )
|
||
|
||
ToggleScheduleSelection scheduleId dayOfWeek ->
|
||
let
|
||
entry =
|
||
{ scheduleId = scheduleId, dayOfWeek = dayOfWeek }
|
||
|
||
newSelected =
|
||
if List.any (\e -> e.scheduleId == scheduleId && e.dayOfWeek == dayOfWeek) model.selectedEntries then
|
||
List.filter (\e -> not (e.scheduleId == scheduleId && e.dayOfWeek == dayOfWeek)) model.selectedEntries
|
||
|
||
else
|
||
entry :: model.selectedEntries
|
||
in
|
||
( { model | selectedEntries = newSelected }, Cmd.none )
|
||
|
||
SaveTimeEntries ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model | error = Nothing }
|
||
, saveTimeEntriesForWeek token model.selectedEntries model.currentYear model.currentWeek model.schedules model.weekDates
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
TimeEntriesSaved (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| error = Nothing
|
||
, weekEditMode = False
|
||
, hasEntriesForCurrentWeek = True
|
||
}
|
||
, Cmd.batch
|
||
[ fetchMyTimeEntries token
|
||
]
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
TimeEntriesSaved (Err _) ->
|
||
( { model | error = Just "Fehler beim Speichern" }, Cmd.none )
|
||
|
||
PreviousWeek ->
|
||
let
|
||
( newYear, newWeek ) =
|
||
previousWeek model.currentYear model.currentWeek
|
||
in
|
||
( { model
|
||
| currentWeek = newWeek
|
||
, currentYear = newYear
|
||
, selectedEntries = []
|
||
, weekEditMode = False
|
||
}
|
||
, case model.token of
|
||
Just token ->
|
||
Cmd.batch
|
||
[ fetchWeekDates token newYear newWeek
|
||
, checkWeekHasEntries token newYear newWeek
|
||
]
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
)
|
||
|
||
NextWeek ->
|
||
let
|
||
( newYear, newWeek ) =
|
||
nextWeek model.currentYear model.currentWeek
|
||
in
|
||
( { model
|
||
| currentWeek = newWeek
|
||
, currentYear = newYear
|
||
, selectedEntries = []
|
||
, weekEditMode = False
|
||
}
|
||
, case model.token of
|
||
Just token ->
|
||
Cmd.batch
|
||
[ fetchWeekDates token newYear newWeek
|
||
, checkWeekHasEntries token newYear newWeek
|
||
]
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
)
|
||
|
||
FetchWeekDates ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchWeekDates token model.currentYear model.currentWeek )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
WeekDatesReceived (Ok weekDates) ->
|
||
( { model | weekDates = Just weekDates }, Cmd.none )
|
||
|
||
WeekDatesReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Wochendaten" }, Cmd.none )
|
||
|
||
CheckWeekHasEntries ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, checkWeekHasEntries token model.currentYear model.currentWeek )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
WeekHasEntriesReceived (Ok hasEntries) ->
|
||
( { model | hasEntriesForCurrentWeek = hasEntries }, Cmd.none )
|
||
|
||
WeekHasEntriesReceived (Err _) ->
|
||
( model, Cmd.none )
|
||
|
||
SetTime time ->
|
||
let
|
||
( year, week ) =
|
||
getISOWeekFromPosix time
|
||
|
||
cmds =
|
||
case model.token of
|
||
Just token ->
|
||
if model.page == UserDashboard || model.page == LoginPage then
|
||
Cmd.batch
|
||
[ checkWeekHasEntries token year week
|
||
, fetchWeekDates token year week
|
||
, fetchMyTimeEntries token
|
||
]
|
||
|
||
else
|
||
Cmd.none
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
in
|
||
( { model
|
||
| currentTime = time
|
||
, currentWeek = week
|
||
, currentYear = year
|
||
}
|
||
, cmds
|
||
)
|
||
|
||
EnableEditMode ->
|
||
let
|
||
currentWeekEntries =
|
||
List.filter
|
||
(\e ->
|
||
let
|
||
( entryYear, entryWeek ) =
|
||
getYearWeekFromDate e.date
|
||
in
|
||
entryWeek == model.currentWeek && entryYear == model.currentYear
|
||
)
|
||
model.timeEntries
|
||
|
||
preSelectedEntries =
|
||
List.map
|
||
(\entry ->
|
||
let
|
||
parts =
|
||
String.split "-" entry.date
|
||
|
||
year =
|
||
parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 2025
|
||
|
||
month =
|
||
parts |> List.drop 1 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1
|
||
|
||
day =
|
||
parts |> List.drop 2 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1
|
||
|
||
dayOfWeek =
|
||
getDayOfWeek year month day
|
||
in
|
||
{ scheduleId = entry.scheduleId, dayOfWeek = dayOfWeek }
|
||
)
|
||
currentWeekEntries
|
||
in
|
||
( { model
|
||
| weekEditMode = True
|
||
, selectedEntries = preSelectedEntries
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
DisableEditMode ->
|
||
( { model
|
||
| weekEditMode = False
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
DeleteWeekEntries ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, deleteWeekEntries token model.currentYear model.currentWeek )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
WeekEntriesDeleted (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| weekEditMode = True
|
||
, selectedEntries = []
|
||
, hasEntriesForCurrentWeek = False
|
||
}
|
||
, fetchMyTimeEntries token
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
WeekEntriesDeleted (Err _) ->
|
||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||
|
||
SwitchTab tab ->
|
||
let
|
||
cmd =
|
||
case tab of
|
||
UsersTab ->
|
||
case model.token of
|
||
Just token ->
|
||
fetchUsers token
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
TimeEntriesTab ->
|
||
case model.token of
|
||
Just token ->
|
||
Cmd.batch
|
||
[ fetchAllTimeEntries token
|
||
, fetchYearlyHoursSummary token
|
||
]
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
SchoolYearsTab ->
|
||
case model.token of
|
||
Just token ->
|
||
Cmd.batch
|
||
[ fetchSchoolYears token
|
||
, fetchActiveSchoolYear token
|
||
]
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
_ ->
|
||
Cmd.none
|
||
in
|
||
( { model | activeTab = tab, mobileMenuOpen = False }, cmd )
|
||
|
||
UpdateNewScheduleDay day ->
|
||
let
|
||
oldSchedule =
|
||
model.newSchedule
|
||
|
||
newSchedule =
|
||
{ oldSchedule | dayOfWeek = day }
|
||
in
|
||
( { model | newSchedule = newSchedule }, Cmd.none )
|
||
|
||
UpdateNewScheduleStart time ->
|
||
let
|
||
oldSchedule =
|
||
model.newSchedule
|
||
|
||
newSchedule =
|
||
{ oldSchedule | startTime = time }
|
||
in
|
||
( { model | newSchedule = newSchedule }, Cmd.none )
|
||
|
||
UpdateNewScheduleEnd time ->
|
||
let
|
||
oldSchedule =
|
||
model.newSchedule
|
||
|
||
newSchedule =
|
||
{ oldSchedule | endTime = time }
|
||
in
|
||
( { model | newSchedule = newSchedule }, Cmd.none )
|
||
|
||
UpdateNewScheduleType scheduleType ->
|
||
let
|
||
oldSchedule =
|
||
model.newSchedule
|
||
|
||
newSchedule =
|
||
{ oldSchedule | scheduleType = scheduleType }
|
||
in
|
||
( { model | newSchedule = newSchedule }, Cmd.none )
|
||
|
||
UpdateNewScheduleTitle title ->
|
||
let
|
||
oldSchedule =
|
||
model.newSchedule
|
||
|
||
newSchedule =
|
||
{ oldSchedule | title = title }
|
||
in
|
||
( { model | newSchedule = newSchedule }, Cmd.none )
|
||
|
||
CreateSchedule ->
|
||
if
|
||
String.isEmpty model.newSchedule.dayOfWeek
|
||
|| String.isEmpty model.newSchedule.startTime
|
||
|| String.isEmpty model.newSchedule.endTime
|
||
then
|
||
( { model | error = Just "Bitte alle Felder ausfüllen" }, Cmd.none )
|
||
|
||
else
|
||
case model.token of
|
||
Just token ->
|
||
( { model | isProcessing = True }, createSchedule token model.newSchedule )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
ScheduleCreated (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
let
|
||
emptySchedule =
|
||
NewSchedule "" "" "" "lesson" ""
|
||
in
|
||
( { model
|
||
| newSchedule = emptySchedule
|
||
, error = Nothing
|
||
, isProcessing = False
|
||
}
|
||
, fetchSchedules model.token
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
ScheduleCreated (Err err) ->
|
||
let
|
||
errorMsg =
|
||
case err of
|
||
Http.BadStatus 400 ->
|
||
"Ungültige Eingabe"
|
||
|
||
Http.BadStatus 409 ->
|
||
"Dieser Stundenplan existiert bereits"
|
||
|
||
Http.Timeout ->
|
||
"Anfrage abgelaufen"
|
||
|
||
Http.NetworkError ->
|
||
"Netzwerkfehler"
|
||
|
||
_ ->
|
||
"Fehler beim Erstellen"
|
||
in
|
||
( { model
|
||
| error = Just errorMsg
|
||
, isProcessing = False
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
DeleteSchedule scheduleId ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, deleteSchedule token scheduleId )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
ScheduleDeleted (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model | error = Nothing }, fetchSchedules (Just token) )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
ScheduleDeleted (Err _) ->
|
||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||
|
||
UpdateNewUsername username ->
|
||
let
|
||
oldUser =
|
||
model.newUser
|
||
|
||
newUser =
|
||
{ oldUser | username = username }
|
||
in
|
||
( { model | newUser = newUser }, Cmd.none )
|
||
|
||
UpdateNewPassword password ->
|
||
let
|
||
oldUser =
|
||
model.newUser
|
||
|
||
newUser =
|
||
{ oldUser | password = password }
|
||
in
|
||
( { model | newUser = newUser }, Cmd.none )
|
||
|
||
UpdateNewUserAdmin isAdmin ->
|
||
let
|
||
oldUser =
|
||
model.newUser
|
||
|
||
newUser =
|
||
{ oldUser | isAdmin = isAdmin }
|
||
in
|
||
( { model | newUser = newUser }, Cmd.none )
|
||
|
||
CreateUser ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, createUser token model.newUser )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
UserCreated (Ok _) ->
|
||
let
|
||
emptyUser =
|
||
NewUser "" "" False
|
||
in
|
||
case model.token of
|
||
Just token ->
|
||
( { model | newUser = emptyUser }, fetchUsers token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
UserCreated (Err _) ->
|
||
( { model | error = Just "Fehler beim Erstellen des Benutzers" }, Cmd.none )
|
||
|
||
DeleteUser userId ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, deleteUser token userId )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
UserDeleted (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| pendingDeleteId = Nothing
|
||
, error = Nothing
|
||
, editingUserId = Nothing
|
||
, resetPasswordUserId = Nothing
|
||
}
|
||
, fetchUsers token
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
UserDeleted (Err _) ->
|
||
( { model | error = Just "Fehler beim Löschen des Benutzers", pendingDeleteId = Nothing }, Cmd.none )
|
||
|
||
FetchUsers ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchUsers token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
UsersReceived (Ok users) ->
|
||
( { model | users = users }, Cmd.none )
|
||
|
||
UsersReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Benutzer" }, Cmd.none )
|
||
|
||
FetchMyTimeEntries ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchMyTimeEntries token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
MyTimeEntriesReceived (Ok entries) ->
|
||
let
|
||
hasEntries =
|
||
List.any
|
||
(\e ->
|
||
let
|
||
( entryYear, entryWeek ) =
|
||
getYearWeekFromDate e.date
|
||
in
|
||
entryWeek == model.currentWeek && entryYear == model.currentYear
|
||
)
|
||
entries
|
||
in
|
||
( { model
|
||
| timeEntries = entries
|
||
, hasEntriesForCurrentWeek = hasEntries
|
||
, weekEditMode = False
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
MyTimeEntriesReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Einträge" }, Cmd.none )
|
||
|
||
FetchAllTimeEntries ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchAllTimeEntries token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
AllTimeEntriesReceived (Ok entries) ->
|
||
( { model | timeEntries = entries }, Cmd.none )
|
||
|
||
AllTimeEntriesReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Zeiteinträge" }, Cmd.none )
|
||
|
||
FetchWeeklyHours ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchWeeklyHours token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
WeeklyHoursReceived (Ok hours) ->
|
||
( { model | weeklyHours = hours }, Cmd.none )
|
||
|
||
WeeklyHoursReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Wochenstunden" }, Cmd.none )
|
||
|
||
FetchYearlyHoursSummary ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchYearlyHoursSummary token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
YearlyHoursSummaryReceived (Ok summary) ->
|
||
( { model | yearlyHoursSummary = summary }, Cmd.none )
|
||
|
||
YearlyHoursSummaryReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Jahresübersicht" }, Cmd.none )
|
||
|
||
MyWeeklySummaryReceived (Ok summary) ->
|
||
( { model | userWeeklySummary = Just summary }, Cmd.none )
|
||
|
||
MyWeeklySummaryReceived (Err _) ->
|
||
( { model | userWeeklySummary = Nothing }, Cmd.none )
|
||
|
||
EditTimeEntry entryId ->
|
||
case List.filter (\e -> e.id == entryId) model.timeEntries |> List.head of
|
||
Just entry ->
|
||
( { model
|
||
| editingTimeEntryId = Just entryId
|
||
, editingTimeEntry =
|
||
{ entryId = entryId
|
||
, date = entry.date
|
||
, startTime = entry.startTime
|
||
, endTime = entry.endTime
|
||
, entryType = entry.entryType
|
||
}
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
CancelEditTimeEntry ->
|
||
( { model
|
||
| editingTimeEntryId = Nothing
|
||
, editingTimeEntry = EditingTimeEntry 0 "" "" "" ""
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
UpdateEditTimeEntryDate date ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | date = date }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
UpdateEditTimeEntryStartTime time ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | startTime = time }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
UpdateEditTimeEntryEndTime time ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | endTime = time }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
UpdateEditTimeEntryType entryType ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | entryType = entryType }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
SaveEditTimeEntry ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, updateTimeEntry token model.editingTimeEntry )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
TimeEntryDeleted (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| editingTimeEntryId = Nothing
|
||
, editingTimeEntry = EditingTimeEntry 0 "" "" "" "lesson"
|
||
, pendingDeleteId = Nothing
|
||
, error = Nothing
|
||
}
|
||
, Cmd.batch
|
||
[ fetchAllTimeEntries token
|
||
, fetchWeeklyHours token
|
||
, fetchYearlyHoursSummary token
|
||
]
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
TimeEntryDeleted (Err _) ->
|
||
( { model | error = Just "Fehler beim Löschen des Eintrags", pendingDeleteId = Nothing }, Cmd.none )
|
||
|
||
EditUserWorkHours userId ->
|
||
case List.filter (\u -> u.id == userId) model.users |> List.head of
|
||
Just user ->
|
||
( { model
|
||
| editingUserId = Just userId
|
||
, editingUserWorkHours = String.fromFloat user.yearlyWorkHours
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
CancelEditUserWorkHours ->
|
||
( { model
|
||
| editingUserId = Nothing
|
||
, editingUserWorkHours = ""
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
UpdateEditUserWorkHours hours ->
|
||
( { model | editingUserWorkHours = hours }, Cmd.none )
|
||
|
||
ResetUserPassword userId ->
|
||
( { model
|
||
| resetPasswordUserId = Just userId
|
||
, resetPasswordNew = ""
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
CancelResetPassword ->
|
||
( { model
|
||
| resetPasswordUserId = Nothing
|
||
, resetPasswordNew = ""
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
UpdateResetPasswordNew password ->
|
||
( { model | resetPasswordNew = password }, Cmd.none )
|
||
|
||
SaveResetPassword ->
|
||
case model.resetPasswordUserId of
|
||
Just userId ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, resetUserPassword token userId model.resetPasswordNew )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
ResetPasswordSaved (Ok _) ->
|
||
( { model
|
||
| resetPasswordUserId = Nothing
|
||
, resetPasswordNew = ""
|
||
, error = Just "Passwort erfolgreich zurückgesetzt"
|
||
}
|
||
, case model.token of
|
||
Just token ->
|
||
fetchUsers token
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
)
|
||
|
||
ResetPasswordSaved (Err _) ->
|
||
( { model | error = Just "Fehler beim Zurücksetzen des Passworts" }, Cmd.none )
|
||
|
||
StartEditingTimeEntry entryId entry ->
|
||
( { model
|
||
| editingTimeEntryId = Just entryId
|
||
, editingTimeEntry = EditingTimeEntry entryId entry.date entry.startTime entry.endTime entry.entryType
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
CancelEditingTimeEntry ->
|
||
( { model
|
||
| editingTimeEntryId = Nothing
|
||
, editingTimeEntry = EditingTimeEntry 0 "" "" "" "lesson"
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
UpdateEditingTimeEntryDate date ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | date = date }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
UpdateEditingTimeEntryStartTime time ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | startTime = time }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
UpdateEditingTimeEntryEndTime time ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | endTime = time }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
UpdateEditingTimeEntryType entryType ->
|
||
let
|
||
old =
|
||
model.editingTimeEntry
|
||
|
||
new =
|
||
{ old | entryType = entryType }
|
||
in
|
||
( { model | editingTimeEntry = new }, Cmd.none )
|
||
|
||
SaveEditingTimeEntry ->
|
||
case ( model.token, model.editingTimeEntryId ) of
|
||
( Just token, Just entryId ) ->
|
||
( model, updateTimeEntry token model.editingTimeEntry )
|
||
|
||
_ ->
|
||
( model, Cmd.none )
|
||
|
||
TimeEntrySaved (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| editingTimeEntryId = Nothing
|
||
, pendingDeleteId = Nothing
|
||
, error = Nothing
|
||
}
|
||
, fetchAllTimeEntries token
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
TimeEntrySaved (Err _) ->
|
||
( { model | error = Just "Fehler beim Speichern des Eintrags" }, Cmd.none )
|
||
|
||
ConfirmDeleteTimeEntry entryId ->
|
||
( { model | pendingDeleteId = Just entryId }, confirmDelete "Soll dieser Zeiteintrag gelöscht werden?" )
|
||
|
||
ConfirmDeleteUser userId ->
|
||
( { model | pendingDeleteId = Just userId }, confirmDelete "Soll dieser Benutzer wirklich gelöscht werden?" )
|
||
|
||
DeleteConfirmed confirmed ->
|
||
if confirmed then
|
||
case ( model.token, model.pendingDeleteId ) of
|
||
( Just token, Just id ) ->
|
||
let
|
||
isTimeEntry =
|
||
List.any (\e -> e.id == id) model.timeEntries
|
||
in
|
||
if isTimeEntry then
|
||
( model, deleteTimeEntry token id )
|
||
|
||
else
|
||
( model, deleteUser token id )
|
||
|
||
_ ->
|
||
( model, Cmd.none )
|
||
|
||
else
|
||
( { model | pendingDeleteId = Nothing }, Cmd.none )
|
||
|
||
SelectUserForManagement userId ->
|
||
( { model | selectedUserId = Just userId, userWorkHoursInput = "", userPasswordInput = "" }, Cmd.none )
|
||
|
||
UpdateUserWorkHours input ->
|
||
( { model | userWorkHoursInput = input }, Cmd.none )
|
||
|
||
SaveUserWorkHours ->
|
||
case ( model.token, model.editingUserId, String.toFloat model.editingUserWorkHours ) of
|
||
( Just token, Just userId, Just hours ) ->
|
||
( model, updateUserWorkHours token userId (String.fromFloat hours) )
|
||
|
||
_ ->
|
||
( { model | error = Just "Ungültige Eingabe für Arbeitszeit" }, Cmd.none )
|
||
|
||
UserWorkHoursSaved (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| editingUserWorkHours = ""
|
||
, editingUserId = Nothing
|
||
, error = Nothing
|
||
}
|
||
, fetchUsers token
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
UserWorkHoursSaved (Err _) ->
|
||
( { model | error = Just "Fehler beim Speichern der Arbeitszeit" }, Cmd.none )
|
||
|
||
UpdateUserPassword input ->
|
||
( { model | userPasswordInput = input }, Cmd.none )
|
||
|
||
SaveUserPassword ->
|
||
case ( model.token, model.selectedUserId ) of
|
||
( Just token, Just userId ) ->
|
||
if String.length model.userPasswordInput > 0 then
|
||
( model, resetUserPassword token userId model.userPasswordInput )
|
||
|
||
else
|
||
( { model | error = Just "Passwort erforderlich" }, Cmd.none )
|
||
|
||
_ ->
|
||
( { model | error = Just "Passwort erforderlich" }, Cmd.none )
|
||
|
||
UserPasswordSaved (Ok _) ->
|
||
( { model
|
||
| userPasswordInput = ""
|
||
, selectedUserId = Nothing
|
||
, error = Nothing
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
UserPasswordSaved (Err _) ->
|
||
( { model | error = Just "Fehler beim Zurücksetzen des Passworts" }, Cmd.none )
|
||
|
||
SelectUserForManualEntry userId ->
|
||
let
|
||
form =
|
||
model.adminManualEntryForm
|
||
in
|
||
( { model | adminManualEntryForm = { form | selectedUserId = Just userId } }, Cmd.none )
|
||
|
||
UpdateManualEntryDate date ->
|
||
let
|
||
form =
|
||
model.adminManualEntryForm
|
||
in
|
||
( { model | adminManualEntryForm = { form | date = date } }, Cmd.none )
|
||
|
||
UpdateManualEntryStartTime time ->
|
||
let
|
||
form =
|
||
model.adminManualEntryForm
|
||
in
|
||
( { model | adminManualEntryForm = { form | startTime = time } }, Cmd.none )
|
||
|
||
UpdateManualEntryEndTime time ->
|
||
let
|
||
form =
|
||
model.adminManualEntryForm
|
||
in
|
||
( { model | adminManualEntryForm = { form | endTime = time } }, Cmd.none )
|
||
|
||
UpdateManualEntryType entryType ->
|
||
let
|
||
form =
|
||
model.adminManualEntryForm
|
||
in
|
||
( { model | adminManualEntryForm = { form | entryType = entryType } }, Cmd.none )
|
||
|
||
SaveAdminTimeEntry ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model | isProcessing = True }, createAdminTimeEntry token model.adminManualEntryForm )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
AdminTimeEntrySaved (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson"
|
||
, error = Nothing
|
||
, isProcessing = False
|
||
}
|
||
, Cmd.batch
|
||
[ fetchAllTimeEntries token
|
||
, fetchYearlyHoursSummary token
|
||
, fetchWeeklyHours token
|
||
]
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
AdminTimeEntrySaved (Err _) ->
|
||
( { model | error = Just "Fehler beim Erstellen des Eintrags", isProcessing = False }, Cmd.none )
|
||
|
||
FetchMyInfo ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchMyInfo token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
MyInfoReceived (Ok user) ->
|
||
( { model | users = [ user ] }, Cmd.none )
|
||
|
||
MyInfoReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden deiner Daten" }, Cmd.none )
|
||
|
||
FetchSchoolYears ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchSchoolYears token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearsReceived (Ok years) ->
|
||
( { model | schoolYears = years }, Cmd.none )
|
||
|
||
SchoolYearsReceived (Err _) ->
|
||
( { model | error = Just "Fehler beim Laden der Schuljahre" }, Cmd.none )
|
||
|
||
FetchActiveSchoolYear ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, fetchActiveSchoolYear token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
ActiveSchoolYearReceived (Ok year) ->
|
||
( { model | activeSchoolYear = Just year }, Cmd.none )
|
||
|
||
ActiveSchoolYearReceived (Err _) ->
|
||
( { model | activeSchoolYear = Nothing }, Cmd.none )
|
||
|
||
UpdateNewSchoolYearName name ->
|
||
let
|
||
old =
|
||
model.newSchoolYear
|
||
|
||
new =
|
||
{ old | name = name }
|
||
in
|
||
( { model | newSchoolYear = new }, Cmd.none )
|
||
|
||
UpdateNewSchoolYearStart date ->
|
||
let
|
||
old =
|
||
model.newSchoolYear
|
||
|
||
new =
|
||
{ old | startDate = date }
|
||
in
|
||
( { model | newSchoolYear = new }, Cmd.none )
|
||
|
||
UpdateNewSchoolYearEnd date ->
|
||
let
|
||
old =
|
||
model.newSchoolYear
|
||
|
||
new =
|
||
{ old | endDate = date }
|
||
in
|
||
( { model | newSchoolYear = new }, Cmd.none )
|
||
|
||
CreateSchoolYear ->
|
||
if
|
||
String.isEmpty model.newSchoolYear.name
|
||
|| String.isEmpty model.newSchoolYear.startDate
|
||
|| String.isEmpty model.newSchoolYear.endDate
|
||
then
|
||
( { model | error = Just "Bitte alle Felder ausfüllen" }, Cmd.none )
|
||
|
||
else
|
||
case model.token of
|
||
Just token ->
|
||
( { model | isProcessing = True }, createSchoolYear token model.newSchoolYear )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearCreated (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model
|
||
| newSchoolYear = NewSchoolYear "" "" ""
|
||
, error = Nothing
|
||
, isProcessing = False
|
||
}
|
||
, fetchSchoolYears token
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearCreated (Err _) ->
|
||
( { model
|
||
| error = Just "Fehler beim Erstellen des Schuljahres"
|
||
, isProcessing = False
|
||
}
|
||
, Cmd.none
|
||
)
|
||
|
||
ActivateSchoolYear id ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, activateSchoolYear token id )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearActivated (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model | error = Nothing }
|
||
, Cmd.batch
|
||
[ fetchSchoolYears token
|
||
, fetchActiveSchoolYear token
|
||
]
|
||
)
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearActivated (Err _) ->
|
||
( { model | error = Just "Fehler beim Aktivieren" }, Cmd.none )
|
||
|
||
DeleteSchoolYear id ->
|
||
case model.token of
|
||
Just token ->
|
||
( model, deleteSchoolYear token id )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearDeleted (Ok _) ->
|
||
case model.token of
|
||
Just token ->
|
||
( { model | error = Nothing }, fetchSchoolYears token )
|
||
|
||
Nothing ->
|
||
( model, Cmd.none )
|
||
|
||
SchoolYearDeleted (Err _) ->
|
||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||
|
||
|
||
|
||
-- SUBSCRIPTIONS
|
||
|
||
|
||
subscriptions : Model -> Sub Msg
|
||
subscriptions model =
|
||
confirmDeleteResponse DeleteConfirmed
|
||
|
||
|
||
|
||
-- HELPER FUNCTIONS
|
||
|
||
|
||
getISOWeekFromPosix : Time.Posix -> ( Int, Int )
|
||
getISOWeekFromPosix time =
|
||
let
|
||
year =
|
||
Time.toYear Time.utc time
|
||
|
||
month =
|
||
Time.toMonth Time.utc time |> monthToInt
|
||
|
||
day =
|
||
Time.toDay Time.utc time
|
||
in
|
||
( year, getISOWeek year month day )
|
||
|
||
|
||
monthToInt : Time.Month -> Int
|
||
monthToInt month =
|
||
case month of
|
||
Time.Jan ->
|
||
1
|
||
|
||
Time.Feb ->
|
||
2
|
||
|
||
Time.Mar ->
|
||
3
|
||
|
||
Time.Apr ->
|
||
4
|
||
|
||
Time.May ->
|
||
5
|
||
|
||
Time.Jun ->
|
||
6
|
||
|
||
Time.Jul ->
|
||
7
|
||
|
||
Time.Aug ->
|
||
8
|
||
|
||
Time.Sep ->
|
||
9
|
||
|
||
Time.Oct ->
|
||
10
|
||
|
||
Time.Nov ->
|
||
11
|
||
|
||
Time.Dec ->
|
||
12
|
||
|
||
|
||
getISOWeek : Int -> Int -> Int -> Int
|
||
getISOWeek year month day =
|
||
let
|
||
dayOfYear =
|
||
getDayOfYear year month day
|
||
|
||
jan4DayOfWeek =
|
||
getDayOfWeek year 1 4
|
||
|
||
mondayOfWeek1DayOfYear =
|
||
4 - jan4DayOfWeek
|
||
|
||
weekNum =
|
||
((dayOfYear - mondayOfWeek1DayOfYear) // 7) + 1
|
||
in
|
||
if weekNum < 1 then
|
||
52
|
||
|
||
else if weekNum > 52 then
|
||
let
|
||
dec31DayOfWeek =
|
||
getDayOfWeek year 12 31
|
||
|
||
jan1DayOfWeek =
|
||
getDayOfWeek year 1 1
|
||
in
|
||
if jan1DayOfWeek == 3 || (isLeapYear year && jan1DayOfWeek == 2) then
|
||
weekNum
|
||
|
||
else
|
||
1
|
||
|
||
else
|
||
weekNum
|
||
|
||
|
||
getDayOfYear : Int -> Int -> Int -> Int
|
||
getDayOfYear year month day =
|
||
let
|
||
daysInMonth =
|
||
[ 31
|
||
, if isLeapYear year then
|
||
29
|
||
|
||
else
|
||
28
|
||
, 31
|
||
, 30
|
||
, 31
|
||
, 30
|
||
, 31
|
||
, 31
|
||
, 30
|
||
, 31
|
||
, 30
|
||
, 31
|
||
]
|
||
|
||
daysBefore =
|
||
List.take (month - 1) daysInMonth |> List.sum
|
||
in
|
||
daysBefore + day
|
||
|
||
|
||
isLeapYear : Int -> Bool
|
||
isLeapYear year =
|
||
(modBy 4 year == 0) && ((modBy 100 year /= 0) || (modBy 400 year == 0))
|
||
|
||
|
||
getDayOfWeek : Int -> Int -> Int -> Int
|
||
getDayOfWeek year month day =
|
||
let
|
||
adjustedMonth =
|
||
if month < 3 then
|
||
month + 12
|
||
|
||
else
|
||
month
|
||
|
||
adjustedYear =
|
||
if month < 3 then
|
||
year - 1
|
||
|
||
else
|
||
year
|
||
|
||
q =
|
||
day
|
||
|
||
m =
|
||
adjustedMonth
|
||
|
||
k =
|
||
modBy 100 adjustedYear
|
||
|
||
j =
|
||
adjustedYear // 100
|
||
|
||
h =
|
||
(q + ((13 * (m + 1)) // 5) + k + (k // 4) + (j // 4) - (2 * j)) |> modBy 7
|
||
in
|
||
(h + 5) |> modBy 7
|
||
|
||
|
||
getDateForWeekDay : Int -> Int -> Int -> String
|
||
getDateForWeekDay year week dayOfWeek =
|
||
let
|
||
jan4DayOfWeek =
|
||
getDayOfWeek year 1 4
|
||
|
||
mondayOfWeek1Date =
|
||
4 - jan4DayOfWeek
|
||
|
||
targetDayOfYear =
|
||
mondayOfWeek1Date + ((week - 1) * 7) + dayOfWeek
|
||
|
||
( finalYear, finalMonth, finalDay ) =
|
||
if targetDayOfYear < 1 then
|
||
addDaysToDate (year - 1) 12 31 targetDayOfYear
|
||
|
||
else
|
||
addDaysToDate year 1 targetDayOfYear 0
|
||
in
|
||
String.fromInt finalYear
|
||
++ "-"
|
||
++ String.padLeft 2 '0' (String.fromInt finalMonth)
|
||
++ "-"
|
||
++ String.padLeft 2 '0' (String.fromInt finalDay)
|
||
|
||
|
||
addDaysToDate : Int -> Int -> Int -> Int -> ( Int, Int, Int )
|
||
addDaysToDate startYear startMonth startDay daysToAdd =
|
||
let
|
||
daysInMonth m y =
|
||
case m of
|
||
1 ->
|
||
31
|
||
|
||
2 ->
|
||
if isLeapYear y then
|
||
29
|
||
|
||
else
|
||
28
|
||
|
||
3 ->
|
||
31
|
||
|
||
4 ->
|
||
30
|
||
|
||
5 ->
|
||
31
|
||
|
||
6 ->
|
||
30
|
||
|
||
7 ->
|
||
31
|
||
|
||
8 ->
|
||
31
|
||
|
||
9 ->
|
||
30
|
||
|
||
10 ->
|
||
31
|
||
|
||
11 ->
|
||
30
|
||
|
||
12 ->
|
||
31
|
||
|
||
_ ->
|
||
0
|
||
|
||
helper y m d remaining =
|
||
if remaining == 0 then
|
||
( y, m, d )
|
||
|
||
else if remaining > 0 then
|
||
let
|
||
daysInCurrentMonth =
|
||
daysInMonth m y
|
||
|
||
daysLeftInMonth =
|
||
daysInCurrentMonth - d
|
||
in
|
||
if remaining <= daysLeftInMonth then
|
||
( y, m, d + remaining )
|
||
|
||
else if m == 12 then
|
||
helper (y + 1) 1 1 (remaining - daysLeftInMonth - 1)
|
||
|
||
else
|
||
helper y (m + 1) 1 (remaining - daysLeftInMonth - 1)
|
||
|
||
else if d + remaining >= 1 then
|
||
( y, m, d + remaining )
|
||
|
||
else if m == 1 then
|
||
let
|
||
prevMonthDays =
|
||
daysInMonth 12 (y - 1)
|
||
in
|
||
helper (y - 1) 12 prevMonthDays (remaining + d)
|
||
|
||
else
|
||
let
|
||
prevMonthDays =
|
||
daysInMonth (m - 1) y
|
||
in
|
||
helper y (m - 1) prevMonthDays (remaining + d)
|
||
in
|
||
helper startYear startMonth startDay daysToAdd
|
||
|
||
|
||
previousWeek : Int -> Int -> ( Int, Int )
|
||
previousWeek year week =
|
||
if week == 1 then
|
||
( year - 1, 52 )
|
||
|
||
else
|
||
( year, week - 1 )
|
||
|
||
|
||
nextWeek : Int -> Int -> ( Int, Int )
|
||
nextWeek year week =
|
||
if week >= 52 then
|
||
( year + 1, 1 )
|
||
|
||
else
|
||
( year, week + 1 )
|
||
|
||
|
||
getWeekDateRange : Int -> Int -> String
|
||
getWeekDateRange year week =
|
||
let
|
||
mondayDate =
|
||
getDateForWeekDay year week 0
|
||
|
||
fridayDate =
|
||
getDateForWeekDay year week 4
|
||
in
|
||
mondayDate ++ " bis " ++ fridayDate
|
||
|
||
|
||
getYearWeekFromDate : String -> ( Int, Int )
|
||
getYearWeekFromDate dateStr =
|
||
let
|
||
parts =
|
||
String.split "-" dateStr
|
||
|
||
year =
|
||
parts |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 2025
|
||
|
||
month =
|
||
parts |> List.drop 1 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1
|
||
|
||
day =
|
||
parts |> List.drop 2 |> List.head |> Maybe.andThen String.toInt |> Maybe.withDefault 1
|
||
in
|
||
( year, getISOWeek year month day )
|
||
|
||
|
||
calculateHours : String -> String -> Float
|
||
calculateHours startTime endTime =
|
||
let
|
||
parseTime timeStr =
|
||
case String.split ":" timeStr of
|
||
[ h, m ] ->
|
||
(String.toFloat h |> Maybe.withDefault 0)
|
||
+ ((String.toFloat m |> Maybe.withDefault 0) / 60)
|
||
|
||
_ ->
|
||
0
|
||
|
||
start =
|
||
parseTime startTime
|
||
|
||
end =
|
||
parseTime endTime
|
||
in
|
||
if end > start then
|
||
end - start
|
||
|
||
else
|
||
0
|
||
|
||
|
||
|
||
-- VIEW
|
||
|
||
|
||
view : Model -> Html Msg
|
||
view model =
|
||
div [ class "container" ]
|
||
[ case model.page of
|
||
LoginPage ->
|
||
viewLogin model
|
||
|
||
UserDashboard ->
|
||
viewUserDashboard model
|
||
|
||
AdminDashboard ->
|
||
viewAdminDashboard model
|
||
]
|
||
|
||
|
||
viewLogin : Model -> Html Msg
|
||
viewLogin model =
|
||
section [ class "section" ]
|
||
[ div [ class "container" ]
|
||
[ div [ class "columns is-centered" ]
|
||
[ div [ class "column is-5-tablet is-4-desktop is-3-widescreen" ]
|
||
[ div [ class "box" ]
|
||
[ h1 [ class "title has-text-centered" ] [ text "Zeiterfassung Login" ]
|
||
, case model.error of
|
||
Just err ->
|
||
div [ class "notification is-danger" ] [ text err ]
|
||
|
||
Nothing ->
|
||
text ""
|
||
, div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Benutzername" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "text"
|
||
, placeholder "Benutzername"
|
||
, value model.username
|
||
, onInput UpdateUsername
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
, div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Passwort" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "password"
|
||
, placeholder "Passwort"
|
||
, value model.password
|
||
, onInput UpdatePassword
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
, div [ class "field" ]
|
||
[ div [ class "control" ]
|
||
[ button
|
||
[ class "button is-primary is-fullwidth"
|
||
, onClick Login
|
||
]
|
||
[ text "Anmelden" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewUserDashboard : Model -> Html Msg
|
||
viewUserDashboard model =
|
||
div []
|
||
[ nav [ class "navbar is-primary" ]
|
||
[ div [ class "navbar-brand" ]
|
||
[ div [ class "navbar-item" ]
|
||
[ h1 [ class "title is-4 has-text-white" ] [ text "Zeiterfassung" ]
|
||
]
|
||
, a
|
||
[ class
|
||
("navbar-burger"
|
||
++ (if model.mobileMenuOpen then
|
||
" is-active"
|
||
|
||
else
|
||
""
|
||
)
|
||
)
|
||
, attribute "role" "navigation"
|
||
, attribute "aria-label" "menu"
|
||
, attribute "aria-expanded"
|
||
(if model.mobileMenuOpen then
|
||
"true"
|
||
|
||
else
|
||
"false"
|
||
)
|
||
, onClick ToggleMobileMenu
|
||
]
|
||
[ span [ attribute "aria-hidden" "true" ] []
|
||
, span [ attribute "aria-hidden" "true" ] []
|
||
, span [ attribute "aria-hidden" "true" ] []
|
||
]
|
||
]
|
||
, div
|
||
[ id "navbarUser"
|
||
, class
|
||
("navbar-menu"
|
||
++ (if model.mobileMenuOpen then
|
||
" is-active"
|
||
|
||
else
|
||
""
|
||
)
|
||
)
|
||
]
|
||
[ div [ class "navbar-end" ]
|
||
[ div [ class "navbar-item" ]
|
||
[ span [ class "has-text-white mr-2" ] [ text model.username ]
|
||
]
|
||
, div [ class "navbar-item" ]
|
||
[ button [ class "button is-light", onClick Logout ]
|
||
[ span [ class "icon" ]
|
||
[ i [ class "fas fa-sign-out-alt" ] [] ]
|
||
, span [] [ text "Abmelden" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, section [ class "section" ]
|
||
[ div [ class "container" ]
|
||
[ viewWeekNavigation model
|
||
, h2 [ class "title" ] [ text "Stundenplan" ]
|
||
, if model.hasEntriesForCurrentWeek && not model.weekEditMode then
|
||
div [ class "notification is-success" ]
|
||
[ div [ class "level" ]
|
||
[ div [ class "level-left" ]
|
||
[ div [ class "level-item" ]
|
||
[ span [ class "icon" ]
|
||
[ i [ class "fas fa-check-circle" ] [] ]
|
||
, span [] [ text "Diese Woche wurde bereits erfasst" ]
|
||
]
|
||
]
|
||
, div [ class "level-right" ]
|
||
[ div [ class "level-item" ]
|
||
[ button
|
||
[ class "button is-warning"
|
||
, onClick EnableEditMode
|
||
, disabled model.isProcessing
|
||
]
|
||
[ text "Bearbeiten" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
|
||
else if model.weekEditMode then
|
||
div [ class "notification is-warning" ]
|
||
[ div [ class "level" ]
|
||
[ div [ class "level-left" ]
|
||
[ div [ class "level-item" ]
|
||
[ span [ class "icon" ]
|
||
[ i [ class "fas fa-edit" ] [] ]
|
||
, span [] [ text "Bearbeitungsmodus aktiv" ]
|
||
]
|
||
]
|
||
, div [ class "level-right" ]
|
||
[ div [ class "level-item" ]
|
||
[ button
|
||
[ class "button is-danger is-small mr-2"
|
||
, onClick DeleteWeekEntries
|
||
, disabled model.isProcessing
|
||
]
|
||
[ text "Einträge löschen" ]
|
||
, button
|
||
[ class "button is-light is-small"
|
||
, onClick DisableEditMode
|
||
]
|
||
[ text "Abbrechen" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
|
||
else
|
||
div [ class "notification is-info is-light" ]
|
||
[ text "Wählen Sie die Zeiten aus, die Sie in dieser Woche gearbeitet haben." ]
|
||
, viewScheduleGridWithWeek model
|
||
, if not model.hasEntriesForCurrentWeek || model.weekEditMode then
|
||
div [ class "field mt-4" ]
|
||
[ div [ class "control" ]
|
||
[ button
|
||
[ class "button is-primary is-large is-fullwidth"
|
||
, onClick SaveTimeEntries
|
||
, disabled (List.isEmpty model.selectedEntries || model.isProcessing)
|
||
]
|
||
[ if model.isProcessing then
|
||
span [ class "icon" ] [ i [ class "fas fa-spinner fa-pulse" ] [] ]
|
||
|
||
else
|
||
text ""
|
||
, text
|
||
(if model.weekEditMode then
|
||
"Änderungen speichern"
|
||
|
||
else
|
||
"Speichern"
|
||
)
|
||
]
|
||
]
|
||
]
|
||
|
||
else
|
||
text ""
|
||
, h3 [ class "subtitle mt-6" ] [ text "Jahresgesamtzeit" ]
|
||
, viewUserYearlyTotal model
|
||
, case model.error of
|
||
Just err ->
|
||
div [ class "notification is-danger mt-4" ] [ text err ]
|
||
|
||
Nothing ->
|
||
text ""
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewAdminDashboard : Model -> Html Msg
|
||
viewAdminDashboard model =
|
||
div []
|
||
[ nav [ class "navbar is-danger" ]
|
||
[ div [ class "navbar-brand" ]
|
||
[ div [ class "navbar-item" ]
|
||
[ h1 [ class "title is-4 has-text-white" ] [ text "Admin Dashboard" ]
|
||
]
|
||
, a
|
||
[ class
|
||
("navbar-burger"
|
||
++ (if model.mobileMenuOpen then
|
||
" is-active"
|
||
|
||
else
|
||
""
|
||
)
|
||
)
|
||
, attribute "aria-label" "menu"
|
||
, attribute "aria-expanded"
|
||
(if model.mobileMenuOpen then
|
||
"true"
|
||
|
||
else
|
||
"false"
|
||
)
|
||
, onClick ToggleMobileMenu
|
||
]
|
||
[ span [ attribute "aria-hidden" "true" ] []
|
||
, span [ attribute "aria-hidden" "true" ] []
|
||
, span [ attribute "aria-hidden" "true" ] []
|
||
]
|
||
]
|
||
, div
|
||
[ id "navbarAdmin"
|
||
, class
|
||
("navbar-menu"
|
||
++ (if model.mobileMenuOpen then
|
||
" is-active"
|
||
|
||
else
|
||
""
|
||
)
|
||
)
|
||
]
|
||
[ div [ class "navbar-end" ]
|
||
[ div [ class "navbar-item" ]
|
||
[ span [ class "has-text-white mr-2" ] [ text model.username ]
|
||
]
|
||
, div [ class "navbar-item" ]
|
||
[ button [ class "button is-light", onClick Logout ]
|
||
[ span [ class "icon" ]
|
||
[ i [ class "fas fa-sign-out-alt" ] [] ]
|
||
, span [] [ text "Abmelden" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, section [ class "section" ]
|
||
[ div [ class "container" ]
|
||
[ div [ class "tabs is-boxed" ]
|
||
[ ul []
|
||
[ li [ classList [ ( "is-active", model.activeTab == ScheduleTab ) ] ]
|
||
[ a [ onClick (SwitchTab ScheduleTab) ] [ text "Stundenplan" ] ]
|
||
, li [ classList [ ( "is-active", model.activeTab == UsersTab ) ] ]
|
||
[ a [ onClick (SwitchTab UsersTab) ] [ text "Benutzer" ] ]
|
||
, li [ classList [ ( "is-active", model.activeTab == TimeEntriesTab ) ] ]
|
||
[ a [ onClick (SwitchTab TimeEntriesTab) ] [ text "Zeiteinträge" ] ]
|
||
, li [ classList [ ( "is-active", model.activeTab == SchoolYearsTab ) ] ]
|
||
[ a [ onClick (SwitchTab SchoolYearsTab) ] [ text "Schuljahre" ] ]
|
||
]
|
||
]
|
||
, case model.activeTab of
|
||
ScheduleTab ->
|
||
viewScheduleTab model
|
||
|
||
UsersTab ->
|
||
viewUsersTab model
|
||
|
||
TimeEntriesTab ->
|
||
viewTimeEntriesTab model
|
||
|
||
SchoolYearsTab ->
|
||
viewSchoolYearsTab model
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewScheduleItemWithDay : Model -> Int -> Schedule -> Html Msg
|
||
viewScheduleItemWithDay model dayOfWeek schedule =
|
||
let
|
||
isSelected =
|
||
List.any (\e -> e.scheduleId == schedule.id && e.dayOfWeek == dayOfWeek) model.selectedEntries
|
||
|
||
isClickable =
|
||
(not model.hasEntriesForCurrentWeek || model.weekEditMode) && not model.isProcessing
|
||
|
||
boxClass =
|
||
if isSelected then
|
||
"box has-background-success-light"
|
||
|
||
else if isClickable then
|
||
"box has-background-white"
|
||
|
||
else
|
||
"box has-background-light"
|
||
|
||
typeText =
|
||
if schedule.scheduleType == "break" then
|
||
" (Pause)"
|
||
|
||
else
|
||
""
|
||
|
||
cursorStyle =
|
||
if isClickable then
|
||
"pointer"
|
||
|
||
else
|
||
"not-allowed"
|
||
|
||
opacity =
|
||
if isClickable || isSelected then
|
||
"1"
|
||
|
||
else
|
||
"0.6"
|
||
in
|
||
div
|
||
[ class boxClass
|
||
, onClick
|
||
(if isClickable then
|
||
ToggleScheduleSelection schedule.id dayOfWeek
|
||
|
||
else
|
||
FetchSchedules
|
||
)
|
||
, style "cursor" cursorStyle
|
||
, style "margin-bottom" "0.5rem"
|
||
, style "padding" "0.75rem"
|
||
, style "opacity" opacity
|
||
, style "transition" "all 0.2s ease"
|
||
, style "border"
|
||
(if isClickable && not isSelected then
|
||
"2px solid transparent"
|
||
|
||
else
|
||
"2px solid currentColor"
|
||
)
|
||
]
|
||
[ p [ class "has-text-weight-bold is-size-7" ]
|
||
[ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
|
||
, p [ class "is-size-7" ]
|
||
[ text (schedule.title ++ typeText) ]
|
||
]
|
||
|
||
|
||
viewScheduleGridWithWeek : Model -> Html Msg
|
||
viewScheduleGridWithWeek model =
|
||
let
|
||
days =
|
||
[ "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag" ]
|
||
|
||
groupedSchedules =
|
||
List.range 0 4
|
||
|> List.map
|
||
(\day ->
|
||
( day, List.filter (\s -> s.dayOfWeek == day) model.schedules )
|
||
)
|
||
in
|
||
div []
|
||
[ div [ class "is-hidden-mobile" ]
|
||
[ div [ class "table-container" ]
|
||
[ table [ class "table is-bordered is-fullwidth" ]
|
||
[ thead []
|
||
[ tr [] (List.map (\day -> th [ class "has-text-centered" ] [ text day ]) days)
|
||
]
|
||
, tbody []
|
||
[ tr []
|
||
(List.map (viewDayColumnWithWeek model) groupedSchedules)
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "is-hidden-tablet" ]
|
||
(List.map2 (viewDayMobile model) days groupedSchedules)
|
||
]
|
||
|
||
|
||
viewDayMobile : Model -> String -> ( Int, List Schedule ) -> Html Msg
|
||
viewDayMobile model dayName ( dayOfWeek, schedules ) =
|
||
let
|
||
dateForDay =
|
||
case model.weekDates of
|
||
Just wd ->
|
||
wd.dates
|
||
|> List.filter (\( day, _ ) -> day == String.fromInt dayOfWeek)
|
||
|> List.head
|
||
|> Maybe.map Tuple.second
|
||
|> Maybe.withDefault "N/A"
|
||
|
||
Nothing ->
|
||
"Laden..."
|
||
in
|
||
div [ class "box mb-4" ]
|
||
[ p [ class "has-text-weight-bold has-text-centered mb-3" ]
|
||
[ text (dayName ++ " - " ++ dateForDay) ]
|
||
, div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules)
|
||
]
|
||
|
||
|
||
viewUserWeeklySummary : Model -> Html Msg
|
||
viewUserWeeklySummary model =
|
||
case model.userWeeklySummary of
|
||
Just summary ->
|
||
let
|
||
progressPercent =
|
||
Basics.min 100 (summary.totalHours / summary.targetHours * 100)
|
||
|
||
progressColor =
|
||
if summary.totalHours >= summary.targetHours then
|
||
"is-success"
|
||
|
||
else if summary.totalHours >= summary.targetHours * 0.8 then
|
||
"is-info"
|
||
|
||
else
|
||
"is-warning"
|
||
in
|
||
div [ class "box" ]
|
||
[ div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ p [ class "heading" ] [ text "Arbeitszeit diese Woche" ]
|
||
, p [ class "title" ] [ text (String.fromFloat summary.totalHours ++ " Std.") ]
|
||
, p [ class "subtitle is-6" ] [ text ("von " ++ String.fromFloat summary.targetHours ++ " Std.") ]
|
||
]
|
||
, div [ class "column" ]
|
||
[ p [ class "heading" ] [ text "Verbleibend" ]
|
||
, p [ class "title is-4", classList [ ( "has-text-success", summary.remainingHours <= 0 ) ] ]
|
||
[ text (String.fromFloat summary.remainingHours ++ " Std.") ]
|
||
, if summary.remainingHours < 0 then
|
||
p [ class "subtitle is-6 has-text-success" ] [ text "✓ Ziel erreicht!" ]
|
||
|
||
else
|
||
p [ class "subtitle is-6" ] [ text "" ]
|
||
]
|
||
]
|
||
, progress
|
||
[ class ("progress " ++ progressColor)
|
||
, value (String.fromFloat progressPercent)
|
||
, Html.Attributes.max "100"
|
||
]
|
||
[ text (String.fromFloat progressPercent ++ "%") ]
|
||
]
|
||
|
||
Nothing ->
|
||
div [ class "box" ]
|
||
[ p [ class "has-text-centered has-text-grey" ] [ text "Laden..." ]
|
||
]
|
||
|
||
|
||
viewUserYearlyTotal : Model -> Html Msg
|
||
viewUserYearlyTotal model =
|
||
let
|
||
yearlyTotal =
|
||
model.timeEntries
|
||
|> List.map
|
||
(\entry ->
|
||
if entry.entryType == "lesson" then
|
||
1.0
|
||
|
||
else
|
||
calculateHours entry.startTime entry.endTime
|
||
)
|
||
|> List.sum
|
||
|
||
userTarget =
|
||
List.filter (\u -> not u.isAdmin) model.users
|
||
|> List.head
|
||
|> Maybe.map .yearlyWorkHours
|
||
|> Maybe.withDefault 1800
|
||
|
||
remaining =
|
||
userTarget - yearlyTotal
|
||
|
||
progressPercent =
|
||
Basics.min 100 (yearlyTotal / userTarget * 100)
|
||
|
||
progressColor =
|
||
if remaining <= 0 then
|
||
"is-success"
|
||
|
||
else if yearlyTotal >= userTarget * 0.8 then
|
||
"is-info"
|
||
|
||
else
|
||
"is-warning"
|
||
in
|
||
div [ class "box" ]
|
||
[ div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ p [ class "heading" ] [ text "Jahresenziel" ]
|
||
, p [ class "title" ] [ text (String.fromFloat userTarget ++ " Std.") ]
|
||
]
|
||
, div [ class "column" ]
|
||
[ p [ class "heading" ] [ text "Geleistete Stunden" ]
|
||
, p [ class "title" ] [ text (String.fromFloat yearlyTotal ++ " Std.") ]
|
||
]
|
||
, div [ class "column" ]
|
||
[ p [ class "heading" ] [ text "Restliche Stunden" ]
|
||
, p
|
||
[ class
|
||
("title is-4 "
|
||
++ (if remaining <= 0 then
|
||
"has-text-success"
|
||
|
||
else
|
||
"has-text-warning"
|
||
)
|
||
)
|
||
]
|
||
[ text (String.fromFloat (Basics.max 0 remaining) ++ " Std.") ]
|
||
]
|
||
]
|
||
, progress
|
||
[ class ("progress " ++ progressColor)
|
||
, value (String.fromFloat progressPercent)
|
||
, Html.Attributes.max "100"
|
||
]
|
||
[ text (String.fromFloat progressPercent ++ "%") ]
|
||
]
|
||
|
||
|
||
viewScheduleTab : Model -> Html Msg
|
||
viewScheduleTab model =
|
||
div []
|
||
[ h2 [ class "title" ] [ text "Stundenplan verwalten" ]
|
||
, viewScheduleForm model
|
||
, viewScheduleList model
|
||
]
|
||
|
||
|
||
viewUsersTab : Model -> Html Msg
|
||
viewUsersTab model =
|
||
div []
|
||
[ h2 [ class "title" ] [ text "Benutzer verwalten" ]
|
||
, viewUserForm model
|
||
, viewUserList model
|
||
]
|
||
|
||
|
||
viewTimeEntriesTab : Model -> Html Msg
|
||
viewTimeEntriesTab model =
|
||
div []
|
||
[ h2 [ class "title" ] [ text "Jahresübersicht" ]
|
||
, viewYearlyHoursSummary model
|
||
, h2 [ class "title mt-6" ] [ text "Manuelle Stundeneintragung" ]
|
||
, viewAdminManualEntryForm model
|
||
, h2 [ class "title mt-6" ] [ text "Alle Zeiteinträge" ]
|
||
, case model.editingTimeEntryId of
|
||
Just _ ->
|
||
viewTimeEntriesEditForm model
|
||
|
||
Nothing ->
|
||
viewTimeEntriesListWithEdit model
|
||
]
|
||
|
||
|
||
viewYearlyHoursSummary : Model -> Html Msg
|
||
viewYearlyHoursSummary model =
|
||
div [ class "box" ]
|
||
[ if List.isEmpty model.yearlyHoursSummary then
|
||
p [ class "has-text-centered" ] [ text "Keine Daten vorhanden" ]
|
||
|
||
else
|
||
table [ class "table is-fullwidth is-striped is-hoverable" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "Mitarbeiter" ]
|
||
, th [ class "has-text-right" ] [ text "Sollen (Stunden)" ]
|
||
, th [ class "has-text-right" ] [ text "Iststand (Stunden)" ]
|
||
, th [ class "has-text-right" ] [ text "Differenz (Stunden)" ]
|
||
, th [ class "has-text-centered" ] [ text "Status" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map viewYearlyHourRow model.yearlyHoursSummary)
|
||
]
|
||
]
|
||
|
||
|
||
viewYearlyHourRow : YearlyHoursSummary -> Html Msg
|
||
viewYearlyHourRow summary =
|
||
let
|
||
statusClass =
|
||
if summary.remainingYearly > 0 then
|
||
"has-text-danger"
|
||
|
||
else if abs summary.remainingYearly < 0.5 then
|
||
"has-text-success"
|
||
|
||
else
|
||
"has-text-warning"
|
||
in
|
||
tr []
|
||
[ td [] [ text summary.username ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat summary.yearlyTarget) ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat summary.yearlyActual) ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat summary.remainingYearly) ]
|
||
, td [ class ("has-text-centered " ++ statusClass) ]
|
||
[ if summary.remainingYearly > 0 then
|
||
text ("Offen: " ++ String.fromFloat summary.remainingYearly)
|
||
|
||
else if summary.remainingYearly < -0.5 then
|
||
text ("Zu viel: " ++ String.fromFloat (abs summary.remainingYearly))
|
||
|
||
else
|
||
text "✓ Erfüllt"
|
||
]
|
||
]
|
||
|
||
|
||
viewAdminManualEntryForm : Model -> Html Msg
|
||
viewAdminManualEntryForm model =
|
||
div [ class "box has-background-info-light" ]
|
||
[ h3 [ class "subtitle" ] [ text "Neuer Zeiteintrag" ]
|
||
, div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Mitarbeiter" ]
|
||
, div [ class "control" ]
|
||
[ div [ class "select is-fullwidth" ]
|
||
[ select [ onInput (SelectUserForManualEntry << Maybe.withDefault 0 << String.toInt) ]
|
||
(option [] [ text "-- Wählen --" ]
|
||
:: List.map (\user -> option [ value (String.fromInt user.id) ] [ text user.username ])
|
||
(List.filter (\u -> not u.isAdmin) model.users)
|
||
)
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Datum" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "date"
|
||
, value model.adminManualEntryForm.date
|
||
, onInput UpdateManualEntryDate
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Startzeit" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "time"
|
||
, value model.adminManualEntryForm.startTime
|
||
, onInput UpdateManualEntryStartTime
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Endzeit" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "time"
|
||
, value model.adminManualEntryForm.endTime
|
||
, onInput UpdateManualEntryEndTime
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Typ" ]
|
||
, div [ class "control" ]
|
||
[ div [ class "select is-fullwidth" ]
|
||
[ select [ onInput UpdateManualEntryType, value model.adminManualEntryForm.entryType ]
|
||
[ option [ value "lesson" ] [ text "Unterricht" ]
|
||
, option [ value "break" ] [ text "Pause" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "field is-grouped mt-4" ]
|
||
[ div [ class "control" ]
|
||
[ button
|
||
[ class "button is-info"
|
||
, onClick SaveAdminTimeEntry
|
||
, disabled
|
||
(case model.adminManualEntryForm.selectedUserId of
|
||
Just _ ->
|
||
model.isProcessing
|
||
|
||
Nothing ->
|
||
True
|
||
)
|
||
]
|
||
[ text "Eintrag erstellen" ]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewTimeEntriesEditForm : Model -> Html Msg
|
||
viewTimeEntriesEditForm model =
|
||
div [ class "box has-background-warning-light" ]
|
||
[ h3 [ class "subtitle" ] [ text "Zeiteintrag bearbeiten" ]
|
||
, div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Datum" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "date"
|
||
, value model.editingTimeEntry.date
|
||
, onInput UpdateEditTimeEntryDate
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Startzeit" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "time"
|
||
, value model.editingTimeEntry.startTime
|
||
, onInput UpdateEditTimeEntryStartTime
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Endzeit" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "time"
|
||
, value model.editingTimeEntry.endTime
|
||
, onInput UpdateEditTimeEntryEndTime
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Typ" ]
|
||
, div [ class "control" ]
|
||
[ div [ class "select is-fullwidth" ]
|
||
[ select [ onInput UpdateEditTimeEntryType, value model.editingTimeEntry.entryType ]
|
||
[ option [ value "lesson" ] [ text "Unterricht" ]
|
||
, option [ value "break" ] [ text "Pause" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "field is-grouped mt-4" ]
|
||
[ div [ class "control" ]
|
||
[ button
|
||
[ class "button is-success"
|
||
, onClick SaveEditTimeEntry
|
||
]
|
||
[ text "Speichern" ]
|
||
]
|
||
, div [ class "control" ]
|
||
[ button
|
||
[ class "button is-light"
|
||
, onClick CancelEditTimeEntry
|
||
]
|
||
[ text "Abbrechen" ]
|
||
]
|
||
]
|
||
, viewTimeEntriesListWithEdit model
|
||
]
|
||
|
||
|
||
viewTimeEntriesListWithEdit : Model -> Html Msg
|
||
viewTimeEntriesListWithEdit model =
|
||
div [ class "box" ]
|
||
[ if List.isEmpty model.timeEntries then
|
||
p [ class "has-text-centered" ] [ text "Keine Einträge vorhanden" ]
|
||
|
||
else
|
||
table [ class "table is-fullwidth is-striped is-hoverable" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "Mitarbeiter" ]
|
||
, th [] [ text "Datum" ]
|
||
, th [] [ text "Zeit" ]
|
||
, th [] [ text "Typ" ]
|
||
, th [ class "has-text-right" ] [ text "Stunden" ]
|
||
, th [ class "has-text-centered" ] [ text "Aktionen" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map (viewTimeEntryRowWithEdit model) model.timeEntries)
|
||
]
|
||
]
|
||
|
||
|
||
viewTimeEntryRowWithEdit : Model -> TimeEntry -> Html Msg
|
||
viewTimeEntryRowWithEdit model entry =
|
||
let
|
||
hours =
|
||
calculateHours entry.startTime entry.endTime
|
||
|
||
isEditing =
|
||
model.editingTimeEntryId == Just entry.id
|
||
in
|
||
if isEditing then
|
||
tr []
|
||
[ td [] [ text entry.username ]
|
||
, td []
|
||
[ input
|
||
[ class "input is-small"
|
||
, type_ "date"
|
||
, value model.editingTimeEntry.date
|
||
, onInput UpdateEditTimeEntryDate
|
||
]
|
||
[]
|
||
]
|
||
, td []
|
||
[ div [ class "field is-grouped" ]
|
||
[ div [ class "control" ]
|
||
[ input
|
||
[ class "input is-small"
|
||
, type_ "time"
|
||
, value model.editingTimeEntry.startTime
|
||
, onInput UpdateEditTimeEntryStartTime
|
||
]
|
||
[]
|
||
]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input is-small"
|
||
, type_ "time"
|
||
, value model.editingTimeEntry.endTime
|
||
, onInput UpdateEditTimeEntryEndTime
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, td []
|
||
[ div [ class "select is-small" ]
|
||
[ select [ value model.editingTimeEntry.entryType, onInput UpdateEditTimeEntryType ]
|
||
[ option [ value "lesson" ] [ text "Unterricht" ]
|
||
, option [ value "break" ] [ text "Pause" ]
|
||
]
|
||
]
|
||
]
|
||
, td [ class "has-text-right" ] [ text "" ]
|
||
, td [ class "has-text-centered" ]
|
||
[ button [ class "button is-small is-success mr-2", onClick SaveEditTimeEntry ] [ text "✓" ]
|
||
, button [ class "button is-small is-light", onClick CancelEditTimeEntry ] [ text "✕" ]
|
||
]
|
||
]
|
||
|
||
else
|
||
tr []
|
||
[ td [] [ text entry.username ]
|
||
, td [] [ text entry.date ]
|
||
, td [] [ text (entry.startTime ++ " - " ++ entry.endTime) ]
|
||
, td [] [ text entry.entryType ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat hours ++ " Std.") ]
|
||
, td [ class "has-text-centered" ]
|
||
[ button
|
||
[ class "button is-small is-info mr-2"
|
||
, onClick (EditTimeEntry entry.id)
|
||
]
|
||
[ text "Bearbeiten" ]
|
||
, button
|
||
[ class "button is-small is-danger"
|
||
, onClick (ConfirmDeleteTimeEntry entry.id)
|
||
]
|
||
[ text "Löschen" ]
|
||
]
|
||
]
|
||
|
||
|
||
viewWeekNavigation : Model -> Html Msg
|
||
viewWeekNavigation model =
|
||
let
|
||
dateRange =
|
||
case model.weekDates of
|
||
Just wd ->
|
||
wd.range
|
||
|
||
Nothing ->
|
||
"Laden..."
|
||
in
|
||
div [ class "box" ]
|
||
[ nav [ class "level" ]
|
||
[ div [ class "level-left" ]
|
||
[ div [ class "level-item" ]
|
||
[ button
|
||
[ class "button is-primary"
|
||
, onClick PreviousWeek
|
||
]
|
||
[ text "← Vorherige Woche" ]
|
||
]
|
||
]
|
||
, div [ class "level-item has-text-centered" ]
|
||
[ div []
|
||
[ p [ class "heading" ] [ text "Kalenderwoche" ]
|
||
, p [ class "title" ]
|
||
[ text ("KW " ++ String.fromInt model.currentWeek ++ " / " ++ String.fromInt model.currentYear) ]
|
||
, p [ class "subtitle is-6" ]
|
||
[ text dateRange ]
|
||
]
|
||
]
|
||
, div [ class "level-right" ]
|
||
[ div [ class "level-item" ]
|
||
[ button
|
||
[ class "button is-primary"
|
||
, onClick NextWeek
|
||
]
|
||
[ text "Nächste Woche →" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewDayColumnWithWeek : Model -> ( Int, List Schedule ) -> Html Msg
|
||
viewDayColumnWithWeek model ( dayOfWeek, schedules ) =
|
||
let
|
||
dateForDay =
|
||
case model.weekDates of
|
||
Just wd ->
|
||
wd.dates
|
||
|> List.filter (\( day, _ ) -> day == String.fromInt dayOfWeek)
|
||
|> List.head
|
||
|> Maybe.map Tuple.second
|
||
|> Maybe.withDefault "N/A"
|
||
|
||
Nothing ->
|
||
"Laden..."
|
||
in
|
||
td [ class "has-background-light", style "vertical-align" "top", style "min-width" "150px" ]
|
||
[ p [ class "has-text-centered has-text-weight-bold is-size-7 mb-2" ]
|
||
[ text dateForDay ]
|
||
, div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules)
|
||
]
|
||
|
||
|
||
viewScheduleForm : Model -> Html Msg
|
||
viewScheduleForm model =
|
||
div [ class "box" ]
|
||
[ div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Wochentag" ]
|
||
, div [ class "control" ]
|
||
[ div [ class "select is-fullwidth" ]
|
||
[ select
|
||
[ onInput UpdateNewScheduleDay
|
||
, disabled model.isProcessing
|
||
, value model.newSchedule.dayOfWeek
|
||
]
|
||
[ option [ value "" ] [ text "Wochentag wählen" ]
|
||
, option [ value "0" ] [ text "Montag" ]
|
||
, option [ value "1" ] [ text "Dienstag" ]
|
||
, option [ value "2" ] [ text "Mittwoch" ]
|
||
, option [ value "3" ] [ text "Donnerstag" ]
|
||
, option [ value "4" ] [ text "Freitag" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Startzeit" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "time"
|
||
, value model.newSchedule.startTime
|
||
, onInput UpdateNewScheduleStart
|
||
, disabled model.isProcessing
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Endzeit" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "time"
|
||
, value model.newSchedule.endTime
|
||
, onInput UpdateNewScheduleEnd
|
||
, disabled model.isProcessing
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Typ" ]
|
||
, div [ class "control" ]
|
||
[ div [ class "select is-fullwidth" ]
|
||
[ select
|
||
[ onInput UpdateNewScheduleType
|
||
, value model.newSchedule.scheduleType
|
||
, disabled model.isProcessing
|
||
]
|
||
[ option [ value "lesson" ] [ text "Unterricht" ]
|
||
, option [ value "break" ] [ text "Pause" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Titel" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "text"
|
||
, placeholder "z.B. Mathematik"
|
||
, value model.newSchedule.title
|
||
, onInput UpdateNewScheduleTitle
|
||
, disabled model.isProcessing
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "field" ]
|
||
[ div [ class "control" ]
|
||
[ button
|
||
[ class "button is-primary"
|
||
, onClick CreateSchedule
|
||
, disabled (String.isEmpty model.newSchedule.dayOfWeek || model.isProcessing)
|
||
]
|
||
[ if model.isProcessing then
|
||
span [ class "icon" ] [ i [ class "fas fa-spinner fa-pulse" ] [] ]
|
||
|
||
else
|
||
text ""
|
||
, text " Hinzufügen"
|
||
]
|
||
]
|
||
]
|
||
, if String.isEmpty model.newSchedule.dayOfWeek then
|
||
div [ class "help is-warning" ] [ text "Bitte alle Felder ausfüllen" ]
|
||
|
||
else
|
||
text ""
|
||
]
|
||
|
||
|
||
viewScheduleList : Model -> Html Msg
|
||
viewScheduleList model =
|
||
div [ class "box" ]
|
||
[ h3 [ class "subtitle" ] [ text "Aktueller Stundenplan" ]
|
||
, table [ class "table is-fullwidth is-striped" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "Tag" ]
|
||
, th [] [ text "Zeit" ]
|
||
, th [] [ text "Typ" ]
|
||
, th [] [ text "Titel" ]
|
||
, th [] [ text "Aktion" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map viewScheduleRow model.schedules)
|
||
]
|
||
]
|
||
|
||
|
||
viewScheduleRow : Schedule -> Html Msg
|
||
viewScheduleRow schedule =
|
||
let
|
||
dayName =
|
||
case schedule.dayOfWeek of
|
||
0 ->
|
||
"Montag"
|
||
|
||
1 ->
|
||
"Dienstag"
|
||
|
||
2 ->
|
||
"Mittwoch"
|
||
|
||
3 ->
|
||
"Donnerstag"
|
||
|
||
4 ->
|
||
"Freitag"
|
||
|
||
_ ->
|
||
"Unbekannt"
|
||
|
||
typeName =
|
||
if schedule.scheduleType == "break" then
|
||
"Pause"
|
||
|
||
else
|
||
"Unterricht"
|
||
in
|
||
tr []
|
||
[ td [] [ text dayName ]
|
||
, td [] [ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
|
||
, td [] [ text typeName ]
|
||
, td [] [ text schedule.title ]
|
||
, td []
|
||
[ button
|
||
[ class "button is-small is-danger"
|
||
, onClick (DeleteSchedule schedule.id)
|
||
]
|
||
[ text "Löschen" ]
|
||
]
|
||
]
|
||
|
||
|
||
viewUserForm : Model -> Html Msg
|
||
viewUserForm model =
|
||
div [ class "box" ]
|
||
[ div [ class "columns" ]
|
||
[ div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Benutzername" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "text"
|
||
, placeholder "Benutzername"
|
||
, value model.newUser.username
|
||
, onInput UpdateNewUsername
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Passwort" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "password"
|
||
, placeholder "Passwort"
|
||
, value model.newUser.password
|
||
, onInput UpdateNewPassword
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column is-narrow" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Admin" ]
|
||
, div [ class "control" ]
|
||
[ label [ class "checkbox" ]
|
||
[ input
|
||
[ type_ "checkbox"
|
||
, checked model.newUser.isAdmin
|
||
, onCheck UpdateNewUserAdmin
|
||
]
|
||
[]
|
||
, text " Admin-Rechte"
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "field" ]
|
||
[ div [ class "control" ]
|
||
[ button [ class "button is-primary", onClick CreateUser ] [ text "Benutzer anlegen" ]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewUserList : Model -> Html Msg
|
||
viewUserList model =
|
||
div [ class "box" ]
|
||
[ h3 [ class "subtitle" ] [ text "Benutzer" ]
|
||
, if List.isEmpty model.users then
|
||
p [ class "has-text-centered" ] [ text "Keine Benutzer vorhanden" ]
|
||
|
||
else
|
||
table [ class "table is-fullwidth is-striped is-hoverable" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "ID" ]
|
||
, th [] [ text "Benutzername" ]
|
||
, th [] [ text "Rolle" ]
|
||
, th [ class "has-text-right" ] [ text "Arbeitszeit/Jahr" ]
|
||
, th [ class "has-text-centered" ] [ text "Aktionen" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map (viewUserRowWithActions model) model.users)
|
||
]
|
||
]
|
||
|
||
|
||
viewUserRowWithActions : Model -> User -> Html Msg
|
||
viewUserRowWithActions model user =
|
||
if model.editingUserId == Just user.id then
|
||
tr []
|
||
[ td [] [ text (String.fromInt user.id) ]
|
||
, td [] [ text user.username ]
|
||
, td []
|
||
[ text
|
||
(if user.isAdmin then
|
||
"Admin"
|
||
|
||
else
|
||
"Benutzer"
|
||
)
|
||
]
|
||
, td []
|
||
[ input
|
||
[ class "input is-small"
|
||
, type_ "number"
|
||
, step "0.5"
|
||
, value model.editingUserWorkHours
|
||
, onInput UpdateEditUserWorkHours
|
||
]
|
||
[]
|
||
]
|
||
, td [ class "has-text-centered" ]
|
||
[ button [ class "button is-small is-success mr-2", onClick SaveUserWorkHours ] [ text "✓" ]
|
||
, button [ class "button is-small is-light", onClick CancelEditUserWorkHours ] [ text "✕" ]
|
||
]
|
||
]
|
||
|
||
else if model.resetPasswordUserId == Just user.id then
|
||
tr []
|
||
[ td [] [ text (String.fromInt user.id) ]
|
||
, td [] [ text user.username ]
|
||
, td []
|
||
[ text
|
||
(if user.isAdmin then
|
||
"Admin"
|
||
|
||
else
|
||
"Benutzer"
|
||
)
|
||
]
|
||
, td []
|
||
[ input
|
||
[ class "input is-small"
|
||
, type_ "password"
|
||
, placeholder "Neues Passwort"
|
||
, value model.resetPasswordNew
|
||
, onInput UpdateResetPasswordNew
|
||
]
|
||
[]
|
||
]
|
||
, td [ class "has-text-centered" ]
|
||
[ button [ class "button is-small is-success mr-2", onClick SaveResetPassword ] [ text "✓" ]
|
||
, button [ class "button is-small is-light", onClick CancelResetPassword ] [ text "✕" ]
|
||
]
|
||
]
|
||
|
||
else
|
||
tr []
|
||
[ td [] [ text (String.fromInt user.id) ]
|
||
, td [] [ text user.username ]
|
||
, td []
|
||
[ text
|
||
(if user.isAdmin then
|
||
"Admin"
|
||
|
||
else
|
||
"Benutzer"
|
||
)
|
||
]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat user.yearlyWorkHours ++ " Std.") ]
|
||
, td [ class "has-text-centered" ]
|
||
[ if user.id == 1 then
|
||
span [ class "tag is-light" ] [ text "Geschützt" ]
|
||
|
||
else
|
||
div []
|
||
[ button
|
||
[ class "button is-small is-info mr-2"
|
||
, onClick (EditUserWorkHours user.id)
|
||
]
|
||
[ text "Arbeitszeit" ]
|
||
, button
|
||
[ class "button is-small is-warning mr-2"
|
||
, onClick (ResetUserPassword user.id)
|
||
]
|
||
[ text "PW Reset" ]
|
||
, button
|
||
[ class "button is-small is-danger"
|
||
, onClick (DeleteUser user.id)
|
||
]
|
||
[ text "Löschen" ]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewUserRow : User -> Html Msg
|
||
viewUserRow user =
|
||
tr []
|
||
[ td [] [ text (String.fromInt user.id) ]
|
||
, td [] [ text user.username ]
|
||
, td []
|
||
[ text
|
||
(if user.isAdmin then
|
||
"Admin"
|
||
|
||
else
|
||
"Benutzer"
|
||
)
|
||
]
|
||
, td []
|
||
[ if user.id == 1 then
|
||
span [ class "tag is-light" ] [ text "Geschützt" ]
|
||
|
||
else
|
||
button
|
||
[ class "button is-small is-danger"
|
||
, onClick (DeleteUser user.id)
|
||
]
|
||
[ text "Löschen" ]
|
||
]
|
||
]
|
||
|
||
|
||
viewWeeklyHoursSummary : Model -> Html Msg
|
||
viewWeeklyHoursSummary model =
|
||
let
|
||
filteredHours =
|
||
List.filter
|
||
(\h -> h.week == model.currentWeek && h.year == model.currentYear)
|
||
model.weeklyHours
|
||
in
|
||
div [ class "box" ]
|
||
[ if List.isEmpty filteredHours then
|
||
p [ class "has-text-centered" ] [ text "Keine Einträge für diese Woche" ]
|
||
|
||
else
|
||
table [ class "table is-fullwidth is-striped" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "Mitarbeiter" ]
|
||
, th [ class "has-text-right" ] [ text "Arbeitet" ]
|
||
, th [ class "has-text-right" ] [ text "Soll" ]
|
||
, th [ class "has-text-right" ] [ text "Verbleibend" ]
|
||
, th [] [ text "Fortschritt" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map viewWeeklyHoursRow filteredHours)
|
||
, tfoot []
|
||
[ tr [ class "has-background-light" ]
|
||
[ th [] [ text "Gesamt" ]
|
||
, th [ class "has-text-right has-text-weight-bold" ]
|
||
[ text (String.fromFloat (List.sum (List.map .totalHours filteredHours)) ++ " Std.") ]
|
||
, th [ class "has-text-right has-text-weight-bold" ]
|
||
[ text (String.fromFloat (List.sum (List.map .targetHours filteredHours)) ++ " Std.") ]
|
||
, th [] [ text "" ]
|
||
, th [] [ text "" ]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewWeeklyHoursRow : WeeklyHours -> Html Msg
|
||
viewWeeklyHoursRow hours =
|
||
let
|
||
progressPercent =
|
||
Basics.min 100 (hours.totalHours / hours.targetHours * 100)
|
||
|
||
progressColor =
|
||
if hours.totalHours >= hours.targetHours then
|
||
"is-success"
|
||
|
||
else if hours.totalHours >= hours.targetHours * 0.8 then
|
||
"is-info"
|
||
|
||
else
|
||
"is-warning"
|
||
in
|
||
tr []
|
||
[ td [] [ text hours.username ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat hours.totalHours ++ " Std.") ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat hours.targetHours ++ " Std.") ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat hours.remainingHours ++ " Std.") ]
|
||
, td []
|
||
[ progress
|
||
[ class ("progress " ++ progressColor)
|
||
, value (String.fromFloat progressPercent)
|
||
, Html.Attributes.max "100"
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
|
||
|
||
viewTimeEntriesList : Model -> Html Msg
|
||
viewTimeEntriesList model =
|
||
let
|
||
filteredEntries =
|
||
List.filter
|
||
(\e ->
|
||
let
|
||
( entryYear, entryWeek ) =
|
||
getYearWeekFromDate e.date
|
||
in
|
||
entryWeek == model.currentWeek && entryYear == model.currentYear
|
||
)
|
||
model.timeEntries
|
||
in
|
||
div [ class "box" ]
|
||
[ if List.isEmpty filteredEntries then
|
||
p [ class "has-text-centered" ] [ text "Keine Einträge für diese Woche" ]
|
||
|
||
else
|
||
table [ class "table is-fullwidth is-striped" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "Mitarbeiter" ]
|
||
, th [] [ text "Datum" ]
|
||
, th [] [ text "Zeit" ]
|
||
, th [] [ text "Typ" ]
|
||
, th [ class "has-text-right" ] [ text "Stunden" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map (viewTimeEntryRowWithActions model) filteredEntries)
|
||
]
|
||
]
|
||
|
||
|
||
viewTimeEntryRowWithActions : Model -> TimeEntry -> Html Msg
|
||
viewTimeEntryRowWithActions model entry =
|
||
let
|
||
hours =
|
||
if entry.entryType == "lesson" then
|
||
1.0
|
||
|
||
else
|
||
calculateHours entry.startTime entry.endTime
|
||
in
|
||
tr []
|
||
[ td [] [ text entry.username ]
|
||
, td [] [ text entry.date ]
|
||
, td [] [ text (entry.startTime ++ " - " ++ entry.endTime) ]
|
||
, td [] [ text entry.entryType ]
|
||
, td [ class "has-text-right" ] [ text (String.fromFloat hours ++ " Std.") ]
|
||
, td []
|
||
[ div [ class "buttons are-small" ]
|
||
[ button
|
||
[ class "button is-info is-small"
|
||
, onClick (StartEditingTimeEntry entry.id entry)
|
||
]
|
||
[ text "Bearbeiten" ]
|
||
, button
|
||
[ class "button is-danger is-small"
|
||
, onClick (ConfirmDeleteTimeEntry entry.id)
|
||
]
|
||
[ text "Löschen" ]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewSchoolYearsTab : Model -> Html Msg
|
||
viewSchoolYearsTab model =
|
||
div []
|
||
[ h2 [ class "title" ] [ text "Schuljahre verwalten" ]
|
||
, case model.activeSchoolYear of
|
||
Just schoolYear ->
|
||
div [ class "notification is-info is-light mb-4" ]
|
||
[ p [ class "has-text-weight-bold" ]
|
||
[ text ("Aktives Schuljahr: " ++ schoolYear.name) ]
|
||
, p [ class "is-size-7" ]
|
||
[ text (schoolYear.startDate ++ " bis " ++ schoolYear.endDate) ]
|
||
]
|
||
|
||
Nothing ->
|
||
div [ class "notification is-warning is-light mb-4" ]
|
||
[ text "⚠️ Kein Schuljahr aktiv! Bitte eines aktivieren." ]
|
||
, viewSchoolYearForm model
|
||
, viewSchoolYearsList model
|
||
]
|
||
|
||
|
||
viewSchoolYearForm : Model -> Html Msg
|
||
viewSchoolYearForm model =
|
||
div [ class "box" ]
|
||
[ h3 [ class "subtitle" ] [ text "Neues Schuljahr erstellen" ]
|
||
, div [ class "columns" ]
|
||
[ div [ class "column is-4" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Name (z.B. 2024/2025)" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "text"
|
||
, placeholder "2024/2025"
|
||
, value model.newSchoolYear.name
|
||
, onInput UpdateNewSchoolYearName
|
||
, disabled model.isProcessing
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column is-4" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Startdatum" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "date"
|
||
, value model.newSchoolYear.startDate
|
||
, onInput UpdateNewSchoolYearStart
|
||
, disabled model.isProcessing
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "column is-4" ]
|
||
[ div [ class "field" ]
|
||
[ label [ class "label" ] [ text "Enddatum" ]
|
||
, div [ class "control" ]
|
||
[ input
|
||
[ class "input"
|
||
, type_ "date"
|
||
, value model.newSchoolYear.endDate
|
||
, onInput UpdateNewSchoolYearEnd
|
||
, disabled model.isProcessing
|
||
]
|
||
[]
|
||
]
|
||
]
|
||
]
|
||
]
|
||
, div [ class "field" ]
|
||
[ div [ class "control" ]
|
||
[ button
|
||
[ class "button is-primary"
|
||
, onClick CreateSchoolYear
|
||
, disabled
|
||
(String.isEmpty model.newSchoolYear.name
|
||
|| String.isEmpty model.newSchoolYear.startDate
|
||
|| String.isEmpty model.newSchoolYear.endDate
|
||
|| model.isProcessing
|
||
)
|
||
]
|
||
[ if model.isProcessing then
|
||
span [ class "icon" ] [ i [ class "fas fa-spinner fa-pulse" ] [] ]
|
||
|
||
else
|
||
text ""
|
||
, text " Schuljahr erstellen"
|
||
]
|
||
]
|
||
]
|
||
]
|
||
|
||
|
||
viewSchoolYearsList : Model -> Html Msg
|
||
viewSchoolYearsList model =
|
||
div [ class "box mt-4" ]
|
||
[ h3 [ class "subtitle" ] [ text "Vorhandene Schuljahre" ]
|
||
, if List.isEmpty model.schoolYears then
|
||
p [ class "has-text-centered has-text-grey" ] [ text "Keine Schuljahre vorhanden" ]
|
||
|
||
else
|
||
table [ class "table is-fullwidth is-striped is-hoverable" ]
|
||
[ thead []
|
||
[ tr []
|
||
[ th [] [ text "Name" ]
|
||
, th [] [ text "Startdatum" ]
|
||
, th [] [ text "Enddatum" ]
|
||
, th [ class "has-text-centered" ] [ text "Status" ]
|
||
, th [ class "has-text-centered" ] [ text "Aktionen" ]
|
||
]
|
||
]
|
||
, tbody []
|
||
(List.map viewSchoolYearRow model.schoolYears)
|
||
]
|
||
]
|
||
|
||
|
||
viewSchoolYearRow : SchoolYear -> Html Msg
|
||
viewSchoolYearRow schoolYear =
|
||
tr []
|
||
[ td [] [ text schoolYear.name ]
|
||
, td [] [ text schoolYear.startDate ]
|
||
, td [] [ text schoolYear.endDate ]
|
||
, td [ class "has-text-centered" ]
|
||
[ if schoolYear.isActive then
|
||
span [ class "tag is-success" ] [ text "Aktiv" ]
|
||
|
||
else
|
||
span [ class "tag is-light" ] [ text "Inaktiv" ]
|
||
]
|
||
, td [ class "has-text-centered" ]
|
||
[ if not schoolYear.isActive then
|
||
button
|
||
[ class "button is-small is-info mr-2"
|
||
, onClick (ActivateSchoolYear schoolYear.id)
|
||
]
|
||
[ text "Aktivieren" ]
|
||
|
||
else
|
||
text ""
|
||
, button
|
||
[ class "button is-small is-danger"
|
||
, onClick (DeleteSchoolYear schoolYear.id)
|
||
]
|
||
[ text "Löschen" ]
|
||
]
|
||
]
|
||
|
||
|
||
|
||
-- HTTP
|
||
|
||
|
||
type alias LoginResult =
|
||
{ token : String
|
||
, username : String
|
||
, isAdmin : Bool
|
||
}
|
||
|
||
|
||
loginRequest : String -> String -> Cmd Msg
|
||
loginRequest username password =
|
||
Http.post
|
||
{ url = "/api/login"
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "username", Encode.string username )
|
||
, ( "password", Encode.string password )
|
||
]
|
||
, expect = Http.expectJson LoginResponse loginDecoder
|
||
}
|
||
|
||
|
||
loginDecoder : Decoder LoginResult
|
||
loginDecoder =
|
||
Decode.map3 LoginResult
|
||
(field "token" string)
|
||
(field "username" string)
|
||
(field "is_admin" bool)
|
||
|
||
|
||
fetchSchedules : Maybe String -> Cmd Msg
|
||
fetchSchedules maybeToken =
|
||
case maybeToken of
|
||
Just token ->
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/schedules"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson SchedulesReceived (Decode.list scheduleDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
|
||
scheduleDecoder : Decoder Schedule
|
||
scheduleDecoder =
|
||
Decode.map6 Schedule
|
||
(field "id" int)
|
||
(field "day_of_week" int)
|
||
(field "start_time" string)
|
||
(field "end_time" string)
|
||
(field "type" string)
|
||
(field "title" string)
|
||
|
||
|
||
fetchMyTimeEntries : String -> Cmd Msg
|
||
fetchMyTimeEntries token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/my-time-entries"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson MyTimeEntriesReceived (Decode.list timeEntryDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
saveTimeEntriesForWeek : String -> List SelectedEntry -> Int -> Int -> List Schedule -> Maybe WeekDates -> Cmd Msg
|
||
saveTimeEntriesForWeek token selectedEntries year week schedules maybeWeekDates =
|
||
case maybeWeekDates of
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
Just weekDates ->
|
||
let
|
||
getScheduleById id =
|
||
List.filter (\s -> s.id == id) schedules |> List.head
|
||
|
||
getDateForDay dayOfWeek =
|
||
weekDates.dates
|
||
|> List.filter (\( day, _ ) -> day == String.fromInt dayOfWeek)
|
||
|> List.head
|
||
|> Maybe.map Tuple.second
|
||
|
||
createEntryData entry =
|
||
case ( getScheduleById entry.scheduleId, getDateForDay entry.dayOfWeek ) of
|
||
( Just schedule, Just dateStr ) ->
|
||
Just <|
|
||
Encode.object
|
||
[ ( "schedule_id", Encode.int entry.scheduleId )
|
||
, ( "date", Encode.string dateStr )
|
||
, ( "type", Encode.string schedule.scheduleType )
|
||
, ( "start_time", Encode.string schedule.startTime )
|
||
, ( "end_time", Encode.string schedule.endTime )
|
||
]
|
||
|
||
_ ->
|
||
Nothing
|
||
|
||
entriesData =
|
||
List.filterMap createEntryData selectedEntries
|
||
in
|
||
if List.isEmpty entriesData then
|
||
Cmd.none
|
||
|
||
else
|
||
Http.request
|
||
{ method = "POST"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/time-entries/batch"
|
||
, body = Http.jsonBody <| Encode.object [ ( "entries", Encode.list identity entriesData ) ]
|
||
, expect = Http.expectWhatever TimeEntriesSaved
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
deleteWeekEntries : String -> Int -> Int -> Cmd Msg
|
||
deleteWeekEntries token year week =
|
||
Http.request
|
||
{ method = "DELETE"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/my-time-entries/week?year=" ++ String.fromInt year ++ "&week=" ++ String.fromInt week
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectWhatever WeekEntriesDeleted
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
createSchedule : String -> NewSchedule -> Cmd Msg
|
||
createSchedule token schedule =
|
||
case String.toInt schedule.dayOfWeek of
|
||
Just day ->
|
||
Http.request
|
||
{ method = "POST"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/schedules"
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "day_of_week", Encode.int day )
|
||
, ( "start_time", Encode.string schedule.startTime )
|
||
, ( "end_time", Encode.string schedule.endTime )
|
||
, ( "type", Encode.string schedule.scheduleType )
|
||
, ( "title", Encode.string schedule.title )
|
||
]
|
||
, expect = Http.expectWhatever ScheduleCreated
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
|
||
deleteSchedule : String -> Int -> Cmd Msg
|
||
deleteSchedule token scheduleId =
|
||
Http.request
|
||
{ method = "DELETE"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/schedules/delete?id=" ++ String.fromInt scheduleId
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectWhatever ScheduleDeleted
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
createUser : String -> NewUser -> Cmd Msg
|
||
createUser token user =
|
||
Http.request
|
||
{ method = "POST"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/users"
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "username", Encode.string user.username )
|
||
, ( "password", Encode.string user.password )
|
||
, ( "is_admin", Encode.bool user.isAdmin )
|
||
]
|
||
, expect = Http.expectWhatever UserCreated
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
deleteUser : String -> Int -> Cmd Msg
|
||
deleteUser token userId =
|
||
Http.request
|
||
{ method = "DELETE"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/users/delete?id=" ++ String.fromInt userId
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectWhatever UserDeleted
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
fetchUsers : String -> Cmd Msg
|
||
fetchUsers token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/users/list"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson UsersReceived (Decode.list userDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
userDecoder : Decoder User
|
||
userDecoder =
|
||
Decode.map4 User
|
||
(field "id" int)
|
||
(field "username" string)
|
||
(field "is_admin" bool)
|
||
(field "yearly_hours" float)
|
||
|
||
|
||
fetchAllTimeEntries : String -> Cmd Msg
|
||
fetchAllTimeEntries token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/time-entries"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson AllTimeEntriesReceived (Decode.list timeEntryDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
timeEntryDecoder : Decoder TimeEntry
|
||
timeEntryDecoder =
|
||
Decode.map8 TimeEntry
|
||
(field "id" int)
|
||
(field "user_id" int)
|
||
(field "schedule_id" int)
|
||
(field "date" string)
|
||
(field "type" string)
|
||
(field "username" string)
|
||
(field "start_time" string)
|
||
(field "end_time" string)
|
||
|
||
|
||
fetchWeeklyHours : String -> Cmd Msg
|
||
fetchWeeklyHours token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/weekly-hours"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson WeeklyHoursReceived (Decode.list weeklyHoursDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
weeklyHoursDecoder : Decoder WeeklyHours
|
||
weeklyHoursDecoder =
|
||
Decode.map7 WeeklyHours
|
||
(field "user_id" int)
|
||
(field "username" string)
|
||
(field "year" int)
|
||
(field "week" int)
|
||
(field "total_hours" float)
|
||
(field "expected_hours" float)
|
||
(field "remaining_hours" float)
|
||
|
||
|
||
fetchYearlyHoursSummary : String -> Cmd Msg
|
||
fetchYearlyHoursSummary token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/yearly-hours-summary"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson YearlyHoursSummaryReceived (Decode.list yearlyHoursSummaryDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
yearlyHoursSummaryDecoder : Decoder YearlyHoursSummary
|
||
yearlyHoursSummaryDecoder =
|
||
Decode.succeed YearlyHoursSummary
|
||
|> Decode.andThen (\f -> Decode.map f (field "user_id" int))
|
||
|> Decode.andThen (\f -> Decode.map f (field "username" string))
|
||
|> Decode.andThen (\f -> Decode.map f (field "year" int))
|
||
|> Decode.andThen (\f -> Decode.map f (field "week" int))
|
||
|> Decode.andThen (\f -> Decode.map f (field "total_hours" float))
|
||
|> Decode.andThen (\f -> Decode.map f (field "yearly_target" float))
|
||
|> Decode.andThen (\f -> Decode.map f (field "yearly_actual" float))
|
||
|> Decode.andThen (\f -> Decode.map f (field "weekly_target" float))
|
||
|> Decode.andThen (\f -> Decode.map f (field "remaining_yearly" float))
|
||
|
||
|
||
createAdminTimeEntry : String -> AdminManualEntry -> Cmd Msg
|
||
createAdminTimeEntry token entry =
|
||
case entry.selectedUserId of
|
||
Just userId ->
|
||
Http.request
|
||
{ method = "POST"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/time-entry"
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "user_id", Encode.int userId )
|
||
, ( "date", Encode.string entry.date )
|
||
, ( "start_time", Encode.string entry.startTime )
|
||
, ( "end_time", Encode.string entry.endTime )
|
||
, ( "type", Encode.string entry.entryType )
|
||
]
|
||
, expect = Http.expectWhatever AdminTimeEntrySaved
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
|
||
fetchWeekDates : String -> Int -> Int -> Cmd Msg
|
||
fetchWeekDates token year week =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/week-dates?year=" ++ String.fromInt year ++ "&week=" ++ String.fromInt week
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson WeekDatesReceived weekDatesDecoder
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
weekDatesDecoder : Decoder WeekDates
|
||
weekDatesDecoder =
|
||
Decode.map4 WeekDates
|
||
(field "year" int)
|
||
(field "week" int)
|
||
(field "dates" (Decode.dict string) |> Decode.map Dict.toList)
|
||
(field "range" string)
|
||
|
||
|
||
checkWeekHasEntries : String -> Int -> Int -> Cmd Msg
|
||
checkWeekHasEntries token year week =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/week-has-entries?year=" ++ String.fromInt year ++ "&week=" ++ String.fromInt week
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson WeekHasEntriesReceived (field "has_entries" bool)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
updateTimeEntry : String -> EditingTimeEntry -> Cmd Msg
|
||
updateTimeEntry token entry =
|
||
Http.request
|
||
{ method = "PUT"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/time-entries/" ++ String.fromInt entry.entryId
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "date", Encode.string entry.date )
|
||
, ( "start_time", Encode.string entry.startTime )
|
||
, ( "end_time", Encode.string entry.endTime )
|
||
, ( "type", Encode.string entry.entryType )
|
||
]
|
||
, expect = Http.expectWhatever TimeEntrySaved
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
deleteTimeEntry : String -> Int -> Cmd Msg
|
||
deleteTimeEntry token entryId =
|
||
Http.request
|
||
{ method = "DELETE"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/time-entries/" ++ String.fromInt entryId
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectWhatever TimeEntryDeleted
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
updateUserWorkHours : String -> Int -> String -> Cmd Msg
|
||
updateUserWorkHours token userId hours =
|
||
case String.toFloat hours of
|
||
Just workHours ->
|
||
Http.request
|
||
{ method = "PUT"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/users/" ++ String.fromInt userId
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "yearly_hours", Encode.float workHours ) ]
|
||
, expect = Http.expectWhatever UserWorkHoursSaved
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
Nothing ->
|
||
Cmd.none
|
||
|
||
|
||
resetUserPassword : String -> Int -> String -> Cmd Msg
|
||
resetUserPassword token userId newPassword =
|
||
Http.request
|
||
{ method = "PUT"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/users/" ++ String.fromInt userId ++ "/reset-password"
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "new_password", Encode.string newPassword ) ]
|
||
, expect = Http.expectWhatever ResetPasswordSaved
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
fetchMyInfo : String -> Cmd Msg
|
||
fetchMyInfo token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/my-info"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson MyInfoReceived userDecoder
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
fetchSchoolYears : String -> Cmd Msg
|
||
fetchSchoolYears token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/school-years"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson SchoolYearsReceived (Decode.list schoolYearDecoder)
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
fetchActiveSchoolYear : String -> Cmd Msg
|
||
fetchActiveSchoolYear token =
|
||
Http.request
|
||
{ method = "GET"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/school-year/active"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectJson ActiveSchoolYearReceived schoolYearDecoder
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
createSchoolYear : String -> NewSchoolYear -> Cmd Msg
|
||
createSchoolYear token schoolYear =
|
||
Http.request
|
||
{ method = "POST"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/school-years"
|
||
, body =
|
||
Http.jsonBody <|
|
||
Encode.object
|
||
[ ( "name", Encode.string schoolYear.name )
|
||
, ( "start_date", Encode.string schoolYear.startDate )
|
||
, ( "end_date", Encode.string schoolYear.endDate )
|
||
]
|
||
, expect = Http.expectWhatever SchoolYearCreated
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
activateSchoolYear : String -> Int -> Cmd Msg
|
||
activateSchoolYear token id =
|
||
Http.request
|
||
{ method = "PUT"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/school-years/" ++ String.fromInt id ++ "/activate"
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectWhatever SchoolYearActivated
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
deleteSchoolYear : String -> Int -> Cmd Msg
|
||
deleteSchoolYear token id =
|
||
Http.request
|
||
{ method = "DELETE"
|
||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||
, url = "/api/admin/school-years/" ++ String.fromInt id
|
||
, body = Http.emptyBody
|
||
, expect = Http.expectWhatever SchoolYearDeleted
|
||
, timeout = Nothing
|
||
, tracker = Nothing
|
||
}
|
||
|
||
|
||
schoolYearDecoder : Decoder SchoolYear
|
||
schoolYearDecoder =
|
||
Decode.map5 SchoolYear
|
||
(field "id" int)
|
||
(field "name" string)
|
||
(field "start_date" string)
|
||
(field "end_date" string)
|
||
(field "is_active" bool)
|