Merge branch 'improve-frontend-maintanability-by-splitting-in-sperate-files'
* improve-frontend-maintanability-by-splitting-in-sperate-files: docs: update Readme fix: fix while deleting timeentries for whole week
This commit is contained in:
commit
3c61c1cb2c
31 changed files with 5042 additions and 4398 deletions
20
README.md
20
README.md
|
|
@ -179,15 +179,15 @@ export JWT_SECRET=development-secret
|
||||||
|
|
||||||
### Umgebungsvariablen
|
### Umgebungsvariablen
|
||||||
|
|
||||||
| Variable | Beschreibung | Standard | Erforderlich |
|
| Variable | Beschreibung | Standard | Erforderlich |
|
||||||
| ------------------------ | ------------------------------------------------ | --------------------------------- | ------------ |
|
| ------------------------ | ------------------------------------------------- | ----------------------------------------------- | ------------ |
|
||||||
| `PORT` | HTTP-Server Port | `8080` | Nein |
|
| `PORT` | HTTP-Server Port | `8080` | Nein |
|
||||||
| `DB_PATH` | Pfad zur SQLite-Datenbank | `./timetracking.db` | Nein |
|
| `DB_PATH` | Pfad zur SQLite-Datenbank | `./timetracking.db` | Nein |
|
||||||
| `JWT_SECRET` | Geheimnis für JWT-Token | - | **Ja** |
|
| `JWT_SECRET` | Geheimnis für JWT-Token | - | **Ja** |
|
||||||
| `INITIAL_ADMIN_PASSWORD` | Initiales Passwort für den Admin-Benutzer | `changeme` | **Ja** |
|
| `INITIAL_ADMIN_PASSWORD` | Initiales Passwort für den Admin-Benutzer | `changeme` | **Ja** |
|
||||||
| `TZ` | Zeitzone | `Europe/Berlin` | Nein |
|
| `TZ` | Zeitzone | `Europe/Berlin` | Nein |
|
||||||
| `ENVIRONMENT` | `production` für HTTPS-Redirect und striktes CORS | `development` | Nein |
|
| `ENVIRONMENT` | `production` für HTTPS-Redirect und striktes CORS | `development` | Nein |
|
||||||
| `CORS_ALLOWED_ORIGINS` | Komma-getrennte Liste von erlaubten Origins | `*` (in dev), `http://localhost:8080` (in prod) | Nein |
|
| `CORS_ALLOWED_ORIGINS` | Komma-getrennte Liste von erlaubten Origins | `*` (in dev), `http://localhost:8080` (in prod) | Nein |
|
||||||
|
|
||||||
### Docker-Volumes
|
### Docker-Volumes
|
||||||
|
|
||||||
|
|
@ -770,6 +770,6 @@ Todo
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Version**: 1.4.1
|
**Version**: 1.5.0
|
||||||
**Letztes Update**: November 2025
|
**Letztes Update**: November 2025
|
||||||
**Entwickelt für**: Schulen zur Verwaltung von Flexistunden pädagogischer Mitarbeiter
|
**Entwickelt für**: Schulen zur Verwaltung von Flexistunden pädagogischer Mitarbeiter
|
||||||
|
|
|
||||||
|
|
@ -608,3 +608,19 @@ func DeleteSchoolYear(db *sql.DB, id int) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteNonManualTimeEntriesByUserAndWeek(db *sql.DB, userID int, year int, week int) error {
|
||||||
|
dates := calculateWeekDates(year, week)
|
||||||
|
var dateList []string
|
||||||
|
for day := 0; day <= 4; day++ {
|
||||||
|
dateList = append(dateList, dates.Dates[fmt.Sprint(day)])
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `DELETE FROM time_entries
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND type != 'manual'
|
||||||
|
AND date IN (?, ?, ?, ?, ?)`
|
||||||
|
|
||||||
|
_, err := db.Exec(query, userID, dateList[0], dateList[1], dateList[2], dateList[3], dateList[4])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -340,7 +340,7 @@ func (app *App) DeleteWeekEntries(c echo.Context) error {
|
||||||
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
return HandleError(c, ErrInvalidInputMsg("Woche"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := DeleteTimeEntriesByUserAndWeek(app.DB, claims.UserID, year, week); err != nil {
|
if err := DeleteNonManualTimeEntriesByUserAndWeek(app.DB, claims.UserID, year, week); err != nil {
|
||||||
return HandleError(c, ErrDatabaseMsg(err))
|
return HandleError(c, ErrDatabaseMsg(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,6 +417,19 @@ func (app *App) CreateBatchTimeEntriesHandler(c echo.Context) error {
|
||||||
return HandleError(c, ErrMissingFieldMsg("Zeiteinträge"))
|
return HandleError(c, ErrMissingFieldMsg("Zeiteinträge"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(req.Entries) > 0 {
|
||||||
|
firstDate := req.Entries[0].Date
|
||||||
|
t, err := time.Parse("2006-01-02", firstDate)
|
||||||
|
if err != nil {
|
||||||
|
return HandleError(c, ErrInvalidInputMsg("Datum-Format"))
|
||||||
|
}
|
||||||
|
year, week := t.ISOWeek()
|
||||||
|
|
||||||
|
if err := DeleteNonManualTimeEntriesByUserAndWeek(app.DB, claims.UserID, year, week); err != nil {
|
||||||
|
return HandleError(c, ErrDatabaseMsg(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tx, err := app.DB.Begin()
|
tx, err := app.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return HandleError(c, ErrDatabaseMsg(err))
|
return HandleError(c, ErrDatabaseMsg(err))
|
||||||
|
|
|
||||||
21
frontend/src/Api/Auth.elm
Normal file
21
frontend/src/Api/Auth.elm
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
module Api.Auth exposing (loginRequest)
|
||||||
|
|
||||||
|
import Api.Decoders exposing (loginDecoder)
|
||||||
|
import Http
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Types.Api exposing (LoginResult)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
109
frontend/src/Api/Decoders.elm
Normal file
109
frontend/src/Api/Decoders.elm
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
module Api.Decoders exposing
|
||||||
|
( apiErrorDecoder
|
||||||
|
, loginDecoder
|
||||||
|
, scheduleDecoder
|
||||||
|
, schoolYearDecoder
|
||||||
|
, timeEntryDecoder
|
||||||
|
, userDecoder
|
||||||
|
, weekDatesDecoder
|
||||||
|
, weeklyHoursDecoder
|
||||||
|
, yearlyHoursSummaryDecoder
|
||||||
|
)
|
||||||
|
|
||||||
|
import Dict
|
||||||
|
import Json.Decode as Decode exposing (Decoder, bool, field, float, int, list, string)
|
||||||
|
import Types.Api exposing (ApiError, LoginResult)
|
||||||
|
import Types.Model exposing (..)
|
||||||
|
|
||||||
|
|
||||||
|
loginDecoder : Decoder LoginResult
|
||||||
|
loginDecoder =
|
||||||
|
Decode.map3 LoginResult
|
||||||
|
(field "token" string)
|
||||||
|
(field "username" string)
|
||||||
|
(field "is_admin" bool)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
userDecoder : Decoder User
|
||||||
|
userDecoder =
|
||||||
|
Decode.map4 User
|
||||||
|
(field "id" int)
|
||||||
|
(field "username" string)
|
||||||
|
(field "is_admin" bool)
|
||||||
|
(field "yearly_hours" float)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
apiErrorDecoder : Decoder ApiError
|
||||||
|
apiErrorDecoder =
|
||||||
|
Decode.map2 ApiError
|
||||||
|
(field "code" string)
|
||||||
|
(field "message" string)
|
||||||
120
frontend/src/Api/Schedule.elm
Normal file
120
frontend/src/Api/Schedule.elm
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
module Api.Schedule exposing
|
||||||
|
( createSchedule
|
||||||
|
, deleteSchedule
|
||||||
|
, fetchSchedules
|
||||||
|
, saveTimeEntriesForWeek
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.Decoders exposing (scheduleDecoder)
|
||||||
|
import Http
|
||||||
|
import Json.Decode
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Types.Model exposing (NewSchedule, Schedule, SelectedEntry, WeekDates)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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 (Json.Decode.list scheduleDecoder)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
85
frontend/src/Api/SchoolYear.elm
Normal file
85
frontend/src/Api/SchoolYear.elm
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
module Api.SchoolYear exposing
|
||||||
|
( activateSchoolYear
|
||||||
|
, createSchoolYear
|
||||||
|
, deleteSchoolYear
|
||||||
|
, fetchActiveSchoolYear
|
||||||
|
, fetchSchoolYears
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.Decoders exposing (schoolYearDecoder)
|
||||||
|
import Http
|
||||||
|
import Json.Decode as Decode
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Types.Model exposing (NewSchoolYear)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
201
frontend/src/Api/TimeEntry.elm
Normal file
201
frontend/src/Api/TimeEntry.elm
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
module Api.TimeEntry exposing
|
||||||
|
( checkWeekHasEntries
|
||||||
|
, createAdminTimeEntry
|
||||||
|
, deleteTimeEntry
|
||||||
|
, deleteWeekEntries
|
||||||
|
, downloadYearlySummaryPDF
|
||||||
|
, fetchAllTimeEntries
|
||||||
|
, fetchMyTimeEntries
|
||||||
|
, fetchWeekDates
|
||||||
|
, fetchWeeklyHours
|
||||||
|
, fetchYearlyHoursSummary
|
||||||
|
, updateTimeEntry
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.Decoders exposing (timeEntryDecoder, weekDatesDecoder, yearlyHoursSummaryDecoder)
|
||||||
|
import Bytes exposing (Bytes)
|
||||||
|
import Http
|
||||||
|
import Json.Decode as Decode exposing (bool, field)
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Types.Model exposing (AdminManualEntry, EditingTimeEntry)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 )
|
||||||
|
, ( "hours", Encode.float (String.toFloat entry.hours |> Maybe.withDefault 0) )
|
||||||
|
, ( "type", Encode.string "manual" )
|
||||||
|
]
|
||||||
|
, expect = Http.expectWhatever AdminTimeEntrySaved
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
downloadYearlySummaryPDF : String -> Cmd Msg
|
||||||
|
downloadYearlySummaryPDF token =
|
||||||
|
Http.request
|
||||||
|
{ method = "GET"
|
||||||
|
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||||
|
, url = "/api/admin/yearly-summary/pdf"
|
||||||
|
, body = Http.emptyBody
|
||||||
|
, expect =
|
||||||
|
Http.expectBytesResponse YearlySummaryPDFReceived
|
||||||
|
(\response ->
|
||||||
|
case response of
|
||||||
|
Http.GoodStatus_ _ body ->
|
||||||
|
Ok body
|
||||||
|
|
||||||
|
Http.BadUrl_ url ->
|
||||||
|
Err (Http.BadUrl url)
|
||||||
|
|
||||||
|
Http.Timeout_ ->
|
||||||
|
Err Http.Timeout
|
||||||
|
|
||||||
|
Http.NetworkError_ ->
|
||||||
|
Err Http.NetworkError
|
||||||
|
|
||||||
|
Http.BadStatus_ metadata _ ->
|
||||||
|
Err (Http.BadStatus metadata.statusCode)
|
||||||
|
)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 Api.Decoders.weeklyHoursDecoder)
|
||||||
|
, timeout = Nothing
|
||||||
|
, tracker = Nothing
|
||||||
|
}
|
||||||
110
frontend/src/Api/User.elm
Normal file
110
frontend/src/Api/User.elm
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
module Api.User exposing
|
||||||
|
( createUser
|
||||||
|
, deleteUser
|
||||||
|
, fetchMyInfo
|
||||||
|
, fetchUsers
|
||||||
|
, resetUserPassword
|
||||||
|
, updateUserWorkHours
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.Decoders exposing (userDecoder)
|
||||||
|
import Http
|
||||||
|
import Json.Decode as Decode
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Types.Model exposing (NewUser)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load diff
17
frontend/src/Types/Api.elm
Normal file
17
frontend/src/Types/Api.elm
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Types.Api exposing
|
||||||
|
( ApiError
|
||||||
|
, LoginResult
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type alias LoginResult =
|
||||||
|
{ token : String
|
||||||
|
, username : String
|
||||||
|
, isAdmin : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias ApiError =
|
||||||
|
{ code : String
|
||||||
|
, message : String
|
||||||
|
}
|
||||||
218
frontend/src/Types/Model.elm
Normal file
218
frontend/src/Types/Model.elm
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
module Types.Model exposing
|
||||||
|
( AdminManualEntry
|
||||||
|
, EditingTimeEntry
|
||||||
|
, Flags
|
||||||
|
, Model
|
||||||
|
, NewSchedule
|
||||||
|
, NewSchoolYear
|
||||||
|
, NewUser
|
||||||
|
, Schedule
|
||||||
|
, SchoolYear
|
||||||
|
, SelectedEntry
|
||||||
|
, TimeEntry
|
||||||
|
, Toast
|
||||||
|
, ToastType(..)
|
||||||
|
, User
|
||||||
|
, WeekDates
|
||||||
|
, WeeklyHours
|
||||||
|
, WeeklySummary
|
||||||
|
, YearlyHoursSummary
|
||||||
|
)
|
||||||
|
|
||||||
|
import Time
|
||||||
|
import Types.Page exposing (AdminTab, Page)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
, toasts : List Toast
|
||||||
|
, nextToastId : Int
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ToastType
|
||||||
|
= ErrorToast
|
||||||
|
| SuccessToast
|
||||||
|
| InfoToast
|
||||||
|
| WarningToast
|
||||||
|
|
||||||
|
|
||||||
|
type alias Toast =
|
||||||
|
{ id : Int
|
||||||
|
, message : String
|
||||||
|
, toastType : ToastType
|
||||||
|
, dismissible : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type alias Flags =
|
||||||
|
{ token : Maybe String
|
||||||
|
, isAdmin : Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
, hours : 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
|
||||||
|
}
|
||||||
133
frontend/src/Types/Msg.elm
Normal file
133
frontend/src/Types/Msg.elm
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
module Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
import Bytes exposing (Bytes)
|
||||||
|
import Http
|
||||||
|
import Time
|
||||||
|
import Types.Api exposing (LoginResult)
|
||||||
|
import Types.Model
|
||||||
|
exposing
|
||||||
|
( Schedule
|
||||||
|
, SchoolYear
|
||||||
|
, TimeEntry
|
||||||
|
, ToastType(..)
|
||||||
|
, User
|
||||||
|
, WeekDates
|
||||||
|
, WeeklyHours
|
||||||
|
, WeeklySummary
|
||||||
|
, YearlyHoursSummary
|
||||||
|
)
|
||||||
|
import Types.Page exposing (AdminTab)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
| UpdateManualEntryHours 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 ())
|
||||||
|
| DownloadYearlySummaryPDF
|
||||||
|
| YearlySummaryPDFReceived (Result Http.Error Bytes)
|
||||||
|
| ShowToast String ToastType
|
||||||
|
| DismissToast Int
|
||||||
|
| AutoDismissToast Int
|
||||||
17
frontend/src/Types/Page.elm
Normal file
17
frontend/src/Types/Page.elm
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
module Types.Page exposing
|
||||||
|
( AdminTab(..)
|
||||||
|
, Page(..)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
type Page
|
||||||
|
= LoginPage
|
||||||
|
| UserDashboard
|
||||||
|
| AdminDashboard
|
||||||
|
|
||||||
|
|
||||||
|
type AdminTab
|
||||||
|
= ScheduleTab
|
||||||
|
| UsersTab
|
||||||
|
| TimeEntriesTab
|
||||||
|
| SchoolYearsTab
|
||||||
115
frontend/src/Update/AuthUpdate.elm
Normal file
115
frontend/src/Update/AuthUpdate.elm
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
module Update.AuthUpdate exposing
|
||||||
|
( handleLogin
|
||||||
|
, handleLoginResponse
|
||||||
|
, handleLogout
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.Auth
|
||||||
|
import Api.Schedule
|
||||||
|
import Api.SchoolYear
|
||||||
|
import Api.TimeEntry
|
||||||
|
import Api.User
|
||||||
|
import Http
|
||||||
|
import Json.Encode as Encode
|
||||||
|
import Task
|
||||||
|
import Types.Model exposing (Model, ToastType(..))
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Types.Page exposing (Page(..))
|
||||||
|
import Utils.DateUtils exposing (getISOWeekFromPosix)
|
||||||
|
import Utils.Ports exposing (removeToken, saveToken)
|
||||||
|
|
||||||
|
|
||||||
|
handleLogin : Model -> ( Model, Cmd Msg )
|
||||||
|
handleLogin model =
|
||||||
|
if model.isProcessing then
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
else
|
||||||
|
( { model | isProcessing = True }, Api.Auth.loginRequest model.username model.password )
|
||||||
|
|
||||||
|
|
||||||
|
handleLoginResponse : Result Http.Error { token : String, username : String, isAdmin : Bool } -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleLoginResponse result model =
|
||||||
|
case result of
|
||||||
|
Ok loginResult ->
|
||||||
|
let
|
||||||
|
newPage =
|
||||||
|
if loginResult.isAdmin then
|
||||||
|
AdminDashboard
|
||||||
|
|
||||||
|
else
|
||||||
|
UserDashboard
|
||||||
|
|
||||||
|
( year, week ) =
|
||||||
|
getISOWeekFromPosix model.currentTime
|
||||||
|
|
||||||
|
tokenData =
|
||||||
|
Encode.object
|
||||||
|
[ ( "token", Encode.string loginResult.token )
|
||||||
|
, ( "isAdmin", Encode.bool loginResult.isAdmin )
|
||||||
|
]
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| token = Just loginResult.token
|
||||||
|
, username = loginResult.username
|
||||||
|
, isAdmin = loginResult.isAdmin
|
||||||
|
, page = newPage
|
||||||
|
, error = Nothing
|
||||||
|
, isProcessing = False
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ saveToken tokenData
|
||||||
|
, Api.Schedule.fetchSchedules (Just loginResult.token)
|
||||||
|
, Task.perform (\_ -> ShowToast ("Willkommen, " ++ loginResult.username ++ "!") SuccessToast) (Task.succeed ())
|
||||||
|
, if not loginResult.isAdmin then
|
||||||
|
Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchMyTimeEntries loginResult.token
|
||||||
|
, Api.TimeEntry.fetchWeekDates loginResult.token year week
|
||||||
|
, Api.TimeEntry.checkWeekHasEntries loginResult.token year week
|
||||||
|
, Api.TimeEntry.fetchYearlyHoursSummary loginResult.token
|
||||||
|
, Api.User.fetchMyInfo loginResult.token
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchMyTimeEntries loginResult.token
|
||||||
|
, Api.TimeEntry.fetchWeekDates loginResult.token year week
|
||||||
|
, Api.TimeEntry.checkWeekHasEntries loginResult.token year week
|
||||||
|
, Api.TimeEntry.fetchYearlyHoursSummary loginResult.token
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
let
|
||||||
|
errorMsg =
|
||||||
|
case err of
|
||||||
|
Http.BadStatus 401 ->
|
||||||
|
"Benutzername oder Passwort ungültig"
|
||||||
|
|
||||||
|
Http.Timeout ->
|
||||||
|
"Zeitüberschreitung - bitte erneut versuchen"
|
||||||
|
|
||||||
|
Http.NetworkError ->
|
||||||
|
"Netzwerkfehler - bitte Verbindung prüfen"
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
"Anmeldung fehlgeschlagen"
|
||||||
|
in
|
||||||
|
( { model | isProcessing = False }
|
||||||
|
, Task.perform (\_ -> ShowToast errorMsg ErrorToast) (Task.succeed ())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
handleLogout : Model -> ( Model, Cmd Msg )
|
||||||
|
handleLogout model =
|
||||||
|
( { model
|
||||||
|
| page = LoginPage
|
||||||
|
, token = Nothing
|
||||||
|
, isAdmin = False
|
||||||
|
, username = ""
|
||||||
|
, password = ""
|
||||||
|
, isProcessing = False
|
||||||
|
}
|
||||||
|
, removeToken ()
|
||||||
|
)
|
||||||
244
frontend/src/Update/ScheduleUpdate.elm
Normal file
244
frontend/src/Update/ScheduleUpdate.elm
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
module Update.ScheduleUpdate exposing
|
||||||
|
( handleCreateSchedule
|
||||||
|
, handleDeleteSchedule
|
||||||
|
, handleDeleteWeekEntries
|
||||||
|
, handleDisableEditMode
|
||||||
|
, handleEnableEditMode
|
||||||
|
, handleSaveTimeEntries
|
||||||
|
, handleScheduleCreated
|
||||||
|
, handleScheduleDeleted
|
||||||
|
, handleSchedulesReceived
|
||||||
|
, handleTimeEntriesSaved
|
||||||
|
, handleToggleScheduleSelection
|
||||||
|
, handleWeekEntriesDeleted
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.Schedule
|
||||||
|
import Api.TimeEntry
|
||||||
|
import Http
|
||||||
|
import Task
|
||||||
|
import Types.Model exposing (Model, NewSchedule, Schedule, SelectedEntry, ToastType(..))
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Utils.DateUtils exposing (getDayOfWeek, getYearWeekFromDate)
|
||||||
|
|
||||||
|
|
||||||
|
handleToggleScheduleSelection : Int -> Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleToggleScheduleSelection scheduleId dayOfWeek model =
|
||||||
|
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 )
|
||||||
|
|
||||||
|
|
||||||
|
handleSaveTimeEntries : Model -> ( Model, Cmd Msg )
|
||||||
|
handleSaveTimeEntries model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | error = Nothing }
|
||||||
|
, Api.Schedule.saveTimeEntriesForWeek token model.selectedEntries model.currentYear model.currentWeek model.schedules model.weekDates
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleTimeEntriesSaved : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleTimeEntriesSaved result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| error = Nothing
|
||||||
|
, weekEditMode = False
|
||||||
|
, hasEntriesForCurrentWeek = True
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchMyTimeEntries token
|
||||||
|
, Task.perform (\_ -> ShowToast "Zeiteinträge erfolgreich gespeichert!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleEnableEditMode : Model -> ( Model, Cmd Msg )
|
||||||
|
handleEnableEditMode model =
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
handleDisableEditMode : Model -> ( Model, Cmd Msg )
|
||||||
|
handleDisableEditMode model =
|
||||||
|
( { model | weekEditMode = False }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleDeleteWeekEntries : Model -> ( Model, Cmd Msg )
|
||||||
|
handleDeleteWeekEntries model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.deleteWeekEntries token model.currentYear model.currentWeek )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleWeekEntriesDeleted : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleWeekEntriesDeleted result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| weekEditMode = True
|
||||||
|
, selectedEntries = []
|
||||||
|
, hasEntriesForCurrentWeek = False
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchMyTimeEntries token
|
||||||
|
, Task.perform (\_ -> ShowToast "Wocheneinträge erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleCreateSchedule : Model -> ( Model, Cmd Msg )
|
||||||
|
handleCreateSchedule model =
|
||||||
|
if
|
||||||
|
String.isEmpty model.newSchedule.dayOfWeek
|
||||||
|
|| String.isEmpty model.newSchedule.startTime
|
||||||
|
|| String.isEmpty model.newSchedule.endTime
|
||||||
|
then
|
||||||
|
( model, Task.perform (\_ -> ShowToast "Bitte alle Felder ausfüllen" WarningToast) (Task.succeed ()) )
|
||||||
|
|
||||||
|
else
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | isProcessing = True }, Api.Schedule.createSchedule token model.newSchedule )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleScheduleCreated : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleScheduleCreated result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
let
|
||||||
|
emptySchedule =
|
||||||
|
NewSchedule "" "" "" "lesson" ""
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| newSchedule = emptySchedule
|
||||||
|
, error = Nothing
|
||||||
|
, isProcessing = False
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.Schedule.fetchSchedules model.token
|
||||||
|
, Task.perform (\_ -> ShowToast "Stundenplan erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( { model | isProcessing = False }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleDeleteSchedule : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleDeleteSchedule scheduleId model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.Schedule.deleteSchedule token scheduleId )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleScheduleDeleted : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleScheduleDeleted result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | error = Nothing }
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.Schedule.fetchSchedules (Just token)
|
||||||
|
, Task.perform (\_ -> ShowToast "Stundenplan erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleSchedulesReceived : Result Http.Error (List Schedule) -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleSchedulesReceived result model =
|
||||||
|
case result of
|
||||||
|
Ok schedules ->
|
||||||
|
( { model | schedules = schedules }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
139
frontend/src/Update/SchoolYearUpdate.elm
Normal file
139
frontend/src/Update/SchoolYearUpdate.elm
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
module Update.SchoolYearUpdate exposing
|
||||||
|
( handleActivateSchoolYear
|
||||||
|
, handleActiveSchoolYearReceived
|
||||||
|
, handleCreateSchoolYear
|
||||||
|
, handleDeleteSchoolYear
|
||||||
|
, handleSchoolYearActivated
|
||||||
|
, handleSchoolYearCreated
|
||||||
|
, handleSchoolYearDeleted
|
||||||
|
, handleSchoolYearsReceived
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.SchoolYear
|
||||||
|
import Http
|
||||||
|
import Task
|
||||||
|
import Types.Model exposing (Model, NewSchoolYear, SchoolYear, ToastType(..))
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
handleCreateSchoolYear : Model -> ( Model, Cmd Msg )
|
||||||
|
handleCreateSchoolYear model =
|
||||||
|
if
|
||||||
|
String.isEmpty model.newSchoolYear.name
|
||||||
|
|| String.isEmpty model.newSchoolYear.startDate
|
||||||
|
|| String.isEmpty model.newSchoolYear.endDate
|
||||||
|
then
|
||||||
|
( model, Task.perform (\_ -> ShowToast "Bitte alle Felder ausfüllen" WarningToast) (Task.succeed ()) )
|
||||||
|
|
||||||
|
else
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | isProcessing = True }, Api.SchoolYear.createSchoolYear token model.newSchoolYear )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleSchoolYearCreated : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleSchoolYearCreated result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| newSchoolYear = NewSchoolYear "" "" ""
|
||||||
|
, error = Nothing
|
||||||
|
, isProcessing = False
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.SchoolYear.fetchSchoolYears token
|
||||||
|
, Task.perform (\_ -> ShowToast "Schuljahr erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( { model | isProcessing = False }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleActivateSchoolYear : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleActivateSchoolYear id model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.SchoolYear.activateSchoolYear token id )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleSchoolYearActivated : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleSchoolYearActivated result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | error = Nothing }
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.SchoolYear.fetchSchoolYears token
|
||||||
|
, Api.SchoolYear.fetchActiveSchoolYear token
|
||||||
|
, Task.perform (\_ -> ShowToast "Schuljahr erfolgreich aktiviert!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleDeleteSchoolYear : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleDeleteSchoolYear id model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.SchoolYear.deleteSchoolYear token id )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleSchoolYearDeleted : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleSchoolYearDeleted result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | error = Nothing }
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.SchoolYear.fetchSchoolYears token
|
||||||
|
, Task.perform (\_ -> ShowToast "Schuljahr erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleSchoolYearsReceived : Result Http.Error (List SchoolYear) -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleSchoolYearsReceived result model =
|
||||||
|
case result of
|
||||||
|
Ok years ->
|
||||||
|
( { model | schoolYears = years }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleActiveSchoolYearReceived : Result Http.Error SchoolYear -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleActiveSchoolYearReceived result model =
|
||||||
|
case result of
|
||||||
|
Ok year ->
|
||||||
|
( { model | activeSchoolYear = Just year }, Cmd.none )
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
( { model | activeSchoolYear = Nothing }, Cmd.none )
|
||||||
189
frontend/src/Update/TimeEntryUpdate.elm
Normal file
189
frontend/src/Update/TimeEntryUpdate.elm
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
module Update.TimeEntryUpdate exposing
|
||||||
|
( handleAdminTimeEntrySaved
|
||||||
|
, handleAllTimeEntriesReceived
|
||||||
|
, handleConfirmDeleteTimeEntry
|
||||||
|
, handleEditTimeEntry
|
||||||
|
, handleMyTimeEntriesReceived
|
||||||
|
, handleSaveAdminTimeEntry
|
||||||
|
, handleSaveEditTimeEntry
|
||||||
|
, handleTimeEntryDeleted
|
||||||
|
, handleTimeEntrySaved
|
||||||
|
, handleYearlyHoursSummaryReceived
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.TimeEntry
|
||||||
|
import Http
|
||||||
|
import Task
|
||||||
|
import Types.Model exposing (AdminManualEntry, EditingTimeEntry, Model, TimeEntry, ToastType(..), YearlyHoursSummary)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Utils.DateUtils exposing (getYearWeekFromDate)
|
||||||
|
import Utils.Ports exposing (confirmDelete)
|
||||||
|
|
||||||
|
|
||||||
|
handleMyTimeEntriesReceived : Result Http.Error (List TimeEntry) -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleMyTimeEntriesReceived result model =
|
||||||
|
case result of
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleAllTimeEntriesReceived : Result Http.Error (List TimeEntry) -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleAllTimeEntriesReceived result model =
|
||||||
|
case result of
|
||||||
|
Ok entries ->
|
||||||
|
( { model | timeEntries = entries }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleEditTimeEntry : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleEditTimeEntry entryId model =
|
||||||
|
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 )
|
||||||
|
|
||||||
|
|
||||||
|
handleSaveEditTimeEntry : Model -> ( Model, Cmd Msg )
|
||||||
|
handleSaveEditTimeEntry model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.updateTimeEntry token model.editingTimeEntry )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleTimeEntrySaved : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleTimeEntrySaved result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| editingTimeEntryId = Nothing
|
||||||
|
, pendingDeleteId = Nothing
|
||||||
|
, error = Nothing
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchAllTimeEntries token
|
||||||
|
, Task.perform (\_ -> ShowToast "Zeiteintrag erfolgreich gespeichert!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleTimeEntryDeleted : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleTimeEntryDeleted result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| editingTimeEntryId = Nothing
|
||||||
|
, editingTimeEntry = EditingTimeEntry 0 "" "" "" "lesson"
|
||||||
|
, pendingDeleteId = Nothing
|
||||||
|
, error = Nothing
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchAllTimeEntries token
|
||||||
|
, Api.TimeEntry.fetchYearlyHoursSummary token
|
||||||
|
, Task.perform (\_ -> ShowToast "Zeiteintrag erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( { model | pendingDeleteId = Nothing }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleConfirmDeleteTimeEntry : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleConfirmDeleteTimeEntry entryId model =
|
||||||
|
( { model | pendingDeleteId = Just entryId }, confirmDelete "Soll dieser Zeiteintrag gelöscht werden?" )
|
||||||
|
|
||||||
|
|
||||||
|
handleSaveAdminTimeEntry : Model -> ( Model, Cmd Msg )
|
||||||
|
handleSaveAdminTimeEntry model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | isProcessing = True }, Api.TimeEntry.createAdminTimeEntry token model.adminManualEntryForm )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleAdminTimeEntrySaved : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleAdminTimeEntrySaved result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| adminManualEntryForm = AdminManualEntry Nothing "" "" "manual"
|
||||||
|
, error = Nothing
|
||||||
|
, isProcessing = False
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchAllTimeEntries token
|
||||||
|
, Api.TimeEntry.fetchYearlyHoursSummary token
|
||||||
|
, Task.perform (\_ -> ShowToast "Manueller Eintrag erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( { model | isProcessing = False }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleYearlyHoursSummaryReceived : Result Http.Error (List YearlyHoursSummary) -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleYearlyHoursSummaryReceived result model =
|
||||||
|
case result of
|
||||||
|
Ok summary ->
|
||||||
|
( { model | yearlyHoursSummary = summary }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
811
frontend/src/Update/Update.elm
Normal file
811
frontend/src/Update/Update.elm
Normal file
|
|
@ -0,0 +1,811 @@
|
||||||
|
module Update.Update exposing (update)
|
||||||
|
|
||||||
|
import Api.Schedule
|
||||||
|
import Api.SchoolYear
|
||||||
|
import Api.TimeEntry
|
||||||
|
import Api.User
|
||||||
|
import File.Download
|
||||||
|
import Process
|
||||||
|
import Task
|
||||||
|
import Time
|
||||||
|
import Types.Model exposing (EditingTimeEntry, Model, NewUser, ToastType(..))
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Types.Page exposing (AdminTab(..), Page(..))
|
||||||
|
import Update.AuthUpdate as Auth
|
||||||
|
import Update.ScheduleUpdate as Schedule
|
||||||
|
import Update.SchoolYearUpdate as SchoolYear
|
||||||
|
import Update.TimeEntryUpdate as TimeEntry
|
||||||
|
import Update.UserUpdate as User
|
||||||
|
import Utils.DateUtils exposing (getISOWeekFromPosix, nextWeek, previousWeek)
|
||||||
|
import Utils.Ports
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
-- Mobile Menu
|
||||||
|
ToggleMobileMenu ->
|
||||||
|
( { model | mobileMenuOpen = not model.mobileMenuOpen }, Cmd.none )
|
||||||
|
|
||||||
|
CloseMobileMenu ->
|
||||||
|
( { model | mobileMenuOpen = False }, Cmd.none )
|
||||||
|
|
||||||
|
-- Auth
|
||||||
|
UpdateUsername username ->
|
||||||
|
( { model | username = username }, Cmd.none )
|
||||||
|
|
||||||
|
UpdatePassword password ->
|
||||||
|
( { model | password = password }, Cmd.none )
|
||||||
|
|
||||||
|
Login ->
|
||||||
|
Auth.handleLogin model
|
||||||
|
|
||||||
|
LoginResponse result ->
|
||||||
|
Auth.handleLoginResponse result model
|
||||||
|
|
||||||
|
Logout ->
|
||||||
|
Auth.handleLogout model
|
||||||
|
|
||||||
|
-- Time
|
||||||
|
SetTime time ->
|
||||||
|
let
|
||||||
|
( year, week ) =
|
||||||
|
getISOWeekFromPosix time
|
||||||
|
|
||||||
|
cmds =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
if model.page == UserDashboard || model.page == LoginPage then
|
||||||
|
Cmd.batch
|
||||||
|
[ Api.TimeEntry.checkWeekHasEntries token year week
|
||||||
|
, Api.TimeEntry.fetchWeekDates token year week
|
||||||
|
, Api.TimeEntry.fetchMyTimeEntries token
|
||||||
|
]
|
||||||
|
|
||||||
|
else
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| currentTime = time
|
||||||
|
, currentWeek = week
|
||||||
|
, currentYear = year
|
||||||
|
}
|
||||||
|
, cmds
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Schedules
|
||||||
|
FetchSchedules ->
|
||||||
|
( model, Api.Schedule.fetchSchedules model.token )
|
||||||
|
|
||||||
|
SchedulesReceived result ->
|
||||||
|
Schedule.handleSchedulesReceived result model
|
||||||
|
|
||||||
|
ToggleScheduleSelection scheduleId dayOfWeek ->
|
||||||
|
Schedule.handleToggleScheduleSelection scheduleId dayOfWeek model
|
||||||
|
|
||||||
|
SaveTimeEntries ->
|
||||||
|
Schedule.handleSaveTimeEntries model
|
||||||
|
|
||||||
|
TimeEntriesSaved result ->
|
||||||
|
Schedule.handleTimeEntriesSaved result model
|
||||||
|
|
||||||
|
EnableEditMode ->
|
||||||
|
Schedule.handleEnableEditMode model
|
||||||
|
|
||||||
|
DisableEditMode ->
|
||||||
|
Schedule.handleDisableEditMode model
|
||||||
|
|
||||||
|
DeleteWeekEntries ->
|
||||||
|
Schedule.handleDeleteWeekEntries model
|
||||||
|
|
||||||
|
WeekEntriesDeleted result ->
|
||||||
|
Schedule.handleWeekEntriesDeleted result model
|
||||||
|
|
||||||
|
CreateSchedule ->
|
||||||
|
Schedule.handleCreateSchedule model
|
||||||
|
|
||||||
|
ScheduleCreated result ->
|
||||||
|
Schedule.handleScheduleCreated result model
|
||||||
|
|
||||||
|
DeleteSchedule scheduleId ->
|
||||||
|
Schedule.handleDeleteSchedule scheduleId model
|
||||||
|
|
||||||
|
ScheduleDeleted result ->
|
||||||
|
Schedule.handleScheduleDeleted result model
|
||||||
|
|
||||||
|
-- Week Navigation
|
||||||
|
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
|
||||||
|
[ Api.TimeEntry.fetchWeekDates token newYear newWeek
|
||||||
|
, Api.TimeEntry.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
|
||||||
|
[ Api.TimeEntry.fetchWeekDates token newYear newWeek
|
||||||
|
, Api.TimeEntry.checkWeekHasEntries token newYear newWeek
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
FetchWeekDates ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.fetchWeekDates token model.currentYear model.currentWeek )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
WeekDatesReceived result ->
|
||||||
|
case result of
|
||||||
|
Ok weekDates ->
|
||||||
|
( { model | weekDates = Just weekDates }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
CheckWeekHasEntries ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.checkWeekHasEntries token model.currentYear model.currentWeek )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
WeekHasEntriesReceived result ->
|
||||||
|
case result of
|
||||||
|
Ok hasEntries ->
|
||||||
|
( { model | hasEntriesForCurrentWeek = hasEntries }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
-- Admin Tabs
|
||||||
|
SwitchTab tab ->
|
||||||
|
let
|
||||||
|
cmd =
|
||||||
|
case tab of
|
||||||
|
UsersTab ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
Api.User.fetchUsers token
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
TimeEntriesTab ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
Cmd.batch
|
||||||
|
[ Api.TimeEntry.fetchAllTimeEntries token
|
||||||
|
, Api.TimeEntry.fetchYearlyHoursSummary token
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
SchoolYearsTab ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
Cmd.batch
|
||||||
|
[ Api.SchoolYear.fetchSchoolYears token
|
||||||
|
, Api.SchoolYear.fetchActiveSchoolYear token
|
||||||
|
]
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Cmd.none
|
||||||
|
in
|
||||||
|
( { model | activeTab = tab, mobileMenuOpen = False }, cmd )
|
||||||
|
|
||||||
|
-- Schedule Form
|
||||||
|
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 )
|
||||||
|
|
||||||
|
-- Users
|
||||||
|
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 ->
|
||||||
|
User.handleCreateUser model
|
||||||
|
|
||||||
|
UserCreated result ->
|
||||||
|
User.handleUserCreated result model
|
||||||
|
|
||||||
|
DeleteUser userId ->
|
||||||
|
User.handleDeleteUser userId model
|
||||||
|
|
||||||
|
UserDeleted result ->
|
||||||
|
User.handleUserDeleted result model
|
||||||
|
|
||||||
|
FetchUsers ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.User.fetchUsers token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
UsersReceived result ->
|
||||||
|
User.handleUsersReceived result model
|
||||||
|
|
||||||
|
EditUserWorkHours userId ->
|
||||||
|
User.handleEditUserWorkHours userId model
|
||||||
|
|
||||||
|
CancelEditUserWorkHours ->
|
||||||
|
( { model
|
||||||
|
| editingUserId = Nothing
|
||||||
|
, editingUserWorkHours = ""
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateEditUserWorkHours hours ->
|
||||||
|
( { model | editingUserWorkHours = hours }, Cmd.none )
|
||||||
|
|
||||||
|
SaveUserWorkHours ->
|
||||||
|
User.handleSaveUserWorkHours model
|
||||||
|
|
||||||
|
UserWorkHoursSaved result ->
|
||||||
|
User.handleUserWorkHoursSaved result model
|
||||||
|
|
||||||
|
ResetUserPassword userId ->
|
||||||
|
User.handleResetUserPassword userId model
|
||||||
|
|
||||||
|
CancelResetPassword ->
|
||||||
|
( { model
|
||||||
|
| resetPasswordUserId = Nothing
|
||||||
|
, resetPasswordNew = ""
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateResetPasswordNew password ->
|
||||||
|
( { model | resetPasswordNew = password }, Cmd.none )
|
||||||
|
|
||||||
|
SaveResetPassword ->
|
||||||
|
User.handleSaveResetPassword model
|
||||||
|
|
||||||
|
ResetPasswordSaved result ->
|
||||||
|
User.handleResetPasswordSaved result model
|
||||||
|
|
||||||
|
UpdateUserWorkHours input ->
|
||||||
|
( { model | userWorkHoursInput = input }, 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, Api.User.resetUserPassword token userId model.userPasswordInput )
|
||||||
|
|
||||||
|
else
|
||||||
|
( model, Task.perform (\_ -> ShowToast "Passwort erforderlich" WarningToast) (Task.succeed ()) )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, Task.perform (\_ -> ShowToast "Passwort erforderlich" WarningToast) (Task.succeed ()) )
|
||||||
|
|
||||||
|
UserPasswordSaved result ->
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
( { model
|
||||||
|
| userPasswordInput = ""
|
||||||
|
, selectedUserId = Nothing
|
||||||
|
, error = Nothing
|
||||||
|
}
|
||||||
|
, Task.perform (\_ -> ShowToast "Passwort erfolgreich zurückgesetzt!" SuccessToast) (Task.succeed ())
|
||||||
|
)
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
SelectUserForManagement userId ->
|
||||||
|
( { model | selectedUserId = Just userId, userWorkHoursInput = "", userPasswordInput = "" }, Cmd.none )
|
||||||
|
|
||||||
|
-- Time Entries
|
||||||
|
FetchMyTimeEntries ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.fetchMyTimeEntries token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
MyTimeEntriesReceived result ->
|
||||||
|
TimeEntry.handleMyTimeEntriesReceived result model
|
||||||
|
|
||||||
|
FetchAllTimeEntries ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.fetchAllTimeEntries token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
AllTimeEntriesReceived result ->
|
||||||
|
TimeEntry.handleAllTimeEntriesReceived result model
|
||||||
|
|
||||||
|
EditTimeEntry entryId ->
|
||||||
|
TimeEntry.handleEditTimeEntry entryId model
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
TimeEntry.handleSaveEditTimeEntry model
|
||||||
|
|
||||||
|
TimeEntrySaved result ->
|
||||||
|
TimeEntry.handleTimeEntrySaved result model
|
||||||
|
|
||||||
|
TimeEntryDeleted result ->
|
||||||
|
TimeEntry.handleTimeEntryDeleted result model
|
||||||
|
|
||||||
|
ConfirmDeleteTimeEntry entryId ->
|
||||||
|
TimeEntry.handleConfirmDeleteTimeEntry entryId model
|
||||||
|
|
||||||
|
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, Api.TimeEntry.updateTimeEntry token model.editingTimeEntry )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
-- Weekly Hours
|
||||||
|
FetchWeeklyHours ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
WeeklyHoursReceived result ->
|
||||||
|
case result of
|
||||||
|
Ok hours ->
|
||||||
|
( { model | weeklyHours = hours }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
MyWeeklySummaryReceived result ->
|
||||||
|
case result of
|
||||||
|
Ok summary ->
|
||||||
|
( { model | userWeeklySummary = Just summary }, Cmd.none )
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
( { model | userWeeklySummary = Nothing }, Cmd.none )
|
||||||
|
|
||||||
|
-- Yearly Hours
|
||||||
|
FetchYearlyHoursSummary ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.TimeEntry.fetchYearlyHoursSummary token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
YearlyHoursSummaryReceived result ->
|
||||||
|
TimeEntry.handleYearlyHoursSummaryReceived result model
|
||||||
|
|
||||||
|
-- Admin Manual Entry
|
||||||
|
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 )
|
||||||
|
|
||||||
|
UpdateManualEntryHours hours ->
|
||||||
|
let
|
||||||
|
form =
|
||||||
|
model.adminManualEntryForm
|
||||||
|
in
|
||||||
|
( { model | adminManualEntryForm = { form | hours = hours } }, Cmd.none )
|
||||||
|
|
||||||
|
UpdateManualEntryType entryType ->
|
||||||
|
let
|
||||||
|
form =
|
||||||
|
model.adminManualEntryForm
|
||||||
|
in
|
||||||
|
( { model | adminManualEntryForm = { form | entryType = entryType } }, Cmd.none )
|
||||||
|
|
||||||
|
SaveAdminTimeEntry ->
|
||||||
|
TimeEntry.handleSaveAdminTimeEntry model
|
||||||
|
|
||||||
|
AdminTimeEntrySaved result ->
|
||||||
|
TimeEntry.handleAdminTimeEntrySaved result model
|
||||||
|
|
||||||
|
-- My Info
|
||||||
|
FetchMyInfo ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.User.fetchMyInfo token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
MyInfoReceived result ->
|
||||||
|
case result of
|
||||||
|
Ok user ->
|
||||||
|
( { model | users = [ user ] }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
-- School Years
|
||||||
|
FetchSchoolYears ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.SchoolYear.fetchSchoolYears token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
SchoolYearsReceived result ->
|
||||||
|
SchoolYear.handleSchoolYearsReceived result model
|
||||||
|
|
||||||
|
FetchActiveSchoolYear ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.SchoolYear.fetchActiveSchoolYear token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
ActiveSchoolYearReceived result ->
|
||||||
|
SchoolYear.handleActiveSchoolYearReceived result model
|
||||||
|
|
||||||
|
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 ->
|
||||||
|
SchoolYear.handleCreateSchoolYear model
|
||||||
|
|
||||||
|
SchoolYearCreated result ->
|
||||||
|
SchoolYear.handleSchoolYearCreated result model
|
||||||
|
|
||||||
|
ActivateSchoolYear id ->
|
||||||
|
SchoolYear.handleActivateSchoolYear id model
|
||||||
|
|
||||||
|
SchoolYearActivated result ->
|
||||||
|
SchoolYear.handleSchoolYearActivated result model
|
||||||
|
|
||||||
|
DeleteSchoolYear id ->
|
||||||
|
SchoolYear.handleDeleteSchoolYear id model
|
||||||
|
|
||||||
|
SchoolYearDeleted result ->
|
||||||
|
SchoolYear.handleSchoolYearDeleted result model
|
||||||
|
|
||||||
|
-- PDF Download
|
||||||
|
DownloadYearlySummaryPDF ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | isProcessing = True }, Api.TimeEntry.downloadYearlySummaryPDF token )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
YearlySummaryPDFReceived result ->
|
||||||
|
case result of
|
||||||
|
Ok pdfBytes ->
|
||||||
|
let
|
||||||
|
filename =
|
||||||
|
"Jahresuebersicht_" ++ String.fromInt model.currentYear ++ ".pdf"
|
||||||
|
in
|
||||||
|
( { model | isProcessing = False }, File.Download.bytes filename "application/pdf" pdfBytes )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( { model | isProcessing = False }, Cmd.none )
|
||||||
|
|
||||||
|
-- Delete Confirmation
|
||||||
|
ConfirmDeleteUser userId ->
|
||||||
|
( { model | pendingDeleteId = Just userId }, Utils.Ports.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, Api.TimeEntry.deleteTimeEntry token id )
|
||||||
|
|
||||||
|
else
|
||||||
|
( model, Api.User.deleteUser token id )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
else
|
||||||
|
( { model | pendingDeleteId = Nothing }, Cmd.none )
|
||||||
|
|
||||||
|
-- Toasts
|
||||||
|
ShowToast message toastType ->
|
||||||
|
let
|
||||||
|
newToast =
|
||||||
|
{ id = model.nextToastId
|
||||||
|
, message = message
|
||||||
|
, toastType = toastType
|
||||||
|
, dismissible = True
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissDelay =
|
||||||
|
case toastType of
|
||||||
|
ErrorToast ->
|
||||||
|
8000
|
||||||
|
|
||||||
|
SuccessToast ->
|
||||||
|
5000
|
||||||
|
|
||||||
|
InfoToast ->
|
||||||
|
5000
|
||||||
|
|
||||||
|
WarningToast ->
|
||||||
|
6000
|
||||||
|
in
|
||||||
|
( { model
|
||||||
|
| toasts = model.toasts ++ [ newToast ]
|
||||||
|
, nextToastId = model.nextToastId + 1
|
||||||
|
}
|
||||||
|
, Task.perform (\_ -> AutoDismissToast newToast.id)
|
||||||
|
(Process.sleep dismissDelay)
|
||||||
|
)
|
||||||
|
|
||||||
|
DismissToast toastId ->
|
||||||
|
( { model | toasts = List.filter (\t -> t.id /= toastId) model.toasts }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
AutoDismissToast toastId ->
|
||||||
|
( { model | toasts = List.filter (\t -> t.id /= toastId) model.toasts }
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
196
frontend/src/Update/UserUpdate.elm
Normal file
196
frontend/src/Update/UserUpdate.elm
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
module Update.UserUpdate exposing
|
||||||
|
( handleCreateUser
|
||||||
|
, handleDeleteUser
|
||||||
|
, handleEditUserWorkHours
|
||||||
|
, handleResetPasswordSaved
|
||||||
|
, handleResetUserPassword
|
||||||
|
, handleSaveResetPassword
|
||||||
|
, handleSaveUserWorkHours
|
||||||
|
, handleUserCreated
|
||||||
|
, handleUserDeleted
|
||||||
|
, handleUserWorkHoursSaved
|
||||||
|
, handleUsersReceived
|
||||||
|
)
|
||||||
|
|
||||||
|
import Api.User
|
||||||
|
import Http
|
||||||
|
import Task
|
||||||
|
import Types.Model exposing (Model, NewUser, ToastType(..), User)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
handleCreateUser : Model -> ( Model, Cmd Msg )
|
||||||
|
handleCreateUser model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.User.createUser token model.newUser )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleUserCreated : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleUserCreated result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
let
|
||||||
|
emptyUser =
|
||||||
|
NewUser "" "" False
|
||||||
|
in
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model | newUser = emptyUser }
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.User.fetchUsers token
|
||||||
|
, Task.perform (\_ -> ShowToast "Benutzer erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleDeleteUser : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleDeleteUser userId model =
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.User.deleteUser token userId )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleUserDeleted : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleUserDeleted result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| pendingDeleteId = Nothing
|
||||||
|
, error = Nothing
|
||||||
|
, editingUserId = Nothing
|
||||||
|
, resetPasswordUserId = Nothing
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.User.fetchUsers token
|
||||||
|
, Task.perform (\_ -> ShowToast "Benutzer erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( { model | pendingDeleteId = Nothing }, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleUsersReceived : Result Http.Error (List User) -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleUsersReceived result model =
|
||||||
|
case result of
|
||||||
|
Ok users ->
|
||||||
|
( { model | users = users }, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleEditUserWorkHours : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleEditUserWorkHours userId model =
|
||||||
|
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 )
|
||||||
|
|
||||||
|
|
||||||
|
handleSaveUserWorkHours : Model -> ( Model, Cmd Msg )
|
||||||
|
handleSaveUserWorkHours model =
|
||||||
|
case ( model.token, model.editingUserId, String.toFloat model.editingUserWorkHours ) of
|
||||||
|
( Just token, Just userId, Just hours ) ->
|
||||||
|
( model, Api.User.updateUserWorkHours token userId (String.fromFloat hours) )
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
( model, Task.perform (\_ -> ShowToast "Ungültige Eingabe für Arbeitszeit" WarningToast) (Task.succeed ()) )
|
||||||
|
|
||||||
|
|
||||||
|
handleUserWorkHoursSaved : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleUserWorkHoursSaved result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( { model
|
||||||
|
| editingUserWorkHours = ""
|
||||||
|
, editingUserId = Nothing
|
||||||
|
, error = Nothing
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ Api.User.fetchUsers token
|
||||||
|
, Task.perform (\_ -> ShowToast "Arbeitszeit erfolgreich gespeichert!" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleResetUserPassword : Int -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleResetUserPassword userId model =
|
||||||
|
( { model
|
||||||
|
| resetPasswordUserId = Just userId
|
||||||
|
, resetPasswordNew = ""
|
||||||
|
}
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
handleSaveResetPassword : Model -> ( Model, Cmd Msg )
|
||||||
|
handleSaveResetPassword model =
|
||||||
|
case model.resetPasswordUserId of
|
||||||
|
Just userId ->
|
||||||
|
case model.token of
|
||||||
|
Just token ->
|
||||||
|
( model, Api.User.resetUserPassword token userId model.resetPasswordNew )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
( model, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
handleResetPasswordSaved : Result Http.Error () -> Model -> ( Model, Cmd Msg )
|
||||||
|
handleResetPasswordSaved result model =
|
||||||
|
case result of
|
||||||
|
Ok _ ->
|
||||||
|
( { model
|
||||||
|
| resetPasswordUserId = Nothing
|
||||||
|
, resetPasswordNew = ""
|
||||||
|
, error = Nothing
|
||||||
|
}
|
||||||
|
, Cmd.batch
|
||||||
|
[ case model.token of
|
||||||
|
Just token ->
|
||||||
|
Api.User.fetchUsers token
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Cmd.none
|
||||||
|
, Task.perform (\_ -> ShowToast "Passwort erfolgreich zurückgesetzt" SuccessToast) (Task.succeed ())
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
Err err ->
|
||||||
|
( model, Cmd.none )
|
||||||
338
frontend/src/Utils/DateUtils.elm
Normal file
338
frontend/src/Utils/DateUtils.elm
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
module Utils.DateUtils exposing
|
||||||
|
( addDaysToDate
|
||||||
|
, getDateForWeekDay
|
||||||
|
, getDayOfWeek
|
||||||
|
, getDayOfYear
|
||||||
|
, getISOWeek
|
||||||
|
, getISOWeekFromPosix
|
||||||
|
, getWeekDateRange
|
||||||
|
, getYearWeekFromDate
|
||||||
|
, isLeapYear
|
||||||
|
, monthToInt
|
||||||
|
, nextWeek
|
||||||
|
, previousWeek
|
||||||
|
)
|
||||||
|
|
||||||
|
import Time
|
||||||
|
|
||||||
|
|
||||||
|
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 )
|
||||||
42
frontend/src/Utils/ErrorHandler.elm
Normal file
42
frontend/src/Utils/ErrorHandler.elm
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
module Utils.ErrorHandler exposing (handleApiError)
|
||||||
|
|
||||||
|
import Api.Decoders exposing (apiErrorDecoder)
|
||||||
|
import Http
|
||||||
|
import Json.Decode as Decode
|
||||||
|
import Task
|
||||||
|
import Types.Model exposing (ToastType(..))
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
handleApiError : Http.Error -> Cmd Msg
|
||||||
|
handleApiError error =
|
||||||
|
let
|
||||||
|
message =
|
||||||
|
case error of
|
||||||
|
Http.BadBody body ->
|
||||||
|
case Decode.decodeString apiErrorDecoder body of
|
||||||
|
Ok apiErr ->
|
||||||
|
apiErr.message
|
||||||
|
|
||||||
|
Err _ ->
|
||||||
|
"Ein Fehler ist aufgetreten"
|
||||||
|
|
||||||
|
Http.BadStatus 401 ->
|
||||||
|
"Keine Berechtigung - bitte erneut anmelden"
|
||||||
|
|
||||||
|
Http.BadStatus 403 ->
|
||||||
|
"Zugriff verweigert"
|
||||||
|
|
||||||
|
Http.BadStatus 404 ->
|
||||||
|
"Ressource nicht gefunden"
|
||||||
|
|
||||||
|
Http.Timeout ->
|
||||||
|
"Zeitüberschreitung - bitte erneut versuchen"
|
||||||
|
|
||||||
|
Http.NetworkError ->
|
||||||
|
"Netzwerkfehler - bitte Verbindung prüfen"
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
"Ein unerwarteter Fehler ist aufgetreten"
|
||||||
|
in
|
||||||
|
Task.perform (\_ -> ShowToast message ErrorToast) (Task.succeed ())
|
||||||
20
frontend/src/Utils/Ports.elm
Normal file
20
frontend/src/Utils/Ports.elm
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
port module Utils.Ports exposing
|
||||||
|
( confirmDelete
|
||||||
|
, confirmDeleteResponse
|
||||||
|
, removeToken
|
||||||
|
, saveToken
|
||||||
|
)
|
||||||
|
|
||||||
|
import Json.Encode as Encode
|
||||||
|
|
||||||
|
|
||||||
|
port saveToken : Encode.Value -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
|
port removeToken : () -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
|
port confirmDelete : String -> Cmd msg
|
||||||
|
|
||||||
|
|
||||||
|
port confirmDeleteResponse : (Bool -> msg) -> Sub msg
|
||||||
34
frontend/src/Utils/TimeUtils.elm
Normal file
34
frontend/src/Utils/TimeUtils.elm
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
module Utils.TimeUtils exposing (calculateHours)
|
||||||
|
|
||||||
|
|
||||||
|
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 if endTime == "manual" then
|
||||||
|
case String.toFloat startTime of
|
||||||
|
Just time ->
|
||||||
|
time
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
0
|
||||||
|
|
||||||
|
else
|
||||||
|
0
|
||||||
1165
frontend/src/View/AdminDashboard.elm
Normal file
1165
frontend/src/View/AdminDashboard.elm
Normal file
File diff suppressed because it is too large
Load diff
99
frontend/src/View/Components/Navigation.elm
Normal file
99
frontend/src/View/Components/Navigation.elm
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
module View.Components.Navigation exposing (viewDayMobile, viewWeekNavigation)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Types.Model exposing (Model, Schedule)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import View.Components.Schedule exposing (viewScheduleItemWithDay)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
[ span [ class "icon" ]
|
||||||
|
[ i [ class "fas fa-chevron-left" ] [] ]
|
||||||
|
, span [] [ text "Vorherige Woche" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "level-item" ]
|
||||||
|
[ div
|
||||||
|
[ style "display" "flex"
|
||||||
|
, style "flex-direction" "column"
|
||||||
|
, style "align-items" "center"
|
||||||
|
, style "gap" "0.5rem"
|
||||||
|
, style "min-width" "250px"
|
||||||
|
]
|
||||||
|
[ p
|
||||||
|
[ class "heading"
|
||||||
|
, style "margin" "0"
|
||||||
|
, style "line-height" "1.2"
|
||||||
|
]
|
||||||
|
[ text "Kalenderwoche" ]
|
||||||
|
, p
|
||||||
|
[ class "title is-3"
|
||||||
|
, style "margin" "0"
|
||||||
|
, style "line-height" "1.2"
|
||||||
|
]
|
||||||
|
[ text ("KW " ++ String.fromInt model.currentWeek ++ " / " ++ String.fromInt model.currentYear) ]
|
||||||
|
, p
|
||||||
|
[ class "subtitle is-6"
|
||||||
|
, style "margin" "0"
|
||||||
|
, style "line-height" "1.2"
|
||||||
|
]
|
||||||
|
[ text dateRange ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
, div [ class "level-right" ]
|
||||||
|
[ div [ class "level-item" ]
|
||||||
|
[ button
|
||||||
|
[ class "button is-primary"
|
||||||
|
, onClick NextWeek
|
||||||
|
]
|
||||||
|
[ span [] [ text "Nächste Woche" ]
|
||||||
|
, span [ class "icon" ]
|
||||||
|
[ i [ class "fas fa-chevron-right" ] [] ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
]
|
||||||
76
frontend/src/View/Components/Schedule.elm
Normal file
76
frontend/src/View/Components/Schedule.elm
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
module View.Components.Schedule exposing (viewScheduleItemWithDay)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Types.Model exposing (Model, Schedule)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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) ]
|
||||||
|
]
|
||||||
66
frontend/src/View/Components/Toast.elm
Normal file
66
frontend/src/View/Components/Toast.elm
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
module View.Components.Toast exposing (viewToasts)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Types.Model exposing (Model, Schedule, Toast, ToastType(..))
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Utils.TimeUtils exposing (calculateHours)
|
||||||
|
import View.Components.Navigation exposing (viewDayMobile, viewWeekNavigation)
|
||||||
|
import View.Components.Schedule exposing (viewScheduleItemWithDay)
|
||||||
|
|
||||||
|
|
||||||
|
viewToasts : List Toast -> Html Msg
|
||||||
|
viewToasts toasts =
|
||||||
|
div [ class "toast-container" ]
|
||||||
|
(List.map viewToast toasts)
|
||||||
|
|
||||||
|
|
||||||
|
viewToast : Toast -> Html Msg
|
||||||
|
viewToast toast =
|
||||||
|
let
|
||||||
|
toastClass =
|
||||||
|
case toast.toastType of
|
||||||
|
ErrorToast ->
|
||||||
|
"toast-error"
|
||||||
|
|
||||||
|
SuccessToast ->
|
||||||
|
"toast-success"
|
||||||
|
|
||||||
|
InfoToast ->
|
||||||
|
"toast-info"
|
||||||
|
|
||||||
|
WarningToast ->
|
||||||
|
"toast-warning"
|
||||||
|
|
||||||
|
icon =
|
||||||
|
case toast.toastType of
|
||||||
|
ErrorToast ->
|
||||||
|
"fas fa-exclamation-circle"
|
||||||
|
|
||||||
|
SuccessToast ->
|
||||||
|
"fas fa-check-circle"
|
||||||
|
|
||||||
|
InfoToast ->
|
||||||
|
"fas fa-info-circle"
|
||||||
|
|
||||||
|
WarningToast ->
|
||||||
|
"fas fa-exclamation-triangle"
|
||||||
|
in
|
||||||
|
div [ class ("toast " ++ toastClass), style "animation" "slideIn 0.3s ease-out" ]
|
||||||
|
[ div [ class "toast-content" ]
|
||||||
|
[ span [ class "toast-icon" ]
|
||||||
|
[ i [ class icon ] [] ]
|
||||||
|
, span [ class "toast-message" ] [ text toast.message ]
|
||||||
|
]
|
||||||
|
, if toast.dismissible then
|
||||||
|
button
|
||||||
|
[ class "toast-close"
|
||||||
|
, onClick (DismissToast toast.id)
|
||||||
|
, attribute "aria-label" "Schließen"
|
||||||
|
]
|
||||||
|
[ i [ class "fas fa-times" ] [] ]
|
||||||
|
|
||||||
|
else
|
||||||
|
text ""
|
||||||
|
]
|
||||||
57
frontend/src/View/Login.elm
Normal file
57
frontend/src/View/Login.elm
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
module View.Login exposing (viewLogin)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Types.Model exposing (Model)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
|
||||||
|
|
||||||
|
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" ]
|
||||||
|
, 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" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
338
frontend/src/View/UserDashboard.elm
Normal file
338
frontend/src/View/UserDashboard.elm
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
module View.UserDashboard exposing (viewUserDashboard)
|
||||||
|
|
||||||
|
import Html exposing (..)
|
||||||
|
import Html.Attributes exposing (..)
|
||||||
|
import Html.Events exposing (..)
|
||||||
|
import Types.Model exposing (Model, Schedule)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Utils.TimeUtils exposing (calculateHours)
|
||||||
|
import View.Components.Navigation exposing (viewDayMobile, viewWeekNavigation)
|
||||||
|
import View.Components.Schedule exposing (viewScheduleItemWithDay)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
viewUserYearlyTotal : Model -> Html Msg
|
||||||
|
viewUserYearlyTotal model =
|
||||||
|
let
|
||||||
|
yearlyTotal =
|
||||||
|
model.timeEntries
|
||||||
|
|> List.map
|
||||||
|
(\entry ->
|
||||||
|
if entry.entryType == "lesson" then
|
||||||
|
1.0
|
||||||
|
|
||||||
|
else
|
||||||
|
Utils.TimeUtils.calculateHours entry.startTime entry.endTime
|
||||||
|
)
|
||||||
|
|> List.sum
|
||||||
|
|
||||||
|
userTarget =
|
||||||
|
List.filter (\u -> not u.isAdmin) model.users
|
||||||
|
|> List.head
|
||||||
|
|> Maybe.map .yearlyWorkHours
|
||||||
|
|> Maybe.withDefault 60
|
||||||
|
|
||||||
|
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 ++ "%") ]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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..." ]
|
||||||
|
]
|
||||||
29
frontend/src/View/View.elm
Normal file
29
frontend/src/View/View.elm
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
module View.View exposing (view)
|
||||||
|
|
||||||
|
import Html exposing (Html, div)
|
||||||
|
import Html.Attributes exposing (class)
|
||||||
|
import Types.Model exposing (Model)
|
||||||
|
import Types.Msg exposing (Msg(..))
|
||||||
|
import Types.Page exposing (Page(..))
|
||||||
|
import View.AdminDashboard exposing (viewAdminDashboard)
|
||||||
|
import View.Components.Toast exposing (viewToasts)
|
||||||
|
import View.Login exposing (viewLogin)
|
||||||
|
import View.UserDashboard exposing (viewUserDashboard)
|
||||||
|
|
||||||
|
|
||||||
|
view : Model -> Html Msg
|
||||||
|
view model =
|
||||||
|
div [ class "app-container" ]
|
||||||
|
[ viewToasts model.toasts
|
||||||
|
, div [ class "container" ]
|
||||||
|
[ case model.page of
|
||||||
|
LoginPage ->
|
||||||
|
viewLogin model
|
||||||
|
|
||||||
|
UserDashboard ->
|
||||||
|
viewUserDashboard model
|
||||||
|
|
||||||
|
AdminDashboard ->
|
||||||
|
viewAdminDashboard model
|
||||||
|
]
|
||||||
|
]
|
||||||
Loading…
Add table
Add a link
Reference in a new issue