feat: change Manual time entry to work with hours instead of start and end time
according to add hours as well the logic was changed to accept ours for manual entries instead of start and end time. This allows to add negative numbers as well, which are added to working time.
This commit is contained in:
parent
c07019e3eb
commit
84def05c50
4 changed files with 84 additions and 135 deletions
|
|
@ -307,12 +307,12 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
|
|
||||||
year, week := t.ISOWeek()
|
year, week := t.ISOWeek()
|
||||||
|
|
||||||
var hours float64
|
entry := TimeEntry{
|
||||||
if entryType == "lesson" {
|
Type: entryType,
|
||||||
hours = 1.0
|
StartTime: startTime,
|
||||||
} else {
|
EndTime: endTime,
|
||||||
hours = calculateHoursDiff(startTime, endTime)
|
|
||||||
}
|
}
|
||||||
|
hours := calculateHours(entry)
|
||||||
|
|
||||||
key := fmt.Sprintf("%d_%d_%d", userID, year, week)
|
key := fmt.Sprintf("%d_%d_%d", userID, year, week)
|
||||||
if existing, exists := hoursMap[key]; exists {
|
if existing, exists := hoursMap[key]; exists {
|
||||||
|
|
@ -359,57 +359,6 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
|
|
||||||
// users, err := GetAllUsers(db)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// entries, err := GetAllTimeEntries(db)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// userTotals := make(map[int]float64)
|
|
||||||
// usernames := make(map[int]string)
|
|
||||||
|
|
||||||
// for _, entry := range entries {
|
|
||||||
// var hours float64
|
|
||||||
// if entry.Type == "lesson" {
|
|
||||||
// hours = 1.0
|
|
||||||
// } else {
|
|
||||||
// hours = calculateHoursDiff(entry.StartTime, entry.EndTime)
|
|
||||||
// }
|
|
||||||
// userTotals[entry.UserID] += hours
|
|
||||||
// usernames[entry.UserID] = entry.Username
|
|
||||||
// }
|
|
||||||
|
|
||||||
// var result []WeeklyHours
|
|
||||||
// for _, user := range users {
|
|
||||||
// if !user.IsAdmin {
|
|
||||||
// total := userTotals[user.ID]
|
|
||||||
// remaining := user.YearlyHours - total
|
|
||||||
|
|
||||||
// result = append(result, WeeklyHours{
|
|
||||||
// UserID: user.ID,
|
|
||||||
// Username: user.Username,
|
|
||||||
// Year: time.Now().Year(),
|
|
||||||
// Week: 0,
|
|
||||||
// TotalHours: total,
|
|
||||||
// YearlyTarget: user.YearlyHours,
|
|
||||||
// YearlyActual: total,
|
|
||||||
// RemainingYearly: remaining,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// sort.Slice(result, func(i, j int) bool {
|
|
||||||
// return result[i].Username < result[j].Username
|
|
||||||
// })
|
|
||||||
|
|
||||||
// return result, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func calculateHoursDiff(startTime, endTime string) float64 {
|
func calculateHoursDiff(startTime, endTime string) float64 {
|
||||||
parseTime := func(timeStr string) float64 {
|
parseTime := func(timeStr string) float64 {
|
||||||
parts := strings.Split(timeStr, ":")
|
parts := strings.Split(timeStr, ":")
|
||||||
|
|
@ -471,7 +420,6 @@ func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (boo
|
||||||
var count int
|
var count int
|
||||||
err := db.QueryRow(query, userID,
|
err := db.QueryRow(query, userID,
|
||||||
dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count)
|
dateList[0], dateList[1], dateList[2], dateList[3], dateList[4]).Scan(&count)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error checking entries: %v", err)
|
log.Printf("Error checking entries: %v", err)
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -489,7 +437,7 @@ func GetActiveSchoolYear(db *sql.DB) (*SchoolYear, error) {
|
||||||
`).Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt)
|
`).Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt)
|
||||||
|
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil // Kein aktives Schuljahr
|
return nil, nil
|
||||||
}
|
}
|
||||||
return &sy, err
|
return &sy, err
|
||||||
}
|
}
|
||||||
|
|
@ -560,7 +508,6 @@ func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
WHERE date >= ? AND date <= ?
|
WHERE date >= ? AND date <= ?
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
`, schoolYear.StartDate, schoolYear.EndDate)
|
`, schoolYear.StartDate, schoolYear.EndDate)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []WeeklyHours{}, err
|
return []WeeklyHours{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -576,12 +523,12 @@ func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var hours float64
|
entry := TimeEntry{
|
||||||
if entryType == "lesson" {
|
Type: entryType,
|
||||||
hours = 1.0
|
StartTime: startTime,
|
||||||
} else {
|
EndTime: endTime,
|
||||||
hours = calculateHoursDiff(startTime, endTime)
|
|
||||||
}
|
}
|
||||||
|
hours := calculateHours(entry)
|
||||||
userTotals[userID] += hours
|
userTotals[userID] += hours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -606,3 +553,30 @@ func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateManualTimeEntry(db *sql.DB, entry *TimeEntry, hours float64) error {
|
||||||
|
entry.StartTime = fmt.Sprintf("%.2f", hours)
|
||||||
|
entry.EndTime = "manual"
|
||||||
|
entry.Type = "manual"
|
||||||
|
|
||||||
|
_, err := db.Exec(`
|
||||||
|
INSERT INTO time_entries (user_id, schedule_id, date, type, start_time, end_time)
|
||||||
|
VALUES (?, 0, ?, ?, ?, ?)
|
||||||
|
`, entry.UserID, entry.Date, entry.Type, entry.StartTime, entry.EndTime)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateHours(entry TimeEntry) float64 {
|
||||||
|
if entry.Type == "lesson" {
|
||||||
|
return 1.0
|
||||||
|
} else if entry.Type == "manual" {
|
||||||
|
hours, err := strconv.ParseFloat(entry.StartTime, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return hours
|
||||||
|
} else {
|
||||||
|
return calculateHoursDiff(entry.StartTime, entry.EndTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,17 +90,15 @@ func (app *App) GetYearlyHoursSummaryHandler(c echo.Context) error {
|
||||||
|
|
||||||
func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
||||||
isAdmin, _ := c.Get("is_admin").(bool)
|
isAdmin, _ := c.Get("is_admin").(bool)
|
||||||
|
|
||||||
if !isAdmin {
|
if !isAdmin {
|
||||||
return echo.NewHTTPError(http.StatusForbidden, "Only admins can create entries for others")
|
return echo.NewHTTPError(http.StatusForbidden, "Only admins can create entries for others")
|
||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
UserID int `json:"user_id"`
|
UserID int `json:"user_id"`
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
StartTime string `json:"start_time"`
|
Hours float64 `json:"hours"`
|
||||||
EndTime string `json:"end_time"`
|
Type string `json:"type"`
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.Bind(&req); err != nil {
|
if err := c.Bind(&req); err != nil {
|
||||||
|
|
@ -110,16 +108,16 @@ func (app *App) AdminCreateTimeEntryHandler(c echo.Context) error {
|
||||||
entry := TimeEntry{
|
entry := TimeEntry{
|
||||||
UserID: req.UserID,
|
UserID: req.UserID,
|
||||||
Date: req.Date,
|
Date: req.Date,
|
||||||
StartTime: req.StartTime,
|
StartTime: "00:00",
|
||||||
EndTime: req.EndTime,
|
EndTime: "00:00",
|
||||||
Type: req.Type,
|
Type: "manual",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := CreateTimeEntry(app.DB, &entry); err != nil {
|
if err := CreateManualTimeEntry(app.DB, &entry, req.Hours); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusCreated, map[string]string{"message": "time entry created"})
|
return c.NoContent(http.StatusCreated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) GetUsersHandler(c echo.Context) error {
|
func (app *App) GetUsersHandler(c echo.Context) error {
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,6 @@ func (l *LoginRateLimiter) Middleware() echo.MiddlewareFunc {
|
||||||
func HTTPSRedirectMiddleware() echo.MiddlewareFunc {
|
func HTTPSRedirectMiddleware() echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c echo.Context) error {
|
return func(c echo.Context) error {
|
||||||
// Nur in Production aktivieren
|
|
||||||
if os.Getenv("ENVIRONMENT") == "production" {
|
if os.Getenv("ENVIRONMENT") == "production" {
|
||||||
if c.Request().Header.Get("X-Forwarded-Proto") != "https" {
|
if c.Request().Header.Get("X-Forwarded-Proto") != "https" {
|
||||||
return c.Redirect(http.StatusMovedPermanently,
|
return c.Redirect(http.StatusMovedPermanently,
|
||||||
|
|
|
||||||
|
|
@ -220,8 +220,7 @@ type alias YearlyHoursSummary =
|
||||||
type alias AdminManualEntry =
|
type alias AdminManualEntry =
|
||||||
{ selectedUserId : Maybe Int
|
{ selectedUserId : Maybe Int
|
||||||
, date : String
|
, date : String
|
||||||
, startTime : String
|
, hours : String
|
||||||
, endTime : String
|
|
||||||
, entryType : String
|
, entryType : String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,7 +292,7 @@ init flags =
|
||||||
, userPasswordInput = ""
|
, userPasswordInput = ""
|
||||||
, isProcessing = False
|
, isProcessing = False
|
||||||
, mobileMenuOpen = False
|
, mobileMenuOpen = False
|
||||||
, adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson"
|
, adminManualEntryForm = AdminManualEntry Nothing "" "" "manual"
|
||||||
, schoolYears = []
|
, schoolYears = []
|
||||||
, newSchoolYear = NewSchoolYear "" "" ""
|
, newSchoolYear = NewSchoolYear "" "" ""
|
||||||
, activeSchoolYear = Nothing
|
, activeSchoolYear = Nothing
|
||||||
|
|
@ -412,8 +411,7 @@ type Msg
|
||||||
| CloseMobileMenu
|
| CloseMobileMenu
|
||||||
| SelectUserForManualEntry Int
|
| SelectUserForManualEntry Int
|
||||||
| UpdateManualEntryDate String
|
| UpdateManualEntryDate String
|
||||||
| UpdateManualEntryStartTime String
|
| UpdateManualEntryHours String
|
||||||
| UpdateManualEntryEndTime String
|
|
||||||
| UpdateManualEntryType String
|
| UpdateManualEntryType String
|
||||||
| SaveAdminTimeEntry
|
| SaveAdminTimeEntry
|
||||||
| AdminTimeEntrySaved (Result Http.Error ())
|
| AdminTimeEntrySaved (Result Http.Error ())
|
||||||
|
|
@ -1437,19 +1435,12 @@ update msg model =
|
||||||
in
|
in
|
||||||
( { model | adminManualEntryForm = { form | date = date } }, Cmd.none )
|
( { model | adminManualEntryForm = { form | date = date } }, Cmd.none )
|
||||||
|
|
||||||
UpdateManualEntryStartTime time ->
|
UpdateManualEntryHours hours ->
|
||||||
let
|
let
|
||||||
form =
|
form =
|
||||||
model.adminManualEntryForm
|
model.adminManualEntryForm
|
||||||
in
|
in
|
||||||
( { model | adminManualEntryForm = { form | startTime = time } }, Cmd.none )
|
( { model | adminManualEntryForm = { form | hours = hours } }, Cmd.none )
|
||||||
|
|
||||||
UpdateManualEntryEndTime time ->
|
|
||||||
let
|
|
||||||
form =
|
|
||||||
model.adminManualEntryForm
|
|
||||||
in
|
|
||||||
( { model | adminManualEntryForm = { form | endTime = time } }, Cmd.none )
|
|
||||||
|
|
||||||
UpdateManualEntryType entryType ->
|
UpdateManualEntryType entryType ->
|
||||||
let
|
let
|
||||||
|
|
@ -1470,7 +1461,7 @@ update msg model =
|
||||||
case model.token of
|
case model.token of
|
||||||
Just token ->
|
Just token ->
|
||||||
( { model
|
( { model
|
||||||
| adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson"
|
| adminManualEntryForm = AdminManualEntry Nothing "" "" "manual"
|
||||||
, error = Nothing
|
, error = Nothing
|
||||||
, isProcessing = False
|
, isProcessing = False
|
||||||
}
|
}
|
||||||
|
|
@ -1997,6 +1988,14 @@ calculateHours startTime endTime =
|
||||||
if end > start then
|
if end > start then
|
||||||
end - start
|
end - start
|
||||||
|
|
||||||
|
else if endTime == "manual" then
|
||||||
|
case String.toFloat startTime of
|
||||||
|
Just time ->
|
||||||
|
time
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
0
|
||||||
|
|
||||||
else
|
else
|
||||||
0
|
0
|
||||||
|
|
||||||
|
|
@ -2661,23 +2660,28 @@ viewYearlyHourRow summary =
|
||||||
viewAdminManualEntryForm : Model -> Html Msg
|
viewAdminManualEntryForm : Model -> Html Msg
|
||||||
viewAdminManualEntryForm model =
|
viewAdminManualEntryForm model =
|
||||||
div [ class "box has-background-info-light" ]
|
div [ class "box has-background-info-light" ]
|
||||||
[ h3 [ class "subtitle" ] [ text "Neuer Zeiteintrag" ]
|
[ h3 [ class "subtitle" ] [ text "Manuelle Stundeneintragung" ]
|
||||||
|
, p [ class "help mb-3" ]
|
||||||
|
[ text "Positive Werte = Abzug, Negative Werte = Hinzurechnung" ]
|
||||||
, div [ class "columns" ]
|
, div [ class "columns" ]
|
||||||
[ div [ class "column" ]
|
[ div [ class "column is-4" ]
|
||||||
[ div [ class "field" ]
|
[ div [ class "field" ]
|
||||||
[ label [ class "label" ] [ text "Mitarbeiter" ]
|
[ label [ class "label" ] [ text "Mitarbeiter" ]
|
||||||
, div [ class "control" ]
|
, div [ class "control" ]
|
||||||
[ div [ class "select is-fullwidth" ]
|
[ div [ class "select is-fullwidth" ]
|
||||||
[ select [ onInput (SelectUserForManualEntry << Maybe.withDefault 0 << String.toInt) ]
|
[ select [ onInput (SelectUserForManualEntry << Maybe.withDefault 0 << String.toInt) ]
|
||||||
(option [] [ text "-- Wählen --" ]
|
(option [ value "" ] [ text "-- Wählen --" ]
|
||||||
:: List.map (\user -> option [ value (String.fromInt user.id) ] [ text user.username ])
|
:: List.map
|
||||||
(List.filter (\u -> not u.isAdmin) model.users)
|
(\u ->
|
||||||
|
option [ value (String.fromInt u.id), selected (model.adminManualEntryForm.selectedUserId == Just u.id) ] [ text u.username ]
|
||||||
|
)
|
||||||
|
model.users
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
, div [ class "column" ]
|
, div [ class "column is-4" ]
|
||||||
[ div [ class "field" ]
|
[ div [ class "field" ]
|
||||||
[ label [ class "label" ] [ text "Datum" ]
|
[ label [ class "label" ] [ text "Datum" ]
|
||||||
, div [ class "control" ]
|
, div [ class "control" ]
|
||||||
|
|
@ -2691,47 +2695,22 @@ viewAdminManualEntryForm model =
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
, div [ class "column is-4" ]
|
||||||
, div [ class "columns" ]
|
|
||||||
[ div [ class "column" ]
|
|
||||||
[ div [ class "field" ]
|
[ div [ class "field" ]
|
||||||
[ label [ class "label" ] [ text "Startzeit" ]
|
[ label [ class "label" ] [ text "Stunden (z.B. 2.5 oder -1.0)" ]
|
||||||
, div [ class "control" ]
|
, div [ class "control" ]
|
||||||
[ input
|
[ input
|
||||||
[ class "input"
|
[ class "input"
|
||||||
, type_ "time"
|
, type_ "number"
|
||||||
, value model.adminManualEntryForm.startTime
|
, step "0.5"
|
||||||
, onInput UpdateManualEntryStartTime
|
, placeholder "z.B. 2.5 oder -1.0"
|
||||||
|
, value model.adminManualEntryForm.hours
|
||||||
|
, onInput UpdateManualEntryHours
|
||||||
]
|
]
|
||||||
[]
|
[]
|
||||||
]
|
]
|
||||||
]
|
, p [ class "help" ]
|
||||||
]
|
[ text "Positiv: Wird abgezogen | Negativ: Wird hinzugerechnet" ]
|
||||||
, div [ class "column" ]
|
|
||||||
[ div [ class "field" ]
|
|
||||||
[ label [ class "label" ] [ text "Endzeit" ]
|
|
||||||
, div [ class "control" ]
|
|
||||||
[ input
|
|
||||||
[ class "input"
|
|
||||||
, type_ "time"
|
|
||||||
, value model.adminManualEntryForm.endTime
|
|
||||||
, onInput UpdateManualEntryEndTime
|
|
||||||
]
|
|
||||||
[]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
, div [ class "column" ]
|
|
||||||
[ div [ class "field" ]
|
|
||||||
[ label [ class "label" ] [ text "Typ" ]
|
|
||||||
, div [ class "control" ]
|
|
||||||
[ div [ class "select is-fullwidth" ]
|
|
||||||
[ select [ onInput UpdateManualEntryType, value model.adminManualEntryForm.entryType ]
|
|
||||||
[ option [ value "lesson" ] [ text "Unterricht" ]
|
|
||||||
, option [ value "break" ] [ text "Pause" ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
@ -2743,7 +2722,7 @@ viewAdminManualEntryForm model =
|
||||||
, disabled
|
, disabled
|
||||||
(case model.adminManualEntryForm.selectedUserId of
|
(case model.adminManualEntryForm.selectedUserId of
|
||||||
Just _ ->
|
Just _ ->
|
||||||
model.isProcessing
|
model.isProcessing || String.isEmpty model.adminManualEntryForm.hours
|
||||||
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
True
|
True
|
||||||
|
|
@ -4012,9 +3991,8 @@ createAdminTimeEntry token entry =
|
||||||
Encode.object
|
Encode.object
|
||||||
[ ( "user_id", Encode.int userId )
|
[ ( "user_id", Encode.int userId )
|
||||||
, ( "date", Encode.string entry.date )
|
, ( "date", Encode.string entry.date )
|
||||||
, ( "start_time", Encode.string entry.startTime )
|
, ( "hours", Encode.float (String.toFloat entry.hours |> Maybe.withDefault 0) )
|
||||||
, ( "end_time", Encode.string entry.endTime )
|
, ( "type", Encode.string "manual" )
|
||||||
, ( "type", Encode.string entry.entryType )
|
|
||||||
]
|
]
|
||||||
, expect = Http.expectWhatever AdminTimeEntrySaved
|
, expect = Http.expectWhatever AdminTimeEntrySaved
|
||||||
, timeout = Nothing
|
, timeout = Nothing
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue