fix: Add Mobile View and fix error while freeze on entering new schedule

This commit is contained in:
Patryk Hegenberg 2025-11-05 17:09:37 +01:00
parent 5001cc1147
commit 9c25956711
6 changed files with 746 additions and 392 deletions

View file

@ -2,35 +2,163 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Schulzeit Erfassung</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Zeiterfassung</title>
<!-- Bulma CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
html, body {
height: 100%;
/* Custom Styles */
body {
min-height: 100vh;
}
.table-container {
overflow-x: auto;
}
/* Responsive Verbesserungen */
@media screen and (max-width: 768px) {
.level {
flex-direction: column;
}
.level-left, .level-right {
width: 100%;
}
.level-item {
justify-content: center;
margin-bottom: 0.5rem;
}
.buttons {
flex-wrap: wrap;
}
.button {
margin-bottom: 0.5rem;
}
}
/* Loading Spinner */
.fa-spinner {
animation: fa-spin 1s infinite linear;
}
@keyframes fa-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div id="app"></div>
<div id="elm"></div>
<script src="/elm.js"></script>
<script>
var storedToken = localStorage.getItem('authToken');
// LocalStorage Helper
function getStoredData() {
try {
const data = localStorage.getItem('timetracking');
if (data) {
return JSON.parse(data);
}
} catch (e) {
console.error('Failed to parse stored data:', e);
}
return { token: null, isAdmin: false };
}
var app = Elm.Main.init({
node: document.getElementById('app'),
flags: storedToken
function saveData(token, isAdmin) {
try {
localStorage.setItem('timetracking', JSON.stringify({
token: token,
isAdmin: isAdmin
}));
} catch (e) {
console.error('Failed to save data:', e);
}
}
function clearData() {
try {
localStorage.removeItem('timetracking');
} catch (e) {
console.error('Failed to clear data:', e);
}
}
// Initialisiere Elm App mit gespeicherten Daten
const storedData = getStoredData();
const app = Elm.Main.init({
node: document.getElementById('elm'),
flags: {
token: storedData.token,
isAdmin: storedData.isAdmin
}
});
// Save token to localStorage
app.ports.saveToken.subscribe(function(token) {
localStorage.setItem('authToken', token);
// Port: Token speichern
app.ports.saveToken.subscribe(function(data) {
saveData(data.token, data.isAdmin);
});
// Remove token from localStorage
// Port: Token entfernen
app.ports.removeToken.subscribe(function() {
localStorage.removeItem('authToken');
clearData();
});
// Port: Lösch-Bestätigung
app.ports.confirmDelete.subscribe(function(message) {
const confirmed = confirm(message);
app.ports.confirmDeleteResponse.send(confirmed);
});
// BUGFIX: Responsive Navbar Toggle
document.addEventListener('DOMContentLoaded', () => {
// Funktion für Burger-Menu
function setupBurgerMenu() {
const burgers = document.querySelectorAll('.navbar-burger');
burgers.forEach(burger => {
burger.addEventListener('click', () => {
const target = burger.dataset.target;
const menu = document.getElementById(target);
if (menu) {
burger.classList.toggle('is-active');
menu.classList.toggle('is-active');
}
});
});
}
// Initial setup
setupBurgerMenu();
// Observer für dynamische Änderungen (wenn Elm DOM updated)
const observer = new MutationObserver((mutations) => {
setupBurgerMenu();
});
observer.observe(document.getElementById('elm'), {
childList: true,
subtree: true
});
});
// Service Worker für Offline-Fähigkeit (optional)
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
navigator.serviceWorker.register('/sw.js').catch(() => {
console.log('Service Worker registration failed');
});
}
</script>
</body>
</html>

View file

@ -14,15 +14,14 @@ import Dict exposing (Dict)
-- PORTS
port saveToken : String -> Cmd msg
port saveToken : Encode.Value -> Cmd msg
port removeToken : () -> Cmd msg
port confirmDelete : String -> Cmd msg
port confirmDeleteResponse : (Bool -> msg) -> Sub msg
-- MAIN
main : Program (Maybe String) Model Msg
main : Program Flags Model Msg
main =
Browser.element
{ init = init
@ -31,6 +30,11 @@ main =
, view = view
}
-- FLAGS
type alias Flags =
{ token : Maybe String
, isAdmin : Bool
}
-- MODEL
@ -56,17 +60,19 @@ type alias Model =
, error : Maybe String
, weekEditMode : Bool
, hasEntriesForCurrentWeek : Bool
, userWeeklySummary : Maybe WeeklySummary -- NEU
, editingTimeEntryId : Maybe Int -- NEU
, editingTimeEntry : EditingTimeEntry -- NEU
, editingUserId : Maybe Int -- NEU
, editingUserWorkHours : String -- NEU
, resetPasswordUserId : Maybe Int -- NEU
, resetPasswordNew : String -- NEU
, pendingDeleteId : Maybe Int -- NEU: Speichert die ID die gelöscht werden soll
, selectedUserId : Maybe Int -- NEU
, 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
}
type Page
@ -92,7 +98,7 @@ type alias User =
{ id : Int
, username : String
, isAdmin : Bool
, weeklyWorkHours : Float -- NEU
, weeklyWorkHours : Float
}
type alias TimeEntry =
@ -160,16 +166,23 @@ type alias WeeklyHours =
, remainingHours : Float
}
init : Maybe String -> (Model, Cmd Msg)
init storedToken =
init : Flags -> (Model, Cmd Msg)
init flags =
let
initialPage =
case flags.token of
Just _ ->
if flags.isAdmin then AdminDashboard else UserDashboard
Nothing ->
LoginPage
model =
{ page = if storedToken /= Nothing then UserDashboard else LoginPage
{ page = initialPage
, activeTab = ScheduleTab
, username = ""
, password = ""
, token = storedToken
, isAdmin = False
, token = flags.token
, isAdmin = flags.isAdmin
, schedules = []
, users = []
, timeEntries = []
@ -185,21 +198,23 @@ init storedToken =
, weekEditMode = False
, hasEntriesForCurrentWeek = False
, weekDates = Nothing
, userWeeklySummary = Nothing -- NEU
, editingTimeEntryId = Nothing -- NEU
, editingTimeEntry = EditingTimeEntry 0 "" "" "" "" -- NEU
, editingUserId = Nothing -- NEU
, editingUserWorkHours = "" -- NEU
, resetPasswordUserId = Nothing -- NEU
, resetPasswordNew = "" -- NEU
, pendingDeleteId = Nothing -- NEU!
, selectedUserId = Nothing -- NEU
, userWeeklySummary = Nothing
, editingTimeEntryId = Nothing
, editingTimeEntry = EditingTimeEntry 0 "" "" "" ""
, editingUserId = Nothing
, editingUserWorkHours = ""
, resetPasswordUserId = Nothing
, resetPasswordNew = ""
, pendingDeleteId = Nothing
, selectedUserId = Nothing
, userWorkHoursInput = ""
, userPasswordInput = ""
, isProcessing = False
, mobileMenuOpen = False
}
cmd =
case storedToken of
case flags.token of
Just token ->
Cmd.batch
[ Task.perform SetTime Time.now
@ -259,30 +274,30 @@ type Msg
| WeekDatesReceived (Result Http.Error WeekDates)
| CheckWeekHasEntries
| WeekHasEntriesReceived (Result Http.Error Bool)
| FetchMyWeeklySummary -- NEU
| MyWeeklySummaryReceived (Result Http.Error WeeklySummary) -- NEU
| EditTimeEntry Int -- NEU
| CancelEditTimeEntry -- NEU
| UpdateEditTimeEntryDate String -- NEU
| UpdateEditTimeEntryStartTime String -- NEU
| UpdateEditTimeEntryEndTime String -- NEU
| UpdateEditTimeEntryType String -- NEU
| SaveEditTimeEntry -- NEU
| TimeEntrySaved (Result Http.Error ()) -- NEU
| TimeEntryDeleted (Result Http.Error ()) -- NEU
| EditUserWorkHours Int -- NEU
| CancelEditUserWorkHours -- NEU
| UpdateEditUserWorkHours String -- NEU
| SaveUserWorkHours -- NEU
| UserWorkHoursSaved (Result Http.Error ()) -- NEU
| ResetUserPassword Int -- NEU
| CancelResetPassword -- NEU
| UpdateResetPasswordNew String -- NEU
| SaveResetPassword -- NEU
| ResetPasswordSaved (Result Http.Error ()) -- NEU
| ConfirmDeleteTimeEntry Int -- NEU
| ConfirmDeleteUser Int -- NEU
| DeleteConfirmed Bool -- NEU
| FetchMyWeeklySummary
| 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
@ -295,10 +310,18 @@ type Msg
| UpdateUserPassword String
| SaveUserPassword
| UserPasswordSaved (Result Http.Error ())
| ToggleMobileMenu
| CloseMobileMenu
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ToggleMobileMenu ->
({ model | mobileMenuOpen = not model.mobileMenuOpen }, Cmd.none)
CloseMobileMenu ->
({ model | mobileMenuOpen = False }, Cmd.none)
UpdateUsername username ->
({ model | username = username }, Cmd.none)
@ -306,13 +329,21 @@ update msg model =
({ model | password = password }, Cmd.none)
Login ->
(model, loginRequest model.username model.password)
if model.isProcessing then
(model, Cmd.none)
else
({ model | isProcessing = True }, loginRequest model.username model.password)
LoginResponse (Ok result) ->
let
newPage = if result.isAdmin then AdminDashboard else UserDashboard
(year, week) = getISOWeekFromPosix model.currentTime
tokenData = Encode.object
[ ("token", Encode.string result.token)
, ("isAdmin", Encode.bool result.isAdmin)
]
in
({ model
| token = Just result.token
@ -320,27 +351,28 @@ update msg model =
, isAdmin = result.isAdmin
, page = newPage
, error = Nothing
, isProcessing = False
}, Cmd.batch
[ saveToken result.token
[ saveToken tokenData
, fetchSchedules (Just result.token)
, if not result.isAdmin then
Cmd.batch
[ fetchMyTimeEntries result.token
, fetchWeekDates result.token year week
, checkWeekHasEntries result.token year week
, fetchMyWeeklySummary result.token year week -- NEU!
, fetchMyWeeklySummary result.token year week
]
else
Cmd.batch
[ fetchMyTimeEntries result.token
, fetchWeekDates result.token year week
, checkWeekHasEntries result.token year week
, fetchMyWeeklySummary result.token year week -- NEU!
, fetchMyWeeklySummary result.token year week
]
])
LoginResponse (Err _) ->
({ model | error = Just "Login fehlgeschlagen" }, Cmd.none)
({ model | error = Just "Login fehlgeschlagen", isProcessing = False }, Cmd.none)
Logout ->
({ model
@ -349,6 +381,7 @@ update msg model =
, isAdmin = False
, username = ""
, password = ""
, isProcessing = False
}, removeToken ())
FetchSchedules ->
@ -389,7 +422,7 @@ update msg model =
, hasEntriesForCurrentWeek = True
}, Cmd.batch
[ fetchMyTimeEntries token
, fetchMyWeeklySummary token model.currentYear model.currentWeek -- NEU!
, fetchMyWeeklySummary token model.currentYear model.currentWeek
])
Nothing ->
(model, Cmd.none)
@ -411,7 +444,7 @@ update msg model =
Cmd.batch
[ fetchWeekDates token newYear newWeek
, checkWeekHasEntries token newYear newWeek
, fetchMyWeeklySummary token newYear newWeek -- NEU!
, fetchMyWeeklySummary token newYear newWeek
]
Nothing ->
Cmd.none
@ -431,7 +464,7 @@ update msg model =
Cmd.batch
[ fetchWeekDates token newYear newWeek
, checkWeekHasEntries token newYear newWeek
, fetchMyWeeklySummary token newYear newWeek -- NEU!
, fetchMyWeeklySummary token newYear newWeek
]
Nothing ->
Cmd.none
@ -474,7 +507,7 @@ update msg model =
[ checkWeekHasEntries token year week
, fetchWeekDates token year week
, fetchMyTimeEntries token
, fetchMyWeeklySummary token year week -- NEU!
, fetchMyWeeklySummary token year week
]
else
Cmd.none
@ -564,7 +597,7 @@ update msg model =
_ ->
Cmd.none
in
({ model | activeTab = tab }, cmd)
({ model | activeTab = tab, mobileMenuOpen = False}, cmd)
UpdateNewScheduleDay day ->
let
@ -602,20 +635,45 @@ update msg model =
({ model | newSchedule = newSchedule }, Cmd.none)
CreateSchedule ->
if String.isEmpty model.newSchedule.dayOfWeek ||
String.isEmpty model.newSchedule.startTime ||
String.isEmpty model.newSchedule.endTime then
({ model | error = Just "Bitte alle Felder ausfüllen" }, Cmd.none)
else
case model.token of
Just token ->
({ model | isProcessing = True }, createSchedule token model.newSchedule)
Nothing ->
(model, Cmd.none)
ScheduleCreated (Ok _) ->
case model.token of
Just token ->
(model, createSchedule token model.newSchedule)
let
emptySchedule = NewSchedule "" "" "" "lesson" ""
in
({ model
| newSchedule = emptySchedule
, error = Nothing
, isProcessing = False
}, fetchSchedules model.token)
Nothing ->
(model, Cmd.none)
ScheduleCreated (Ok _) ->
ScheduleCreated (Err err) ->
let
emptySchedule = NewSchedule "" "" "" "lesson" ""
errorMsg = case err of
Http.BadStatus 400 -> "Ungültige Eingabe"
Http.BadStatus 409 -> "Dieser Stundenplan existiert bereits"
Http.Timeout -> "Anfrage abgelaufen"
Http.NetworkError -> "Netzwerkfehler"
_ -> "Fehler beim Erstellen"
in
({ model | newSchedule = emptySchedule }, fetchSchedules model.token)
ScheduleCreated (Err _) ->
({ model | error = Just "Fehler beim Erstellen" }, Cmd.none)
({ model
| error = Just errorMsg
, isProcessing = False
}, Cmd.none)
DeleteSchedule scheduleId ->
case model.token of
@ -989,7 +1047,7 @@ update msg model =
({ model | userWorkHoursInput = input }, Cmd.none)
SaveUserWorkHours ->
case (model.token, model.editingUserId, String.toFloat model.editingUserWorkHours) of -- ← Änderungen!
case (model.token, model.editingUserId, String.toFloat model.editingUserWorkHours) of
(Just token, Just userId, Just hours) ->
(model, updateUserWorkHours token userId (String.fromFloat hours))
_ ->
@ -999,28 +1057,12 @@ update msg model =
case model.token of
Just token ->
({ model
| editingUserWorkHours = "" -- ← Änderung
, editingUserId = Nothing -- ← Änderung
| editingUserWorkHours = ""
, editingUserId = Nothing
, error = Nothing
}, fetchUsers token)
Nothing ->
(model, Cmd.none)
-- SaveUserWorkHours ->
-- case (model.token, model.selectedUserId, String.toFloat model.userWorkHoursInput) of
-- (Just token, Just userId, Just hours) ->
-- (model, updateUserWorkHours token userId (String.fromFloat hours))
-- _ ->
-- ({ model | error = Just "Ungültige Eingabe für Arbeitszeit" }, Cmd.none)
-- UserWorkHoursSaved (Ok _) ->
-- case model.token of
-- Just token ->
-- ({ model
-- | userWorkHoursInput = ""
-- , error = Nothing
-- }, fetchUsers token)
-- Nothing ->
-- (model, Cmd.none)
UserWorkHoursSaved (Err _) ->
({ model | error = Just "Fehler beim Speichern der Arbeitszeit" }, Cmd.none)
@ -1055,7 +1097,7 @@ update msg model =
subscriptions : Model -> Sub Msg
subscriptions model =
confirmDeleteResponse DeleteConfirmed -- NEU
confirmDeleteResponse DeleteConfirmed
-- HELPER FUNCTIONS
@ -1324,18 +1366,38 @@ viewLogin model =
viewUserDashboard : Model -> Html Msg
viewUserDashboard model =
div []
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 "")) -- NEU!
]
, div [ class "navbar-menu" ]
[ div [ class "navbar-end" ]
[ div [ class "navbar-item" ]
[ span [ class "has-text-white mr-4" ] [ text model.username ]
, button [ class "button is-light", onClick Logout ] [ text "Abmelden" ]
[ 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" ]
]
]
]
]
@ -1360,6 +1422,7 @@ viewUserDashboard model =
[ button
[ class "button is-warning"
, onClick EnableEditMode
, disabled model.isProcessing
] [ text "Bearbeiten" ]
]
]
@ -1380,6 +1443,7 @@ viewUserDashboard model =
[ 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"
@ -1400,8 +1464,14 @@ viewUserDashboard model =
[ button
[ class "button is-primary is-large is-fullwidth"
, onClick SaveTimeEntries
, disabled (List.isEmpty model.selectedEntries)
] [ text (if model.weekEditMode then "Änderungen speichern" else "Speichern") ]
, 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
@ -1418,6 +1488,151 @@ viewUserDashboard model =
]
]
viewAdminDashboard : Model -> Html Msg
viewAdminDashboard model =
div []
[ nav [ class "navbar is-danger" ]
[ div [ class "navbar-brand" ]
[ div [ class "navbar-item" ]
[ h1 [ class "title is-4 has-text-white" ] [ text "Admin Dashboard" ]
]
, a
[ class ("navbar-burger" ++ (if model.mobileMenuOpen then " is-active" else ""))
, attribute "aria-label" "menu"
, attribute "aria-expanded" (if model.mobileMenuOpen then "true" else "false")
, onClick ToggleMobileMenu
]
[ span [ attribute "aria-hidden" "true" ] []
, span [ attribute "aria-hidden" "true" ] []
, span [ attribute "aria-hidden" "true" ] []
]
]
, div
[ id "navbarAdmin"
, class ("navbar-menu" ++ (if model.mobileMenuOpen then " is-active" else ""))
]
[ div [ class "navbar-end" ]
[ div [ class "navbar-item" ]
[ span [ class "has-text-white mr-2" ] [ text model.username ]
]
, div [ class "navbar-item" ]
[ button [ class "button is-light", onClick Logout ]
[ span [ class "icon" ]
[ i [ class "fas fa-sign-out-alt" ] [] ]
, span [] [ text "Abmelden" ]
]
]
]
]
]
, section [ class "section" ]
[ div [ class "container" ]
[ div [ class "tabs is-boxed" ]
[ ul []
[ li [ classList [("is-active", model.activeTab == ScheduleTab)] ]
[ a [ onClick (SwitchTab ScheduleTab) ] [ text "Stundenplan" ] ]
, li [ classList [("is-active", model.activeTab == UsersTab)] ]
[ a [ onClick (SwitchTab UsersTab) ] [ text "Benutzer" ] ]
, li [ classList [("is-active", model.activeTab == TimeEntriesTab)] ]
[ a [ onClick (SwitchTab TimeEntriesTab) ] [ text "Zeiteinträge" ] ]
]
]
, case model.activeTab of
ScheduleTab ->
viewScheduleTab model
UsersTab ->
viewUsersTab model
TimeEntriesTab ->
viewTimeEntriesTab model
]
]
]
viewScheduleItemWithDay : Model -> Int -> Schedule -> Html Msg
viewScheduleItemWithDay model dayOfWeek schedule =
let
isSelected = List.any (\e -> e.scheduleId == schedule.id && e.dayOfWeek == dayOfWeek) model.selectedEntries
isClickable = (not model.hasEntriesForCurrentWeek || model.weekEditMode) && not model.isProcessing
boxClass =
if isSelected then
"box has-background-success-light"
else if isClickable then
"box has-background-white"
else
"box has-background-light"
typeText = if schedule.scheduleType == "break" then " (Pause)" else ""
cursorStyle = if isClickable then "pointer" else "not-allowed"
opacity = if isClickable || isSelected then "1" else "0.6"
in
div
[ class boxClass
, onClick (if isClickable then ToggleScheduleSelection schedule.id dayOfWeek else FetchSchedules)
, style "cursor" cursorStyle
, style "margin-bottom" "0.5rem"
, style "padding" "0.75rem"
, style "opacity" opacity
, style "transition" "all 0.2s ease"
, style "border" (if isClickable && not isSelected then "2px solid transparent" else "2px solid currentColor")
]
[ p [ class "has-text-weight-bold is-size-7" ]
[ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
, p [ class "is-size-7" ]
[ text (schedule.title ++ typeText) ]
]
viewScheduleGridWithWeek : Model -> Html Msg
viewScheduleGridWithWeek model =
let
days = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag"]
groupedSchedules = List.range 0 4
|> List.map (\day ->
( day, List.filter (\s -> s.dayOfWeek == day) model.schedules )
)
in
div []
[
div [ class "is-hidden-mobile" ]
[ div [ class "table-container" ]
[ table [ class "table is-bordered is-fullwidth" ]
[ thead []
[ tr [] (List.map (\day -> th [ class "has-text-centered" ] [ text day ]) days)
]
, tbody []
[ tr []
(List.map (viewDayColumnWithWeek model) groupedSchedules)
]
]
]
]
, div [ class "is-hidden-tablet" ]
(List.map2 (viewDayMobile model) days groupedSchedules)
]
viewDayMobile : Model -> String -> (Int, List Schedule) -> Html Msg
viewDayMobile model dayName (dayOfWeek, schedules) =
let
dateForDay =
case model.weekDates of
Just wd ->
wd.dates
|> List.filter (\(day, _) -> day == String.fromInt dayOfWeek)
|> List.head
|> Maybe.map Tuple.second
|> Maybe.withDefault "N/A"
Nothing ->
"Laden..."
in
div [ class "box mb-4" ]
[ p [ class "has-text-weight-bold has-text-centered mb-3" ]
[ text (dayName ++ " - " ++ dateForDay) ]
, div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules)
]
viewUserWeeklySummary : Model -> Html Msg
viewUserWeeklySummary model =
case model.userWeeklySummary of
@ -1462,46 +1677,6 @@ viewUserWeeklySummary model =
[ p [ class "has-text-centered has-text-grey" ] [ text "Laden..." ]
]
viewAdminDashboard : Model -> Html Msg
viewAdminDashboard model =
div []
[ nav [ class "navbar is-danger" ]
[ div [ class "navbar-brand" ]
[ div [ class "navbar-item" ]
[ h1 [ class "title is-4 has-text-white" ] [ text "Admin Dashboard" ]
]
]
, div [ class "navbar-menu" ]
[ div [ class "navbar-end" ]
[ div [ class "navbar-item" ]
[ span [ class "has-text-white mr-4" ] [ text model.username ]
, button [ class "button is-light", onClick Logout ] [ text "Abmelden" ]
]
]
]
]
, section [ class "section" ]
[ div [ class "container" ]
[ div [ class "tabs is-boxed" ]
[ ul []
[ li [ classList [("is-active", model.activeTab == ScheduleTab)] ]
[ a [ onClick (SwitchTab ScheduleTab) ] [ text "Stundenplan" ] ]
, li [ classList [("is-active", model.activeTab == UsersTab)] ]
[ a [ onClick (SwitchTab UsersTab) ] [ text "Benutzer" ] ]
, li [ classList [("is-active", model.activeTab == TimeEntriesTab)] ]
[ a [ onClick (SwitchTab TimeEntriesTab) ] [ text "Zeiteinträge" ] ]
]
]
, case model.activeTab of
ScheduleTab ->
viewScheduleTab model
UsersTab ->
viewUsersTab model
TimeEntriesTab ->
viewTimeEntriesTab model
]
]
]
viewScheduleTab : Model -> Html Msg
viewScheduleTab model =
@ -1534,7 +1709,6 @@ viewTimeEntriesTab model =
viewTimeEntriesListWithEdit model
]
-- Separate Edit Form View
viewTimeEntriesEditForm : Model -> Html Msg
viewTimeEntriesEditForm model =
div [ class "box has-background-warning-light" ]
@ -1649,7 +1823,6 @@ viewTimeEntryRowWithEdit model entry =
isEditing = model.editingTimeEntryId == Just entry.id
in
if isEditing then
-- Edit-Modus
tr []
[ td [] [ text entry.username ]
, td []
@ -1752,28 +1925,6 @@ viewWeekNavigation 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 [ 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)
]
]
]
viewDayColumnWithWeek : Model -> (Int, List Schedule) -> Html Msg
viewDayColumnWithWeek model (dayOfWeek, schedules) =
let
@ -1794,37 +1945,6 @@ viewDayColumnWithWeek model (dayOfWeek, schedules) =
, div [] (List.map (viewScheduleItemWithDay model dayOfWeek) schedules)
]
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
boxClass =
if isSelected then
"box has-background-success-light"
else
"box has-background-white"
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 CheckWeekHasEntries) -- Dummy-Event wenn nicht klickbar
, style "cursor" cursorStyle
, style "margin-bottom" "0.5rem"
, style "padding" "0.75rem"
, style "opacity" opacity
]
[ p [ class "has-text-weight-bold is-size-7" ]
[ text (schedule.startTime ++ " - " ++ schedule.endTime) ]
, p [ class "is-size-7" ]
[ text (schedule.title ++ typeText) ]
]
viewScheduleForm : Model -> Html Msg
viewScheduleForm model =
@ -1835,7 +1955,11 @@ viewScheduleForm model =
[ label [ class "label" ] [ text "Wochentag" ]
, div [ class "control" ]
[ div [ class "select is-fullwidth" ]
[ select [ onInput UpdateNewScheduleDay ]
[ select
[ onInput UpdateNewScheduleDay
, disabled model.isProcessing
, value model.newSchedule.dayOfWeek
]
[ option [ value "" ] [ text "Wochentag wählen" ]
, option [ value "0" ] [ text "Montag" ]
, option [ value "1" ] [ text "Dienstag" ]
@ -1856,6 +1980,7 @@ viewScheduleForm model =
, type_ "time"
, value model.newSchedule.startTime
, onInput UpdateNewScheduleStart
, disabled model.isProcessing
] []
]
]
@ -1869,6 +1994,7 @@ viewScheduleForm model =
, type_ "time"
, value model.newSchedule.endTime
, onInput UpdateNewScheduleEnd
, disabled model.isProcessing
] []
]
]
@ -1880,7 +2006,11 @@ viewScheduleForm model =
[ label [ class "label" ] [ text "Typ" ]
, div [ class "control" ]
[ div [ class "select is-fullwidth" ]
[ select [ onInput UpdateNewScheduleType, value model.newSchedule.scheduleType ]
[ select
[ onInput UpdateNewScheduleType
, value model.newSchedule.scheduleType
, disabled model.isProcessing
]
[ option [ value "lesson" ] [ text "Unterricht" ]
, option [ value "break" ] [ text "Pause" ]
]
@ -1898,6 +2028,7 @@ viewScheduleForm model =
, placeholder "z.B. Mathematik"
, value model.newSchedule.title
, onInput UpdateNewScheduleTitle
, disabled model.isProcessing
] []
]
]
@ -1905,9 +2036,23 @@ viewScheduleForm model =
]
, div [ class "field" ]
[ div [ class "control" ]
[ button [ class "button is-primary", onClick CreateSchedule ] [ text "Hinzufügen" ]
[ button
[ class "button is-primary"
, onClick CreateSchedule
, disabled (String.isEmpty model.newSchedule.dayOfWeek || model.isProcessing)
]
[ if model.isProcessing then
span [ class "icon" ] [ i [ class "fas fa-spinner fa-pulse" ] [] ]
else
text ""
, text " Hinzufügen"
]
]
]
, if String.isEmpty model.newSchedule.dayOfWeek then
div [ class "help is-warning" ] [ text "Bitte alle Felder ausfüllen" ]
else
text ""
]
viewScheduleList : Model -> Html Msg
@ -2035,7 +2180,6 @@ viewUserList model =
viewUserRowWithActions : Model -> User -> Html Msg
viewUserRowWithActions model user =
if model.editingUserId == Just user.id then
-- Edit Work Hours Mode
tr []
[ td [] [ text (String.fromInt user.id) ]
, td [] [ text user.username ]
@ -2207,7 +2351,7 @@ viewTimeEntriesList model =
]
]
, tbody []
(List.map (viewTimeEntryRowWithActions model) filteredEntries) -- KORRIGIERT: model übergeben
(List.map (viewTimeEntryRowWithActions model) filteredEntries)
]
]
@ -2490,8 +2634,8 @@ weeklyHoursDecoder =
(field "year" int)
(field "week" int)
(field "total_hours" float)
(field "expected_hours" float) -- NEU
(field "remaining_hours" float) -- NEU
(field "expected_hours" float)
(field "remaining_hours" float)
fetchWeekDates : String -> Int -> Int -> Cmd Msg
fetchWeekDates token year week =