feat: add schoolyear based calculation
This commit is contained in:
parent
e65ba85c43
commit
c07019e3eb
5 changed files with 675 additions and 44 deletions
|
|
@ -66,6 +66,14 @@ func createTables(db *sql.DB) {
|
|||
details TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS school_years (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
}
|
||||
|
||||
for _, query := range queries {
|
||||
|
|
@ -92,6 +100,8 @@ func createIndexes(db *sql.DB) {
|
|||
`CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_schedules_day ON schedules(day_of_week)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_school_years_active ON school_years(is_active)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_school_years_dates ON school_years(start_date, end_date)`,
|
||||
}
|
||||
|
||||
for _, idx := range indexes {
|
||||
|
|
@ -349,56 +359,56 @@ func GetWeeklyHours(db *sql.DB) ([]WeeklyHours, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
|
||||
users, err := GetAllUsers(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 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
|
||||
}
|
||||
// entries, err := GetAllTimeEntries(db)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
userTotals := make(map[int]float64)
|
||||
usernames := make(map[int]string)
|
||||
// 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
|
||||
}
|
||||
// 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
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 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
|
||||
})
|
||||
// sort.Slice(result, func(i, j int) bool {
|
||||
// return result[i].Username < result[j].Username
|
||||
// })
|
||||
|
||||
return result, nil
|
||||
}
|
||||
// return result, nil
|
||||
// }
|
||||
|
||||
func calculateHoursDiff(startTime, endTime string) float64 {
|
||||
parseTime := func(timeStr string) float64 {
|
||||
|
|
@ -469,3 +479,130 @@ func CheckUserHasEntriesForWeek(db *sql.DB, userID int, year int, week int) (boo
|
|||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func GetActiveSchoolYear(db *sql.DB) (*SchoolYear, error) {
|
||||
var sy SchoolYear
|
||||
err := db.QueryRow(`
|
||||
SELECT id, name, start_date, end_date, is_active, created_at
|
||||
FROM school_years
|
||||
WHERE is_active = 1
|
||||
`).Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil // Kein aktives Schuljahr
|
||||
}
|
||||
return &sy, err
|
||||
}
|
||||
|
||||
func GetAllSchoolYears(db *sql.DB) ([]SchoolYear, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, name, start_date, end_date, is_active, created_at
|
||||
FROM school_years
|
||||
ORDER BY start_date DESC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
years := []SchoolYear{}
|
||||
for rows.Next() {
|
||||
var sy SchoolYear
|
||||
if err := rows.Scan(&sy.ID, &sy.Name, &sy.StartDate, &sy.EndDate, &sy.IsActive, &sy.CreatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
years = append(years, sy)
|
||||
}
|
||||
return years, rows.Err()
|
||||
}
|
||||
|
||||
func CreateSchoolYear(db *sql.DB, name, startDate, endDate string) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO school_years (name, start_date, end_date, is_active)
|
||||
VALUES (?, ?, ?, 0)
|
||||
`, name, startDate, endDate)
|
||||
return err
|
||||
}
|
||||
|
||||
func SetActiveSchoolYear(db *sql.DB, id int) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec("UPDATE school_years SET is_active = 0"); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec("UPDATE school_years SET is_active = 1 WHERE id = ?", id); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func GetYearlyHoursSummary(db *sql.DB) ([]WeeklyHours, error) {
|
||||
schoolYear, err := GetActiveSchoolYear(db)
|
||||
if err != nil || schoolYear == nil {
|
||||
return []WeeklyHours{}, err
|
||||
}
|
||||
|
||||
users, err := GetAllUsers(db)
|
||||
if err != nil {
|
||||
return []WeeklyHours{}, err
|
||||
}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT user_id, date, start_time, end_time, type
|
||||
FROM time_entries
|
||||
WHERE date >= ? AND date <= ?
|
||||
ORDER BY date DESC
|
||||
`, schoolYear.StartDate, schoolYear.EndDate)
|
||||
|
||||
if err != nil {
|
||||
return []WeeklyHours{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
userTotals := make(map[int]float64)
|
||||
|
||||
for rows.Next() {
|
||||
var userID int
|
||||
var date, startTime, endTime, entryType string
|
||||
|
||||
if err := rows.Scan(&userID, &date, &startTime, &endTime, &entryType); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var hours float64
|
||||
if entryType == "lesson" {
|
||||
hours = 1.0
|
||||
} else {
|
||||
hours = calculateHoursDiff(startTime, endTime)
|
||||
}
|
||||
userTotals[userID] += hours
|
||||
}
|
||||
|
||||
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: 0,
|
||||
Week: 0,
|
||||
TotalHours: total,
|
||||
YearlyTarget: user.YearlyHours,
|
||||
YearlyActual: total,
|
||||
RemainingYearly: remaining,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -448,3 +448,51 @@ func (app *App) CreateUserHandler(c echo.Context) error {
|
|||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (app *App) GetSchoolYearsHandler(c echo.Context) error {
|
||||
years, err := GetAllSchoolYears(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if years == nil {
|
||||
years = []SchoolYear{}
|
||||
}
|
||||
return c.JSON(http.StatusOK, years)
|
||||
}
|
||||
|
||||
func (app *App) CreateSchoolYearHandler(c echo.Context) error {
|
||||
var req CreateSchoolYearRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := CreateSchoolYear(app.DB, req.Name, req.StartDate, req.EndDate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusCreated)
|
||||
}
|
||||
|
||||
func (app *App) SetActiveSchoolYearHandler(c echo.Context) error {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID")
|
||||
}
|
||||
|
||||
if err := SetActiveSchoolYear(app.DB, id); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (app *App) GetActiveSchoolYearHandler(c echo.Context) error {
|
||||
year, err := GetActiveSchoolYear(app.DB)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
if year == nil {
|
||||
return c.JSON(http.StatusOK, map[string]any{"active": false})
|
||||
}
|
||||
return c.JSON(http.StatusOK, year)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ func main() {
|
|||
protected.GET("/week-has-entries", app.CheckWeekHasEntries)
|
||||
protected.GET("/yearly-hours-summary", app.GetYearlyHoursSummaryHandler)
|
||||
protected.GET("/my-info", app.GetMyInfoHandler)
|
||||
protected.GET("/school-year/active", app.GetActiveSchoolYearHandler)
|
||||
}
|
||||
|
||||
admin := e.Group("/api/admin")
|
||||
|
|
@ -64,6 +65,9 @@ func main() {
|
|||
admin.PUT("/time-entries/:id", app.UpdateTimeEntryHandler)
|
||||
admin.DELETE("/time-entries/:id", app.DeleteTimeEntryHandler)
|
||||
admin.POST("/time-entry", app.AdminCreateTimeEntryHandler)
|
||||
admin.GET("/school-years", app.GetSchoolYearsHandler)
|
||||
admin.POST("/school-years", app.CreateSchoolYearHandler)
|
||||
admin.PUT("/school-years/:id/activate", app.SetActiveSchoolYearHandler)
|
||||
}
|
||||
|
||||
e.Static("/", "./static")
|
||||
|
|
|
|||
|
|
@ -61,6 +61,21 @@ type CreateUserRequest struct {
|
|||
YearlyHours float64 `json:"yearly_hours"`
|
||||
}
|
||||
|
||||
type SchoolYear struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CreateSchoolYearRequest struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
StartDate string `json:"start_date" validate:"required"`
|
||||
EndDate string `json:"end_date" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Username string `json:"username"`
|
||||
YearlyHours float64 `json:"yearly_hours"`
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ type alias Model =
|
|||
, isProcessing : Bool
|
||||
, mobileMenuOpen : Bool
|
||||
, adminManualEntryForm : AdminManualEntry
|
||||
, schoolYears : List SchoolYear
|
||||
, newSchoolYear : NewSchoolYear
|
||||
, activeSchoolYear : Maybe SchoolYear
|
||||
, editingSchoolYearId : Maybe Int
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -106,6 +110,7 @@ type AdminTab
|
|||
= ScheduleTab
|
||||
| UsersTab
|
||||
| TimeEntriesTab
|
||||
| SchoolYearsTab
|
||||
|
||||
|
||||
type alias Schedule =
|
||||
|
|
@ -221,6 +226,22 @@ type alias AdminManualEntry =
|
|||
}
|
||||
|
||||
|
||||
type alias SchoolYear =
|
||||
{ id : Int
|
||||
, name : String
|
||||
, startDate : String
|
||||
, endDate : String
|
||||
, isActive : Bool
|
||||
}
|
||||
|
||||
|
||||
type alias NewSchoolYear =
|
||||
{ name : String
|
||||
, startDate : String
|
||||
, endDate : String
|
||||
}
|
||||
|
||||
|
||||
init : Flags -> ( Model, Cmd Msg )
|
||||
init flags =
|
||||
let
|
||||
|
|
@ -273,6 +294,10 @@ init flags =
|
|||
, isProcessing = False
|
||||
, mobileMenuOpen = False
|
||||
, adminManualEntryForm = AdminManualEntry Nothing "" "" "" "lesson"
|
||||
, schoolYears = []
|
||||
, newSchoolYear = NewSchoolYear "" "" ""
|
||||
, activeSchoolYear = Nothing
|
||||
, editingSchoolYearId = Nothing
|
||||
}
|
||||
|
||||
cmd =
|
||||
|
|
@ -283,7 +308,7 @@ init flags =
|
|||
, fetchSchedules (Just token)
|
||||
, fetchYearlyHoursSummary token
|
||||
, if flags.isAdmin then
|
||||
Cmd.none
|
||||
fetchSchoolYears token
|
||||
|
||||
else
|
||||
fetchMyInfo token
|
||||
|
|
@ -394,6 +419,19 @@ type Msg
|
|||
| AdminTimeEntrySaved (Result Http.Error ())
|
||||
| FetchMyInfo
|
||||
| MyInfoReceived (Result Http.Error User)
|
||||
| FetchSchoolYears
|
||||
| SchoolYearsReceived (Result Http.Error (List SchoolYear))
|
||||
| FetchActiveSchoolYear
|
||||
| ActiveSchoolYearReceived (Result Http.Error SchoolYear)
|
||||
| UpdateNewSchoolYearName String
|
||||
| UpdateNewSchoolYearStart String
|
||||
| UpdateNewSchoolYearEnd String
|
||||
| CreateSchoolYear
|
||||
| SchoolYearCreated (Result Http.Error ())
|
||||
| ActivateSchoolYear Int
|
||||
| SchoolYearActivated (Result Http.Error ())
|
||||
| DeleteSchoolYear Int
|
||||
| SchoolYearDeleted (Result Http.Error ())
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
|
|
@ -732,6 +770,17 @@ update msg model =
|
|||
Nothing ->
|
||||
Cmd.none
|
||||
|
||||
SchoolYearsTab ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
Cmd.batch
|
||||
[ fetchSchoolYears token
|
||||
, fetchActiveSchoolYear token
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
Cmd.none
|
||||
|
||||
_ ->
|
||||
Cmd.none
|
||||
in
|
||||
|
|
@ -1452,6 +1501,145 @@ update msg model =
|
|||
MyInfoReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden deiner Daten" }, Cmd.none )
|
||||
|
||||
FetchSchoolYears ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( model, fetchSchoolYears token )
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearsReceived (Ok years) ->
|
||||
( { model | schoolYears = years }, Cmd.none )
|
||||
|
||||
SchoolYearsReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Schuljahre" }, Cmd.none )
|
||||
|
||||
FetchActiveSchoolYear ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( model, fetchActiveSchoolYear token )
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
ActiveSchoolYearReceived (Ok year) ->
|
||||
( { model | activeSchoolYear = Just year }, Cmd.none )
|
||||
|
||||
ActiveSchoolYearReceived (Err _) ->
|
||||
( { model | activeSchoolYear = Nothing }, Cmd.none )
|
||||
|
||||
UpdateNewSchoolYearName name ->
|
||||
let
|
||||
old =
|
||||
model.newSchoolYear
|
||||
|
||||
new =
|
||||
{ old | name = name }
|
||||
in
|
||||
( { model | newSchoolYear = new }, Cmd.none )
|
||||
|
||||
UpdateNewSchoolYearStart date ->
|
||||
let
|
||||
old =
|
||||
model.newSchoolYear
|
||||
|
||||
new =
|
||||
{ old | startDate = date }
|
||||
in
|
||||
( { model | newSchoolYear = new }, Cmd.none )
|
||||
|
||||
UpdateNewSchoolYearEnd date ->
|
||||
let
|
||||
old =
|
||||
model.newSchoolYear
|
||||
|
||||
new =
|
||||
{ old | endDate = date }
|
||||
in
|
||||
( { model | newSchoolYear = new }, Cmd.none )
|
||||
|
||||
CreateSchoolYear ->
|
||||
if
|
||||
String.isEmpty model.newSchoolYear.name
|
||||
|| String.isEmpty model.newSchoolYear.startDate
|
||||
|| String.isEmpty model.newSchoolYear.endDate
|
||||
then
|
||||
( { model | error = Just "Bitte alle Felder ausfüllen" }, Cmd.none )
|
||||
|
||||
else
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model | isProcessing = True }, createSchoolYear token model.newSchoolYear )
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearCreated (Ok _) ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model
|
||||
| newSchoolYear = NewSchoolYear "" "" ""
|
||||
, error = Nothing
|
||||
, isProcessing = False
|
||||
}
|
||||
, fetchSchoolYears token
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearCreated (Err _) ->
|
||||
( { model
|
||||
| error = Just "Fehler beim Erstellen des Schuljahres"
|
||||
, isProcessing = False
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
ActivateSchoolYear id ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( model, activateSchoolYear token id )
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearActivated (Ok _) ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model | error = Nothing }
|
||||
, Cmd.batch
|
||||
[ fetchSchoolYears token
|
||||
, fetchActiveSchoolYear token
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearActivated (Err _) ->
|
||||
( { model | error = Just "Fehler beim Aktivieren" }, Cmd.none )
|
||||
|
||||
DeleteSchoolYear id ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( model, deleteSchoolYear token id )
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearDeleted (Ok _) ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model | error = Nothing }, fetchSchoolYears token )
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearDeleted (Err _) ->
|
||||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||||
|
||||
|
||||
|
||||
-- SUBSCRIPTIONS
|
||||
|
|
@ -2115,6 +2303,8 @@ viewAdminDashboard model =
|
|||
[ a [ onClick (SwitchTab UsersTab) ] [ text "Benutzer" ] ]
|
||||
, li [ classList [ ( "is-active", model.activeTab == TimeEntriesTab ) ] ]
|
||||
[ a [ onClick (SwitchTab TimeEntriesTab) ] [ text "Zeiteinträge" ] ]
|
||||
, li [ classList [ ( "is-active", model.activeTab == SchoolYearsTab ) ] ]
|
||||
[ a [ onClick (SwitchTab SchoolYearsTab) ] [ text "Schuljahre" ] ]
|
||||
]
|
||||
]
|
||||
, case model.activeTab of
|
||||
|
|
@ -2126,6 +2316,9 @@ viewAdminDashboard model =
|
|||
|
||||
TimeEntriesTab ->
|
||||
viewTimeEntriesTab model
|
||||
|
||||
SchoolYearsTab ->
|
||||
viewSchoolYearsTab model
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -3344,6 +3537,159 @@ viewTimeEntryRowWithActions model entry =
|
|||
]
|
||||
|
||||
|
||||
viewSchoolYearsTab : Model -> Html Msg
|
||||
viewSchoolYearsTab model =
|
||||
div []
|
||||
[ h2 [ class "title" ] [ text "Schuljahre verwalten" ]
|
||||
, case model.activeSchoolYear of
|
||||
Just schoolYear ->
|
||||
div [ class "notification is-info is-light mb-4" ]
|
||||
[ p [ class "has-text-weight-bold" ]
|
||||
[ text ("Aktives Schuljahr: " ++ schoolYear.name) ]
|
||||
, p [ class "is-size-7" ]
|
||||
[ text (schoolYear.startDate ++ " bis " ++ schoolYear.endDate) ]
|
||||
]
|
||||
|
||||
Nothing ->
|
||||
div [ class "notification is-warning is-light mb-4" ]
|
||||
[ text "⚠️ Kein Schuljahr aktiv! Bitte eines aktivieren." ]
|
||||
, viewSchoolYearForm model
|
||||
, viewSchoolYearsList model
|
||||
]
|
||||
|
||||
|
||||
viewSchoolYearForm : Model -> Html Msg
|
||||
viewSchoolYearForm model =
|
||||
div [ class "box" ]
|
||||
[ h3 [ class "subtitle" ] [ text "Neues Schuljahr erstellen" ]
|
||||
, div [ class "columns" ]
|
||||
[ div [ class "column is-4" ]
|
||||
[ div [ class "field" ]
|
||||
[ label [ class "label" ] [ text "Name (z.B. 2024/2025)" ]
|
||||
, div [ class "control" ]
|
||||
[ input
|
||||
[ class "input"
|
||||
, type_ "text"
|
||||
, placeholder "2024/2025"
|
||||
, value model.newSchoolYear.name
|
||||
, onInput UpdateNewSchoolYearName
|
||||
, disabled model.isProcessing
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "column is-4" ]
|
||||
[ div [ class "field" ]
|
||||
[ label [ class "label" ] [ text "Startdatum" ]
|
||||
, div [ class "control" ]
|
||||
[ input
|
||||
[ class "input"
|
||||
, type_ "date"
|
||||
, value model.newSchoolYear.startDate
|
||||
, onInput UpdateNewSchoolYearStart
|
||||
, disabled model.isProcessing
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "column is-4" ]
|
||||
[ div [ class "field" ]
|
||||
[ label [ class "label" ] [ text "Enddatum" ]
|
||||
, div [ class "control" ]
|
||||
[ input
|
||||
[ class "input"
|
||||
, type_ "date"
|
||||
, value model.newSchoolYear.endDate
|
||||
, onInput UpdateNewSchoolYearEnd
|
||||
, disabled model.isProcessing
|
||||
]
|
||||
[]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
, div [ class "field" ]
|
||||
[ div [ class "control" ]
|
||||
[ button
|
||||
[ class "button is-primary"
|
||||
, onClick CreateSchoolYear
|
||||
, disabled
|
||||
(String.isEmpty model.newSchoolYear.name
|
||||
|| String.isEmpty model.newSchoolYear.startDate
|
||||
|| String.isEmpty model.newSchoolYear.endDate
|
||||
|| model.isProcessing
|
||||
)
|
||||
]
|
||||
[ if model.isProcessing then
|
||||
span [ class "icon" ] [ i [ class "fas fa-spinner fa-pulse" ] [] ]
|
||||
|
||||
else
|
||||
text ""
|
||||
, text " Schuljahr erstellen"
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewSchoolYearsList : Model -> Html Msg
|
||||
viewSchoolYearsList model =
|
||||
div [ class "box mt-4" ]
|
||||
[ h3 [ class "subtitle" ] [ text "Vorhandene Schuljahre" ]
|
||||
, if List.isEmpty model.schoolYears then
|
||||
p [ class "has-text-centered has-text-grey" ] [ text "Keine Schuljahre vorhanden" ]
|
||||
|
||||
else
|
||||
table [ class "table is-fullwidth is-striped is-hoverable" ]
|
||||
[ thead []
|
||||
[ tr []
|
||||
[ th [] [ text "Name" ]
|
||||
, th [] [ text "Startdatum" ]
|
||||
, th [] [ text "Enddatum" ]
|
||||
, th [ class "has-text-centered" ] [ text "Status" ]
|
||||
, th [ class "has-text-centered" ] [ text "Aktionen" ]
|
||||
]
|
||||
]
|
||||
, tbody []
|
||||
(List.map viewSchoolYearRow model.schoolYears)
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
viewSchoolYearRow : SchoolYear -> Html Msg
|
||||
viewSchoolYearRow schoolYear =
|
||||
tr []
|
||||
[ td [] [ text schoolYear.name ]
|
||||
, td [] [ text schoolYear.startDate ]
|
||||
, td [] [ text schoolYear.endDate ]
|
||||
, td [ class "has-text-centered" ]
|
||||
[ if schoolYear.isActive then
|
||||
span [ class "tag is-success" ] [ text "Aktiv" ]
|
||||
|
||||
else
|
||||
span [ class "tag is-light" ] [ text "Inaktiv" ]
|
||||
]
|
||||
, td [ class "has-text-centered" ]
|
||||
[ if not schoolYear.isActive then
|
||||
button
|
||||
[ class "button is-small is-info mr-2"
|
||||
, onClick (ActivateSchoolYear schoolYear.id)
|
||||
]
|
||||
[ text "Aktivieren" ]
|
||||
|
||||
else
|
||||
text ""
|
||||
, button
|
||||
[ class "button is-small is-danger"
|
||||
, onClick (DeleteSchoolYear schoolYear.id)
|
||||
]
|
||||
[ text "Löschen" ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
||||
-- HTTP
|
||||
|
||||
|
|
@ -3795,3 +4141,84 @@ fetchMyInfo token =
|
|||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
fetchSchoolYears : String -> Cmd Msg
|
||||
fetchSchoolYears token =
|
||||
Http.request
|
||||
{ method = "GET"
|
||||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||
, url = "/api/admin/school-years"
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectJson SchoolYearsReceived (Decode.list schoolYearDecoder)
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
fetchActiveSchoolYear : String -> Cmd Msg
|
||||
fetchActiveSchoolYear token =
|
||||
Http.request
|
||||
{ method = "GET"
|
||||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||
, url = "/api/school-year/active"
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectJson ActiveSchoolYearReceived schoolYearDecoder
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
createSchoolYear : String -> NewSchoolYear -> Cmd Msg
|
||||
createSchoolYear token schoolYear =
|
||||
Http.request
|
||||
{ method = "POST"
|
||||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||
, url = "/api/admin/school-years"
|
||||
, body =
|
||||
Http.jsonBody <|
|
||||
Encode.object
|
||||
[ ( "name", Encode.string schoolYear.name )
|
||||
, ( "start_date", Encode.string schoolYear.startDate )
|
||||
, ( "end_date", Encode.string schoolYear.endDate )
|
||||
]
|
||||
, expect = Http.expectWhatever SchoolYearCreated
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
activateSchoolYear : String -> Int -> Cmd Msg
|
||||
activateSchoolYear token id =
|
||||
Http.request
|
||||
{ method = "PUT"
|
||||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||
, url = "/api/admin/school-years/" ++ String.fromInt id ++ "/activate"
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectWhatever SchoolYearActivated
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
deleteSchoolYear : String -> Int -> Cmd Msg
|
||||
deleteSchoolYear token id =
|
||||
Http.request
|
||||
{ method = "DELETE"
|
||||
, headers = [ Http.header "Authorization" ("Bearer " ++ token) ]
|
||||
, url = "/api/admin/school-years/" ++ String.fromInt id
|
||||
, body = Http.emptyBody
|
||||
, expect = Http.expectWhatever SchoolYearDeleted
|
||||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
schoolYearDecoder : Decoder SchoolYear
|
||||
schoolYearDecoder =
|
||||
Decode.map5 SchoolYear
|
||||
(field "id" int)
|
||||
(field "name" string)
|
||||
(field "start_date" string)
|
||||
(field "end_date" string)
|
||||
(field "is_active" bool)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue