feat: improve app security and error handling
Improve overall app security by: - using dynamic statements for all sql querries - introducing environment variables for initial admin password - introducing enironment variable for cors address - improving error handling
This commit is contained in:
parent
95057c1b8d
commit
3ac1947106
11 changed files with 1333 additions and 453 deletions
|
|
@ -1,164 +1,338 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta charset="UTF-8" />
|
||||
<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">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css" />
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
|
||||
|
||||
<style>
|
||||
/* Custom Styles */
|
||||
/* Toast-Container */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Basis-Toast */
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
pointer-events: all;
|
||||
min-width: 320px;
|
||||
transition: all 0.3s ease;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.toast:hover {
|
||||
transform: translateX(-5px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Toast-Content */
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Close-Button */
|
||||
.toast-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
margin-left: 12px;
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
transition: color 0.2s ease;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
/* Toast-Typen */
|
||||
.toast-error {
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffe5e5 100%);
|
||||
border-left-color: #e53e3e;
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: linear-gradient(135deg, #f0fff4 0%, #e6ffed 100%);
|
||||
border-left-color: #38a169;
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: #38a169;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: linear-gradient(135deg, #ebf8ff 0%, #e0f3ff 100%);
|
||||
border-left-color: #3182ce;
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: #3182ce;
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
background: linear-gradient(135deg, #fffaf0 0%, #fff5e6 100%);
|
||||
border-left-color: #dd6b20;
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: #dd6b20;
|
||||
}
|
||||
|
||||
/* Animationen */
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toast.dismissing {
|
||||
animation: slideOut 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
/* Mobile Anpassungen */
|
||||
@media screen and (max-width: 768px) {
|
||||
.toast-container {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support (optional) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.toast {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
color: #1a202c;
|
||||
}
|
||||
|
||||
.toast-close {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.toast-close:hover {
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
.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); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="elm"></div>
|
||||
|
||||
|
||||
<script src="/elm.js"></script>
|
||||
<script>
|
||||
// LocalStorage Helper
|
||||
function getStoredData() {
|
||||
try {
|
||||
const data = localStorage.getItem('timetracking');
|
||||
const data = localStorage.getItem("timetracking");
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse stored data:', e);
|
||||
console.error("Failed to parse stored data:", e);
|
||||
}
|
||||
return { token: null, isAdmin: false };
|
||||
return {token: null, isAdmin: false};
|
||||
}
|
||||
|
||||
|
||||
function saveData(token, isAdmin) {
|
||||
try {
|
||||
localStorage.setItem('timetracking', JSON.stringify({
|
||||
token: token,
|
||||
isAdmin: isAdmin
|
||||
}));
|
||||
localStorage.setItem(
|
||||
"timetracking",
|
||||
JSON.stringify({
|
||||
token: token,
|
||||
isAdmin: isAdmin,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to save data:', e);
|
||||
console.error("Failed to save data:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clearData() {
|
||||
try {
|
||||
localStorage.removeItem('timetracking');
|
||||
localStorage.removeItem("timetracking");
|
||||
} catch (e) {
|
||||
console.error('Failed to clear data:', 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'),
|
||||
node: document.getElementById("elm"),
|
||||
flags: {
|
||||
token: storedData.token,
|
||||
isAdmin: storedData.isAdmin
|
||||
}
|
||||
isAdmin: storedData.isAdmin,
|
||||
},
|
||||
});
|
||||
|
||||
// Port: Token speichern
|
||||
app.ports.saveToken.subscribe(function(data) {
|
||||
|
||||
app.ports.saveToken.subscribe(function (data) {
|
||||
saveData(data.token, data.isAdmin);
|
||||
});
|
||||
|
||||
// Port: Token entfernen
|
||||
app.ports.removeToken.subscribe(function() {
|
||||
|
||||
app.ports.removeToken.subscribe(function () {
|
||||
clearData();
|
||||
});
|
||||
|
||||
// Port: Lösch-Bestätigung
|
||||
app.ports.confirmDelete.subscribe(function(message) {
|
||||
|
||||
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
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
function setupBurgerMenu() {
|
||||
const burgers = document.querySelectorAll('.navbar-burger');
|
||||
|
||||
burgers.forEach(burger => {
|
||||
burger.addEventListener('click', () => {
|
||||
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');
|
||||
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'), {
|
||||
|
||||
observer.observe(document.getElementById("elm"), {
|
||||
childList: true,
|
||||
subtree: 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');
|
||||
|
||||
if (
|
||||
"serviceWorker" in navigator &&
|
||||
window.location.protocol === "https:"
|
||||
) {
|
||||
navigator.serviceWorker.register("/sw.js").catch(() => {
|
||||
console.log("Service Worker registration failed");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Html.Events exposing (..)
|
|||
import Http
|
||||
import Json.Decode as Decode exposing (Decoder, bool, field, float, int, list, string)
|
||||
import Json.Encode as Encode
|
||||
import Process
|
||||
import Task
|
||||
import Time
|
||||
|
||||
|
|
@ -99,6 +100,23 @@ type alias Model =
|
|||
, 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
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -299,6 +317,8 @@ init flags =
|
|||
, newSchoolYear = NewSchoolYear "" "" ""
|
||||
, activeSchoolYear = Nothing
|
||||
, editingSchoolYearId = Nothing
|
||||
, toasts = []
|
||||
, nextToastId = 0
|
||||
}
|
||||
|
||||
cmd =
|
||||
|
|
@ -309,7 +329,11 @@ init flags =
|
|||
, fetchSchedules (Just token)
|
||||
, fetchYearlyHoursSummary token
|
||||
, if flags.isAdmin then
|
||||
fetchSchoolYears token
|
||||
Cmd.batch
|
||||
[ fetchSchoolYears token
|
||||
, fetchUsers token
|
||||
, fetchAllTimeEntries token
|
||||
]
|
||||
|
||||
else
|
||||
fetchMyInfo token
|
||||
|
|
@ -434,6 +458,9 @@ type Msg
|
|||
| SchoolYearDeleted (Result Http.Error ())
|
||||
| DownloadYearlySummaryPDF
|
||||
| YearlySummaryPDFReceived (Result Http.Error Bytes.Bytes)
|
||||
| ShowToast String ToastType
|
||||
| DismissToast Int
|
||||
| AutoDismissToast Int
|
||||
|
||||
|
||||
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||
|
|
@ -487,6 +514,7 @@ update msg model =
|
|||
, Cmd.batch
|
||||
[ saveToken tokenData
|
||||
, fetchSchedules (Just result.token)
|
||||
, Task.perform (\_ -> ShowToast ("Willkommen, " ++ result.username ++ "!") SuccessToast) (Task.succeed ())
|
||||
, if not result.isAdmin then
|
||||
Cmd.batch
|
||||
[ fetchMyTimeEntries result.token
|
||||
|
|
@ -506,8 +534,25 @@ update msg model =
|
|||
]
|
||||
)
|
||||
|
||||
LoginResponse (Err _) ->
|
||||
( { model | error = Just "Login fehlgeschlagen", isProcessing = False }, Cmd.none )
|
||||
LoginResponse (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 ())
|
||||
)
|
||||
|
||||
Logout ->
|
||||
( { model
|
||||
|
|
@ -527,8 +572,8 @@ update msg model =
|
|||
SchedulesReceived (Ok schedules) ->
|
||||
( { model | schedules = schedules }, Cmd.none )
|
||||
|
||||
SchedulesReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden des Stundenplans" }, Cmd.none )
|
||||
SchedulesReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
ToggleScheduleSelection scheduleId dayOfWeek ->
|
||||
let
|
||||
|
|
@ -564,14 +609,15 @@ update msg model =
|
|||
}
|
||||
, Cmd.batch
|
||||
[ fetchMyTimeEntries token
|
||||
, Task.perform (\_ -> ShowToast "Zeiteinträge erfolgreich gespeichert!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
TimeEntriesSaved (Err _) ->
|
||||
( { model | error = Just "Fehler beim Speichern" }, Cmd.none )
|
||||
TimeEntriesSaved (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
PreviousWeek ->
|
||||
let
|
||||
|
|
@ -628,8 +674,8 @@ update msg model =
|
|||
WeekDatesReceived (Ok weekDates) ->
|
||||
( { model | weekDates = Just weekDates }, Cmd.none )
|
||||
|
||||
WeekDatesReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Wochendaten" }, Cmd.none )
|
||||
WeekDatesReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
CheckWeekHasEntries ->
|
||||
case model.token of
|
||||
|
|
@ -642,8 +688,8 @@ update msg model =
|
|||
WeekHasEntriesReceived (Ok hasEntries) ->
|
||||
( { model | hasEntriesForCurrentWeek = hasEntries }, Cmd.none )
|
||||
|
||||
WeekHasEntriesReceived (Err _) ->
|
||||
( model, Cmd.none )
|
||||
WeekHasEntriesReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
SetTime time ->
|
||||
let
|
||||
|
|
@ -740,14 +786,17 @@ update msg model =
|
|||
, selectedEntries = []
|
||||
, hasEntriesForCurrentWeek = False
|
||||
}
|
||||
, fetchMyTimeEntries token
|
||||
, Cmd.batch
|
||||
[ fetchMyTimeEntries token
|
||||
, Task.perform (\_ -> ShowToast "Wocheneinträge erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
WeekEntriesDeleted (Err _) ->
|
||||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||||
WeekEntriesDeleted (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
SwitchTab tab ->
|
||||
let
|
||||
|
|
@ -844,7 +893,7 @@ update msg model =
|
|||
|| String.isEmpty model.newSchedule.startTime
|
||||
|| String.isEmpty model.newSchedule.endTime
|
||||
then
|
||||
( { model | error = Just "Bitte alle Felder ausfüllen" }, Cmd.none )
|
||||
( model, Task.perform (\_ -> ShowToast "Bitte alle Felder ausfüllen" WarningToast) (Task.succeed ()) )
|
||||
|
||||
else
|
||||
case model.token of
|
||||
|
|
@ -866,37 +915,17 @@ update msg model =
|
|||
, error = Nothing
|
||||
, isProcessing = False
|
||||
}
|
||||
, fetchSchedules model.token
|
||||
, Cmd.batch
|
||||
[ fetchSchedules model.token
|
||||
, Task.perform (\_ -> ShowToast "Stundenplan erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
ScheduleCreated (Err err) ->
|
||||
let
|
||||
errorMsg =
|
||||
case err of
|
||||
Http.BadStatus 400 ->
|
||||
"Ungültige Eingabe"
|
||||
|
||||
Http.BadStatus 409 ->
|
||||
"Dieser Stundenplan existiert bereits"
|
||||
|
||||
Http.Timeout ->
|
||||
"Anfrage abgelaufen"
|
||||
|
||||
Http.NetworkError ->
|
||||
"Netzwerkfehler"
|
||||
|
||||
_ ->
|
||||
"Fehler beim Erstellen"
|
||||
in
|
||||
( { model
|
||||
| error = Just errorMsg
|
||||
, isProcessing = False
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
( { model | isProcessing = False }, handleApiError err )
|
||||
|
||||
DeleteSchedule scheduleId ->
|
||||
case model.token of
|
||||
|
|
@ -909,13 +938,18 @@ update msg model =
|
|||
ScheduleDeleted (Ok _) ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model | error = Nothing }, fetchSchedules (Just token) )
|
||||
( { model | error = Nothing }
|
||||
, Cmd.batch
|
||||
[ fetchSchedules (Just token)
|
||||
, Task.perform (\_ -> ShowToast "Stundenplan erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
ScheduleDeleted (Err _) ->
|
||||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||||
ScheduleDeleted (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
UpdateNewUsername username ->
|
||||
let
|
||||
|
|
@ -962,13 +996,18 @@ update msg model =
|
|||
in
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model | newUser = emptyUser }, fetchUsers token )
|
||||
( { model | newUser = emptyUser }
|
||||
, Cmd.batch
|
||||
[ fetchUsers token
|
||||
, Task.perform (\_ -> ShowToast "Benutzer erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
UserCreated (Err _) ->
|
||||
( { model | error = Just "Fehler beim Erstellen des Benutzers" }, Cmd.none )
|
||||
UserCreated (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
DeleteUser userId ->
|
||||
case model.token of
|
||||
|
|
@ -987,14 +1026,17 @@ update msg model =
|
|||
, editingUserId = Nothing
|
||||
, resetPasswordUserId = Nothing
|
||||
}
|
||||
, fetchUsers token
|
||||
, Cmd.batch
|
||||
[ fetchUsers token
|
||||
, Task.perform (\_ -> ShowToast "Benutzer erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
UserDeleted (Err _) ->
|
||||
( { model | error = Just "Fehler beim Löschen des Benutzers", pendingDeleteId = Nothing }, Cmd.none )
|
||||
UserDeleted (Err err) ->
|
||||
( { model | pendingDeleteId = Nothing }, handleApiError err )
|
||||
|
||||
FetchUsers ->
|
||||
case model.token of
|
||||
|
|
@ -1007,8 +1049,8 @@ update msg model =
|
|||
UsersReceived (Ok users) ->
|
||||
( { model | users = users }, Cmd.none )
|
||||
|
||||
UsersReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Benutzer" }, Cmd.none )
|
||||
UsersReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
FetchMyTimeEntries ->
|
||||
case model.token of
|
||||
|
|
@ -1039,8 +1081,8 @@ update msg model =
|
|||
, Cmd.none
|
||||
)
|
||||
|
||||
MyTimeEntriesReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Einträge" }, Cmd.none )
|
||||
MyTimeEntriesReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
FetchAllTimeEntries ->
|
||||
case model.token of
|
||||
|
|
@ -1053,8 +1095,8 @@ update msg model =
|
|||
AllTimeEntriesReceived (Ok entries) ->
|
||||
( { model | timeEntries = entries }, Cmd.none )
|
||||
|
||||
AllTimeEntriesReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Zeiteinträge" }, Cmd.none )
|
||||
AllTimeEntriesReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
FetchWeeklyHours ->
|
||||
case model.token of
|
||||
|
|
@ -1067,8 +1109,8 @@ update msg model =
|
|||
WeeklyHoursReceived (Ok hours) ->
|
||||
( { model | weeklyHours = hours }, Cmd.none )
|
||||
|
||||
WeeklyHoursReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Wochenstunden" }, Cmd.none )
|
||||
WeeklyHoursReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
FetchYearlyHoursSummary ->
|
||||
case model.token of
|
||||
|
|
@ -1081,8 +1123,8 @@ update msg model =
|
|||
YearlyHoursSummaryReceived (Ok summary) ->
|
||||
( { model | yearlyHoursSummary = summary }, Cmd.none )
|
||||
|
||||
YearlyHoursSummaryReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Jahresübersicht" }, Cmd.none )
|
||||
YearlyHoursSummaryReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
MyWeeklySummaryReceived (Ok summary) ->
|
||||
( { model | userWeeklySummary = Just summary }, Cmd.none )
|
||||
|
|
@ -1176,16 +1218,16 @@ update msg model =
|
|||
}
|
||||
, Cmd.batch
|
||||
[ fetchAllTimeEntries token
|
||||
, fetchWeeklyHours token
|
||||
, fetchYearlyHoursSummary token
|
||||
, Task.perform (\_ -> ShowToast "Zeiteintrag erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
TimeEntryDeleted (Err _) ->
|
||||
( { model | error = Just "Fehler beim Löschen des Eintrags", pendingDeleteId = Nothing }, Cmd.none )
|
||||
TimeEntryDeleted (Err err) ->
|
||||
( { model | pendingDeleteId = Nothing }, handleApiError err )
|
||||
|
||||
EditUserWorkHours userId ->
|
||||
case List.filter (\u -> u.id == userId) model.users |> List.head of
|
||||
|
|
@ -1247,18 +1289,21 @@ update msg model =
|
|||
( { model
|
||||
| resetPasswordUserId = Nothing
|
||||
, resetPasswordNew = ""
|
||||
, error = Just "Passwort erfolgreich zurückgesetzt"
|
||||
, error = Nothing
|
||||
}
|
||||
, case model.token of
|
||||
Just token ->
|
||||
fetchUsers token
|
||||
, Cmd.batch
|
||||
[ case model.token of
|
||||
Just token ->
|
||||
fetchUsers token
|
||||
|
||||
Nothing ->
|
||||
Cmd.none
|
||||
Nothing ->
|
||||
Cmd.none
|
||||
, Task.perform (\_ -> ShowToast "Passwort erfolgreich zurückgesetzt" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
ResetPasswordSaved (Err _) ->
|
||||
( { model | error = Just "Fehler beim Zurücksetzen des Passworts" }, Cmd.none )
|
||||
ResetPasswordSaved (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
StartEditingTimeEntry entryId entry ->
|
||||
( { model
|
||||
|
|
@ -1332,14 +1377,17 @@ update msg model =
|
|||
, pendingDeleteId = Nothing
|
||||
, error = Nothing
|
||||
}
|
||||
, fetchAllTimeEntries token
|
||||
, Cmd.batch
|
||||
[ fetchAllTimeEntries token
|
||||
, Task.perform (\_ -> ShowToast "Zeiteintrag erfolgreich gespeichert!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
TimeEntrySaved (Err _) ->
|
||||
( { model | error = Just "Fehler beim Speichern des Eintrags" }, Cmd.none )
|
||||
TimeEntrySaved (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
ConfirmDeleteTimeEntry entryId ->
|
||||
( { model | pendingDeleteId = Just entryId }, confirmDelete "Soll dieser Zeiteintrag gelöscht werden?" )
|
||||
|
|
@ -1379,7 +1427,7 @@ update msg model =
|
|||
( model, updateUserWorkHours token userId (String.fromFloat hours) )
|
||||
|
||||
_ ->
|
||||
( { model | error = Just "Ungültige Eingabe für Arbeitszeit" }, Cmd.none )
|
||||
( model, Task.perform (\_ -> ShowToast "Ungültige Eingabe für Arbeitszeit" WarningToast) (Task.succeed ()) )
|
||||
|
||||
UserWorkHoursSaved (Ok _) ->
|
||||
case model.token of
|
||||
|
|
@ -1389,14 +1437,17 @@ update msg model =
|
|||
, editingUserId = Nothing
|
||||
, error = Nothing
|
||||
}
|
||||
, fetchUsers token
|
||||
, Cmd.batch
|
||||
[ fetchUsers token
|
||||
, Task.perform (\_ -> ShowToast "Arbeitszeit erfolgreich gespeichert!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
UserWorkHoursSaved (Err _) ->
|
||||
( { model | error = Just "Fehler beim Speichern der Arbeitszeit" }, Cmd.none )
|
||||
UserWorkHoursSaved (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
UpdateUserPassword input ->
|
||||
( { model | userPasswordInput = input }, Cmd.none )
|
||||
|
|
@ -1408,10 +1459,10 @@ update msg model =
|
|||
( model, resetUserPassword token userId model.userPasswordInput )
|
||||
|
||||
else
|
||||
( { model | error = Just "Passwort erforderlich" }, Cmd.none )
|
||||
( model, Task.perform (\_ -> ShowToast "Passwort erforderlich" WarningToast) (Task.succeed ()) )
|
||||
|
||||
_ ->
|
||||
( { model | error = Just "Passwort erforderlich" }, Cmd.none )
|
||||
( model, Task.perform (\_ -> ShowToast "Passwort erforderlich" WarningToast) (Task.succeed ()) )
|
||||
|
||||
UserPasswordSaved (Ok _) ->
|
||||
( { model
|
||||
|
|
@ -1419,11 +1470,11 @@ update msg model =
|
|||
, selectedUserId = Nothing
|
||||
, error = Nothing
|
||||
}
|
||||
, Cmd.none
|
||||
, Task.perform (\_ -> ShowToast "Passwort erfolgreich zurückgesetzt!" SuccessToast) (Task.succeed ())
|
||||
)
|
||||
|
||||
UserPasswordSaved (Err _) ->
|
||||
( { model | error = Just "Fehler beim Zurücksetzen des Passworts" }, Cmd.none )
|
||||
UserPasswordSaved (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
SelectUserForManualEntry userId ->
|
||||
let
|
||||
|
|
@ -1472,15 +1523,15 @@ update msg model =
|
|||
, Cmd.batch
|
||||
[ fetchAllTimeEntries token
|
||||
, fetchYearlyHoursSummary token
|
||||
, fetchWeeklyHours token
|
||||
, Task.perform (\_ -> ShowToast "Manueller Eintrag erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
AdminTimeEntrySaved (Err _) ->
|
||||
( { model | error = Just "Fehler beim Erstellen des Eintrags", isProcessing = False }, Cmd.none )
|
||||
AdminTimeEntrySaved (Err err) ->
|
||||
( { model | isProcessing = False }, handleApiError err )
|
||||
|
||||
FetchMyInfo ->
|
||||
case model.token of
|
||||
|
|
@ -1493,8 +1544,8 @@ update msg model =
|
|||
MyInfoReceived (Ok user) ->
|
||||
( { model | users = [ user ] }, Cmd.none )
|
||||
|
||||
MyInfoReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden deiner Daten" }, Cmd.none )
|
||||
MyInfoReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
FetchSchoolYears ->
|
||||
case model.token of
|
||||
|
|
@ -1507,8 +1558,8 @@ update msg model =
|
|||
SchoolYearsReceived (Ok years) ->
|
||||
( { model | schoolYears = years }, Cmd.none )
|
||||
|
||||
SchoolYearsReceived (Err _) ->
|
||||
( { model | error = Just "Fehler beim Laden der Schuljahre" }, Cmd.none )
|
||||
SchoolYearsReceived (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
FetchActiveSchoolYear ->
|
||||
case model.token of
|
||||
|
|
@ -1560,7 +1611,7 @@ update msg model =
|
|||
|| String.isEmpty model.newSchoolYear.startDate
|
||||
|| String.isEmpty model.newSchoolYear.endDate
|
||||
then
|
||||
( { model | error = Just "Bitte alle Felder ausfüllen" }, Cmd.none )
|
||||
( model, Task.perform (\_ -> ShowToast "Bitte alle Felder ausfüllen" WarningToast) (Task.succeed ()) )
|
||||
|
||||
else
|
||||
case model.token of
|
||||
|
|
@ -1578,19 +1629,17 @@ update msg model =
|
|||
, error = Nothing
|
||||
, isProcessing = False
|
||||
}
|
||||
, fetchSchoolYears token
|
||||
, Cmd.batch
|
||||
[ fetchSchoolYears token
|
||||
, Task.perform (\_ -> ShowToast "Schuljahr erfolgreich erstellt!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearCreated (Err _) ->
|
||||
( { model
|
||||
| error = Just "Fehler beim Erstellen des Schuljahres"
|
||||
, isProcessing = False
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
SchoolYearCreated (Err err) ->
|
||||
( { model | isProcessing = False }, handleApiError err )
|
||||
|
||||
ActivateSchoolYear id ->
|
||||
case model.token of
|
||||
|
|
@ -1607,14 +1656,15 @@ update msg model =
|
|||
, Cmd.batch
|
||||
[ fetchSchoolYears token
|
||||
, fetchActiveSchoolYear token
|
||||
, Task.perform (\_ -> ShowToast "Schuljahr erfolgreich aktiviert!" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearActivated (Err _) ->
|
||||
( { model | error = Just "Fehler beim Aktivieren" }, Cmd.none )
|
||||
SchoolYearActivated (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
DeleteSchoolYear id ->
|
||||
case model.token of
|
||||
|
|
@ -1627,13 +1677,18 @@ update msg model =
|
|||
SchoolYearDeleted (Ok _) ->
|
||||
case model.token of
|
||||
Just token ->
|
||||
( { model | error = Nothing }, fetchSchoolYears token )
|
||||
( { model | error = Nothing }
|
||||
, Cmd.batch
|
||||
[ fetchSchoolYears token
|
||||
, Task.perform (\_ -> ShowToast "Schuljahr erfolgreich gelöscht" SuccessToast) (Task.succeed ())
|
||||
]
|
||||
)
|
||||
|
||||
Nothing ->
|
||||
( model, Cmd.none )
|
||||
|
||||
SchoolYearDeleted (Err _) ->
|
||||
( { model | error = Just "Fehler beim Löschen" }, Cmd.none )
|
||||
SchoolYearDeleted (Err err) ->
|
||||
( model, handleApiError err )
|
||||
|
||||
DownloadYearlySummaryPDF ->
|
||||
case model.token of
|
||||
|
|
@ -1650,11 +1705,47 @@ update msg model =
|
|||
in
|
||||
( { model | isProcessing = False }, File.Download.bytes filename "application/pdf" pdfBytes )
|
||||
|
||||
YearlySummaryPDFReceived (Err _) ->
|
||||
YearlySummaryPDFReceived (Err err) ->
|
||||
( { model | isProcessing = False }, handleApiError err )
|
||||
|
||||
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
|
||||
| error = Just "Fehler beim Herunterladen der PDF"
|
||||
, isProcessing = False
|
||||
| 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
|
||||
)
|
||||
|
||||
|
|
@ -2031,18 +2122,77 @@ calculateHours startTime endTime =
|
|||
-- VIEW
|
||||
|
||||
|
||||
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 ""
|
||||
]
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
div [ class "container" ]
|
||||
[ case model.page of
|
||||
LoginPage ->
|
||||
viewLogin model
|
||||
div [ class "app-container" ]
|
||||
[ viewToasts model.toasts
|
||||
, div [ class "container" ]
|
||||
[ case model.page of
|
||||
LoginPage ->
|
||||
viewLogin model
|
||||
|
||||
UserDashboard ->
|
||||
viewUserDashboard model
|
||||
UserDashboard ->
|
||||
viewUserDashboard model
|
||||
|
||||
AdminDashboard ->
|
||||
viewAdminDashboard model
|
||||
AdminDashboard ->
|
||||
viewAdminDashboard model
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -2054,12 +2204,6 @@ viewLogin model =
|
|||
[ div [ class "column is-5-tablet is-4-desktop is-3-widescreen" ]
|
||||
[ div [ class "box" ]
|
||||
[ h1 [ class "title has-text-centered" ] [ text "Zeiterfassung Login" ]
|
||||
, case model.error of
|
||||
Just err ->
|
||||
div [ class "notification is-danger" ] [ text err ]
|
||||
|
||||
Nothing ->
|
||||
text ""
|
||||
, div [ class "field" ]
|
||||
[ label [ class "label" ] [ text "Benutzername" ]
|
||||
, div [ class "control" ]
|
||||
|
|
@ -2249,12 +2393,6 @@ viewUserDashboard model =
|
|||
text ""
|
||||
, h3 [ class "subtitle mt-6" ] [ text "Jahresgesamtzeit" ]
|
||||
, viewUserYearlyTotal model
|
||||
, case model.error of
|
||||
Just err ->
|
||||
div [ class "notification is-danger mt-4" ] [ text err ]
|
||||
|
||||
Nothing ->
|
||||
text ""
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
@ -4311,3 +4449,50 @@ downloadYearlySummaryPDF token =
|
|||
, timeout = Nothing
|
||||
, tracker = Nothing
|
||||
}
|
||||
|
||||
|
||||
type alias ApiError =
|
||||
{ code : String
|
||||
, message : String
|
||||
}
|
||||
|
||||
|
||||
apiErrorDecoder : Decoder ApiError
|
||||
apiErrorDecoder =
|
||||
Decode.map2 ApiError
|
||||
(field "code" string)
|
||||
(field "message" string)
|
||||
|
||||
|
||||
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 ())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue