Compare commits
6 Commits
2d8747c971
...
23896e4f08
| Author | SHA1 | Date | |
|---|---|---|---|
| 23896e4f08 | |||
| 7e54800bc3 | |||
| 61ce5aab3a | |||
| 1d7b563a6d | |||
| 46218f9bca | |||
| 8911165c4b |
@@ -35,7 +35,7 @@ func autoLogout(w http.ResponseWriter) {
|
|||||||
fmt.Printf("Error logging out user %v\n", err)
|
fmt.Printf("Error logging out user %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
loggedOutUsers = append(loggedOutUsers, user)
|
loggedOutUsers = append(loggedOutUsers, user)
|
||||||
log.Printf("Automaticaly logged out user %s, %s ", user.Name, user.Vorname)
|
log.Printf("Automaticaly logged out user %d ", user.PersonalNummer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,66 +21,7 @@ import (
|
|||||||
const DE_DATE string = "02.01.2006"
|
const DE_DATE string = "02.01.2006"
|
||||||
const FILE_YEAR_MONTH string = "2006_01"
|
const FILE_YEAR_MONTH string = "2006_01"
|
||||||
|
|
||||||
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) {
|
var PDF_DIRECTORY = helper.GetEnv("PDF_PATH", "/doc/") // TODO
|
||||||
var typstDays []typstDay
|
|
||||||
for _, day := range days {
|
|
||||||
var thisTypstDay typstDay
|
|
||||||
work, pause, overtime := day.GetTimes(u, models.WorktimeBaseDay, false)
|
|
||||||
workVirtual := day.GetWorktime(u, models.WorktimeBaseDay, true)
|
|
||||||
overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2)
|
|
||||||
thisTypstDay.Date = day.Date().Format(DE_DATE)
|
|
||||||
thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true)
|
|
||||||
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
|
|
||||||
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
|
|
||||||
thisTypstDay.IsFriday = day.Date().Weekday() == time.Friday
|
|
||||||
|
|
||||||
if workVirtual > work {
|
|
||||||
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true)
|
|
||||||
} else {
|
|
||||||
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
thisTypstDay.DayParts = convertDayToTypstDayParts(day, u)
|
|
||||||
typstDays = append(typstDays, thisTypstDay)
|
|
||||||
}
|
|
||||||
return typstDays, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertDayToTypstDayParts(day models.IWorkDay, user models.User) []typstDayPart {
|
|
||||||
var typstDayParts []typstDayPart
|
|
||||||
switch day.Type() {
|
|
||||||
case models.DayTypeWorkday:
|
|
||||||
workDay, _ := day.(*models.WorkDay)
|
|
||||||
for i := 0; i < len(workDay.Bookings); i += 2 {
|
|
||||||
var typstDayPart typstDayPart
|
|
||||||
typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04")
|
|
||||||
if i+1 < len(workDay.Bookings) {
|
|
||||||
typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04")
|
|
||||||
} else {
|
|
||||||
typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04")
|
|
||||||
}
|
|
||||||
typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name
|
|
||||||
typstDayPart.IsWorkDay = true
|
|
||||||
typstDayParts = append(typstDayParts, typstDayPart)
|
|
||||||
}
|
|
||||||
if day.IsKurzArbeit() && len(workDay.Bookings) > 0 {
|
|
||||||
tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user)
|
|
||||||
typstDayParts = append(typstDayParts, typstDayPart{
|
|
||||||
BookingFrom: tsFrom.Format("15:04"),
|
|
||||||
BookingTo: tsTo.Format("15:04"),
|
|
||||||
WorkType: "Kurzarbeit",
|
|
||||||
IsWorkDay: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case models.DayTypeCompound:
|
|
||||||
for _, c := range day.(*models.CompoundDay).DayParts {
|
|
||||||
typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user)...)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()})
|
|
||||||
}
|
|
||||||
return typstDayParts
|
|
||||||
}
|
|
||||||
|
|
||||||
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
||||||
helper.RequiresLogin(Session, w, r)
|
helper.RequiresLogin(Session, w, r)
|
||||||
@@ -101,6 +42,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !helper.IsDebug() {
|
||||||
n := 0
|
n := 0
|
||||||
for _, e := range employes {
|
for _, e := range employes {
|
||||||
if user.IsSuperior(e) {
|
if user.IsSuperior(e) {
|
||||||
@@ -109,6 +51,7 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
employes = employes[:n]
|
employes = employes[:n]
|
||||||
|
}
|
||||||
|
|
||||||
reportData := createReports(employes, startDate)
|
reportData := createReports(employes, startDate)
|
||||||
|
|
||||||
@@ -119,8 +62,9 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.Warn("Could not create pdf report", slog.Any("Error", err))
|
slog.Warn("Could not create pdf report", slog.Any("Error", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-type", "application/pdf")
|
w.Header().Set("Content-type", "application/pdf")
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH)))
|
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=Monatsabrechnung_%s.pdf", startDate.Format(FILE_YEAR_MONTH)))
|
||||||
output.WriteTo(w)
|
output.WriteTo(w)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
case "download":
|
case "download":
|
||||||
@@ -131,11 +75,12 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
output, err := zipPfd(pdfReports, &reportData)
|
output, err := zipPfd(pdfReports, &reportData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Could not create pdf report", slog.Any("Error", err))
|
slog.Warn("Could not zip pdf reports", slog.Any("Error", err))
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-type", "application/zip")
|
w.Header().Set("Content-type", "application/zip")
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s", startDate.Format(FILE_YEAR_MONTH)))
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachement; filename=Monatsabrechnung_%s.zip", startDate.Format(FILE_YEAR_MONTH)))
|
||||||
output.WriteTo(w)
|
output.WriteTo(w)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -145,6 +90,75 @@ func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertDaysToTypst(days []models.IWorkDay, u models.User, weekbase models.WorktimeBase) ([]typstDay, error) {
|
||||||
|
var typstDays []typstDay
|
||||||
|
for i, day := range days {
|
||||||
|
if !day.IsSubmittedAndAccepted() && !helper.IsDebug() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var thisTypstDay typstDay
|
||||||
|
workVirtual, pause, overtime := day.GetTimes(u, weekbase, true)
|
||||||
|
overtime = workVirtual - u.ArbeitszeitProWocheFrac(0.2)
|
||||||
|
|
||||||
|
if day.Type() == models.DayTypeHoliday {
|
||||||
|
// workVirtual = 0
|
||||||
|
overtime = 0
|
||||||
|
}
|
||||||
|
thisTypstDay.Date = day.Date().Format(DE_DATE)
|
||||||
|
thisTypstDay.Worktime = helper.FormatDurationFill(workVirtual, true)
|
||||||
|
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
|
||||||
|
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
|
||||||
|
thisTypstDay.IsFriday = i == len(days)-1
|
||||||
|
|
||||||
|
if work := day.GetWorktime(u, weekbase, false); workVirtual > work {
|
||||||
|
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(workVirtual-work, true)
|
||||||
|
} else {
|
||||||
|
thisTypstDay.Kurzarbeit = helper.FormatDurationFill(0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
thisTypstDay.DayParts = convertDayToTypstDayParts(day, u, weekbase)
|
||||||
|
typstDays = append(typstDays, thisTypstDay)
|
||||||
|
}
|
||||||
|
return typstDays, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertDayToTypstDayParts(day models.IWorkDay, user models.User, weekBase models.WorktimeBase) []typstDayPart {
|
||||||
|
var typstDayParts []typstDayPart
|
||||||
|
switch day.Type() {
|
||||||
|
case models.DayTypeWorkday:
|
||||||
|
workDay, _ := day.(*models.WorkDay)
|
||||||
|
for i := 0; i < len(workDay.Bookings); i += 2 {
|
||||||
|
var typstDayPart typstDayPart
|
||||||
|
typstDayPart.BookingFrom = workDay.Bookings[i].Timestamp.Format("15:04")
|
||||||
|
if i+1 < len(workDay.Bookings) {
|
||||||
|
typstDayPart.BookingTo = workDay.Bookings[i+1].Timestamp.Format("15:04")
|
||||||
|
} else {
|
||||||
|
typstDayPart.BookingTo = workDay.Bookings[i].Timestamp.Format("15:04")
|
||||||
|
}
|
||||||
|
typstDayPart.WorkType = workDay.Bookings[i].BookingType.Name
|
||||||
|
typstDayPart.IsWorkDay = true
|
||||||
|
typstDayParts = append(typstDayParts, typstDayPart)
|
||||||
|
}
|
||||||
|
if day.IsKurzArbeit() {
|
||||||
|
tsFrom, tsTo := workDay.GenerateKurzArbeitBookings(user, weekBase)
|
||||||
|
typstDayParts = append(typstDayParts, typstDayPart{
|
||||||
|
BookingFrom: tsFrom.Format("15:04"),
|
||||||
|
BookingTo: tsTo.Format("15:04"),
|
||||||
|
WorkType: "Kurzarbeit",
|
||||||
|
IsWorkDay: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case models.DayTypeCompound:
|
||||||
|
for _, c := range day.(*models.CompoundDay).DayParts {
|
||||||
|
typstDayParts = append(typstDayParts, convertDayToTypstDayParts(c, user, weekBase)...)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
typstDayParts = append(typstDayParts, typstDayPart{IsWorkDay: false, WorkType: day.ToString()})
|
||||||
|
}
|
||||||
|
return typstDayParts
|
||||||
|
}
|
||||||
|
|
||||||
func createReports(employes []models.User, startDate time.Time) []typstData {
|
func createReports(employes []models.User, startDate time.Time) []typstData {
|
||||||
startDate = helper.GetFirstOfMonth(startDate)
|
startDate = helper.GetFirstOfMonth(startDate)
|
||||||
endDate := startDate.AddDate(0, 1, -1)
|
endDate := startDate.AddDate(0, 1, -1)
|
||||||
@@ -161,37 +175,49 @@ func createReports(employes []models.User, startDate time.Time) []typstData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) {
|
func createEmployeReport(employee models.User, startDate, endDate time.Time) (typstData, error) {
|
||||||
publicHolidays, err := models.GetHolidaysFromTo(startDate, endDate)
|
// publicHolidays, _ := models.GetHolidaysFromTo(startDate, endDate)
|
||||||
targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)-len(publicHolidays))
|
targetHoursThisMonth := employee.ArbeitszeitProWocheFrac(.2) * time.Duration(helper.GetWorkingDays(startDate, endDate)) //-len(publicHolidays)
|
||||||
workDaysThisMonth := models.GetDays(employee, startDate, endDate.AddDate(0, 0, 1), false)
|
mondaysThisMonth := helper.GetMondays(helper.GenerateDateRange(startDate, endDate), false)
|
||||||
|
|
||||||
slog.Debug("Baseline Working hours", "targetHours", targetHoursThisMonth.Hours())
|
|
||||||
|
|
||||||
|
var weeks []models.WorkWeek
|
||||||
var workHours, kurzarbeitHours time.Duration
|
var workHours, kurzarbeitHours time.Duration
|
||||||
for _, day := range workDaysThisMonth {
|
|
||||||
tmpvirtualHours := day.GetWorktime(employee, models.WorktimeBaseDay, true)
|
|
||||||
tmpactualHours := day.GetWorktime(employee, models.WorktimeBaseDay, false)
|
|
||||||
if day.IsKurzArbeit() && tmpvirtualHours > tmpactualHours {
|
|
||||||
slog.Debug("Adding kurzarbeit to workday", "day", day.Date())
|
|
||||||
kurzarbeitHours += tmpvirtualHours - tmpactualHours
|
|
||||||
}
|
|
||||||
workHours += tmpvirtualHours
|
|
||||||
}
|
|
||||||
worktimeBalance := workHours - targetHoursThisMonth
|
|
||||||
|
|
||||||
typstDays, err := convertDaysToTypst(workDaysThisMonth, employee)
|
for _, monday := range mondaysThisMonth {
|
||||||
|
var week models.WorkWeek
|
||||||
|
if monday.After(startDate) {
|
||||||
|
week = models.NewWorkWeekSimple(employee, monday, true)
|
||||||
|
} else if startDate.Sub(monday) < time.Hour*24*6 {
|
||||||
|
week = models.NewWorkWeek(employee, startDate, monday.Add(6*24*time.Hour), true)
|
||||||
|
}
|
||||||
|
workHours += week.WorktimeVirtual
|
||||||
|
kurzarbeitHours += week.Kurzarbeit
|
||||||
|
weeks = append(weeks, week)
|
||||||
|
}
|
||||||
|
|
||||||
|
monthOvertime := workHours - targetHoursThisMonth
|
||||||
|
totalOvertime, err := employee.GetReportedOvertime(endDate)
|
||||||
|
|
||||||
|
var typstDays []typstDay
|
||||||
|
for _, week := range weeks {
|
||||||
|
weekTypstDays, err := convertDaysToTypst(week.Days, employee, week.WeekBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Failed to convert to days", slog.Any("error", err))
|
slog.Error("Error converting days into typst", "error", err)
|
||||||
return typstData{}, err
|
continue
|
||||||
|
}
|
||||||
|
typstDays = append(typstDays, weekTypstDays...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Cannot retrieve total Overtime", "Error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata := typstMetadata{
|
metadata := typstMetadata{
|
||||||
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
|
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
|
||||||
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
|
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
|
||||||
Overtime: helper.FormatDurationFill(worktimeBalance, true),
|
Overtime: helper.FormatDurationFill(monthOvertime, true),
|
||||||
WorkTime: helper.FormatDurationFill(workHours, true),
|
WorkTime: helper.FormatDurationFill(workHours, true),
|
||||||
Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true),
|
Kurzarbeit: helper.FormatDurationFill(kurzarbeitHours, true),
|
||||||
OvertimeTotal: "",
|
OvertimeTotal: helper.FormatDurationFill(totalOvertime+monthOvertime, true),
|
||||||
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
|
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
|
||||||
}
|
}
|
||||||
return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil
|
return typstData{Meta: metadata, Days: typstDays, FileName: fmt.Sprintf("%s_%s.pdf", startDate.Format(FILE_YEAR_MONTH), employee.Name)}, nil
|
||||||
@@ -202,8 +228,7 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
|
|||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
|
|
||||||
typstCLI := typst.CLI{
|
typstCLI := typst.CLI{
|
||||||
WorkingDirectory: "/doc/",
|
WorkingDirectory: PDF_DIRECTORY,
|
||||||
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
|
if err := typst.InjectValues(&markup, map[string]any{"data": data}); err != nil {
|
||||||
@@ -230,8 +255,7 @@ func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) {
|
|||||||
var outputMulti []bytes.Buffer
|
var outputMulti []bytes.Buffer
|
||||||
|
|
||||||
typstRender := typst.CLI{
|
typstRender := typst.CLI{
|
||||||
WorkingDirectory: "/doc/",
|
WorkingDirectory: PDF_DIRECTORY,
|
||||||
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
@@ -273,6 +297,16 @@ func zipPfd(pdfReports []bytes.Buffer, reportData *[]typstData) (bytes.Buffer, e
|
|||||||
return zipOutput, err
|
return zipOutput, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lenWorkDays(workDays []models.IWorkDay) int {
|
||||||
|
var lenght int
|
||||||
|
for _, day := range workDays {
|
||||||
|
if !day.IsEmpty() || day.IsKurzArbeit() {
|
||||||
|
lenght += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lenght
|
||||||
|
}
|
||||||
|
|
||||||
type typstMetadata struct {
|
type typstMetadata struct {
|
||||||
TimeRange string `json:"time-range"`
|
TimeRange string `json:"time-range"`
|
||||||
EmployeeName string `json:"employee-name"`
|
EmployeeName string `json:"employee-name"`
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
workWeek := models.NewWorkWeek(user, weekTs, true)
|
workWeek := models.NewWorkWeekSimple(user, weekTs, true)
|
||||||
|
|
||||||
switch r.FormValue("method") {
|
switch r.FormValue("method") {
|
||||||
case "send":
|
case "send":
|
||||||
@@ -70,7 +70,7 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
|
|||||||
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
|
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
|
||||||
lastSub := helper.GetMonday(submissionDate)
|
lastSub := helper.GetMonday(submissionDate)
|
||||||
|
|
||||||
userWeek := models.NewWorkWeek(user, lastSub, true)
|
userWeek := models.NewWorkWeekSimple(user, lastSub, true)
|
||||||
|
|
||||||
var workWeeks []models.WorkWeek
|
var workWeeks []models.WorkWeek
|
||||||
teamMembers, err := user.GetTeamMembers()
|
teamMembers, err := user.GetTeamMembers()
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true)
|
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true)
|
||||||
}
|
}
|
||||||
if reportedOvertime, err := user.GetReportedOvertime(); err == nil {
|
if reportedOvertime, err := user.GetReportedOvertime(time.Now()); err == nil {
|
||||||
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
|
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
|
||||||
} else {
|
} else {
|
||||||
log.Println("Cannot calculate overtime: ", err)
|
log.Println("Cannot calculate overtime: ", err)
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ func GetEnv(key, fallback string) string {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDebug() bool {
|
||||||
|
return GetEnv("GO_ENV", "production") == "debug"
|
||||||
|
}
|
||||||
|
|
||||||
type CacheItem struct {
|
type CacheItem struct {
|
||||||
value any
|
value any
|
||||||
expiration time.Time
|
expiration time.Time
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package helper
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +19,64 @@ func GetMonday(ts time.Time) time.Time {
|
|||||||
return ts
|
return ts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetMondays(allDays []time.Time, onlyInRange bool) []time.Time {
|
||||||
|
var mondays []time.Time
|
||||||
|
var start, end time.Time
|
||||||
|
|
||||||
|
for _, day := range allDays {
|
||||||
|
mondays = append(mondays, GetMonday(day))
|
||||||
|
|
||||||
|
if start.IsZero() || day.Before(start) {
|
||||||
|
start = day
|
||||||
|
}
|
||||||
|
if end.IsZero() || day.After(end) {
|
||||||
|
end = day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mondays = slices.Compact(mondays)
|
||||||
|
if onlyInRange {
|
||||||
|
return DaysInRange(mondays, start, end)
|
||||||
|
}
|
||||||
|
return mondays
|
||||||
|
}
|
||||||
|
|
||||||
|
func DaysInRange(days []time.Time, startDate, endDate time.Time) []time.Time {
|
||||||
|
filtered := []time.Time{}
|
||||||
|
startDate = startDate.Add(-time.Minute)
|
||||||
|
endDate = endDate.Add(time.Minute)
|
||||||
|
|
||||||
|
for _, day := range days {
|
||||||
|
if day.After(startDate) && day.Before(endDate) {
|
||||||
|
filtered = append(filtered, day)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsMonday(day time.Time) bool {
|
||||||
|
return day.Weekday() == time.Monday
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateDateRange returns a slice of all dates between start and end (inclusive).
|
||||||
|
func GenerateDateRange(start, end time.Time) []time.Time {
|
||||||
|
var dates []time.Time
|
||||||
|
|
||||||
|
// Ensure start is before or equal to end
|
||||||
|
if start.After(end) {
|
||||||
|
return dates
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize times to midnight
|
||||||
|
current := start.Truncate(time.Hour * 24)
|
||||||
|
end = end.Truncate(time.Hour * 24)
|
||||||
|
|
||||||
|
for !current.After(end) {
|
||||||
|
dates = append(dates, current)
|
||||||
|
current = current.AddDate(0, 0, 1) // Add one day
|
||||||
|
}
|
||||||
|
return dates
|
||||||
|
}
|
||||||
|
|
||||||
func GetFirstOfMonth(ts time.Time) time.Time {
|
func GetFirstOfMonth(ts time.Time) time.Time {
|
||||||
if ts.Day() > 1 {
|
if ts.Day() > 1 {
|
||||||
return ts.AddDate(0, 0, -(ts.Day() - 1))
|
return ts.AddDate(0, 0, -(ts.Day() - 1))
|
||||||
|
|||||||
@@ -26,6 +26,101 @@ func TestGetMonday(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsMonday_ReturnsTrueForMonday(t *testing.T) {
|
||||||
|
monday := time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
if !IsMonday(monday) {
|
||||||
|
t.Errorf("Expected IsMonday to return true for Monday, got false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsMonday_ReturnsFalseForNonMonday(t *testing.T) {
|
||||||
|
tuesday := time.Date(2023, 4, 4, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
if IsMonday(tuesday) {
|
||||||
|
t.Errorf("Expected IsMonday to return false for Tuesday, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateDateRange(t *testing.T) {
|
||||||
|
start := time.Date(2026, 2, 9, 0, 0, 0, 0, time.UTC)
|
||||||
|
end := time.Date(2026, 2, 11, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
dates := GenerateDateRange(start, end)
|
||||||
|
|
||||||
|
if len(dates) != 3 {
|
||||||
|
t.Fatalf("expected 3 dates, got %d", len(dates))
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []string{"2026-02-09", "2026-02-10", "2026-02-11"}
|
||||||
|
for i, d := range dates {
|
||||||
|
got := d.Format("2006-01-02")
|
||||||
|
if got != expected[i] {
|
||||||
|
t.Errorf("expected %s, got %s", expected[i], got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMondays_ReturnsOnlyMondays(t *testing.T) {
|
||||||
|
startDate := time.Date(2026, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||||
|
endDate := time.Date(2026, 01, 31, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
daysInMonth := GenerateDateRange(startDate, endDate)
|
||||||
|
result := GetMondays(daysInMonth, false)
|
||||||
|
if len(result) < 5 {
|
||||||
|
t.Errorf("Expected 5 monday, got %d", len(result))
|
||||||
|
} else if len(result) > 5 {
|
||||||
|
t.Errorf("Expected 5 monday, got %d", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[0] != time.Date(2025, 12, 29, 0, 0, 0, 0, time.UTC) {
|
||||||
|
t.Errorf("Expected first monday to be %v, got %v", "2025-12-29", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMondays_ReturnsOnlyMondaysInRange(t *testing.T) {
|
||||||
|
startDate := time.Date(2026, 01, 01, 0, 0, 0, 0, time.UTC)
|
||||||
|
endDate := time.Date(2026, 01, 31, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
daysInMonth := GenerateDateRange(startDate, endDate)
|
||||||
|
result := GetMondays(daysInMonth, true)
|
||||||
|
if len(result) < 4 {
|
||||||
|
t.Errorf("Expected 4 monday, got %d", len(result))
|
||||||
|
} else if len(result) > 4 {
|
||||||
|
t.Errorf("Expected 4 monday, got %d", len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[0] != time.Date(2026, 1, 5, 0, 0, 0, 0, time.UTC) {
|
||||||
|
t.Errorf("Expected first monday to be %v, got %v", "2026-01-05", result[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDaysInRange(t *testing.T) {
|
||||||
|
days := []time.Time{
|
||||||
|
time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC), // Tuesday
|
||||||
|
time.Date(2023, 4, 4, 0, 0, 0, 0, time.UTC), // Wednesday
|
||||||
|
time.Date(2023, 4, 5, 0, 0, 0, 0, time.UTC), // Thursday
|
||||||
|
time.Date(2023, 4, 6, 0, 0, 0, 0, time.UTC), // Friday
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Date(2023, 4, 3, 0, 0, 0, 0, time.UTC)
|
||||||
|
end := time.Date(2023, 4, 5, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
daysInRange := DaysInRange(days, start, end)
|
||||||
|
|
||||||
|
if len(daysInRange) != 3 {
|
||||||
|
t.Errorf("Expected 3 days in range, got %d", len(daysInRange))
|
||||||
|
}
|
||||||
|
|
||||||
|
if daysInRange[0] != days[0] {
|
||||||
|
t.Errorf("Expected first day in range to be %v, got %v", days[0], daysInRange[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if daysInRange[2] != days[2] {
|
||||||
|
t.Errorf("Expected third day in range to be %v, got %v", days[2], daysInRange[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFormatDurationFill(t *testing.T) {
|
func TestFormatDurationFill(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func SetCors(w http.ResponseWriter) {
|
|||||||
|
|
||||||
func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) {
|
func RequiresLogin(session *scs.SessionManager, w http.ResponseWriter, r *http.Request) {
|
||||||
r = r.WithContext(context.WithValue(r.Context(), "session", session))
|
r = r.WithContext(context.WithValue(r.Context(), "session", session))
|
||||||
if GetEnv("GO_ENV", "production") == "debug" {
|
if IsDebug() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if session.Exists(r.Context(), "user") {
|
if session.Exists(r.Context(), "user") {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("No .env file found in directory!")
|
slog.Info("No .env file found in directory!")
|
||||||
}
|
}
|
||||||
if helper.GetEnv("GO_ENV", "production") == "debug" {
|
if helper.IsDebug() {
|
||||||
logLevel.Set(slog.LevelDebug)
|
logLevel.Set(slog.LevelDebug)
|
||||||
envs := os.Environ()
|
envs := os.Environ()
|
||||||
slog.Debug("Debug mode enabled", "Environment Variables", envs)
|
slog.Debug("Debug mode enabled", "Environment Variables", envs)
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ package models
|
|||||||
// the absence data is based on the entries in the "abwesenheit" database table
|
// the absence data is based on the entries in the "abwesenheit" database table
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ func (a *Absence) GetWorktime(u User, base WorktimeBase, includeKurzarbeit bool)
|
|||||||
return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100)
|
return u.ArbeitszeitProTagFrac(float32(a.AbwesenheitTyp.WorkTime) / 100)
|
||||||
case WorktimeBaseWeek:
|
case WorktimeBaseWeek:
|
||||||
if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
|
if a.AbwesenheitTyp.WorkTime <= 0 && includeKurzarbeit {
|
||||||
return u.ArbeitszeitProTagFrac(0.2)
|
return u.ArbeitszeitProWocheFrac(0.2)
|
||||||
} else if a.AbwesenheitTyp.WorkTime <= 0 {
|
} else if a.AbwesenheitTyp.WorkTime <= 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -295,3 +297,24 @@ func (a *Absence) Delete() error {
|
|||||||
_, err = qStr.Exec(a.CounterId)
|
_, err = qStr.Exec(a.CounterId)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Absence) IsSubmittedAndAccepted() bool {
|
||||||
|
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(abwesenheiten) AND $2 >= woche_start AND $2 < woche_start + INTERVAL '1 week';`) // @> array contains
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Error when preparing SQL Statement", "error", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer qStr.Close()
|
||||||
|
var isSubmittedAndChecked bool = false
|
||||||
|
|
||||||
|
err = qStr.QueryRow(a.CounterId, a.Date()).Scan(&isSubmittedAndChecked)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
// No rows found ==> not even submitted
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Unexpected error when executing SQL Statement", "error", err)
|
||||||
|
}
|
||||||
|
return isSubmittedAndChecked
|
||||||
|
}
|
||||||
|
|||||||
@@ -95,27 +95,6 @@ func (b *Booking) Verify() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Booking) IsSubmittedAndChecked() bool {
|
|
||||||
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE $1 = ANY(anwesenheiten);`)
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("Error when preparing SQL Statement", "error", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer qStr.Close()
|
|
||||||
var isSubmittedAndChecked bool = false
|
|
||||||
|
|
||||||
err = qStr.QueryRow(b.CounterId).Scan(&isSubmittedAndChecked)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
// No rows found ==> not even submitted
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("Unexpected error when executing SQL Statement", "error", err)
|
|
||||||
}
|
|
||||||
return isSubmittedAndChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Booking) Insert() error {
|
func (b *Booking) Insert() error {
|
||||||
if !checkLastBooking(*b) {
|
if !checkLastBooking(*b) {
|
||||||
return SameBookingError{}
|
return SameBookingError{}
|
||||||
@@ -209,7 +188,7 @@ func (b Booking) Save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Booking) GetBookingType() string {
|
func (b *Booking) GetBookingType() string {
|
||||||
debug := (helper.GetEnv("GO_ENV", "production") == "debug")
|
debug := helper.IsDebug()
|
||||||
switch b.CheckInOut {
|
switch b.CheckInOut {
|
||||||
case 1: //manuelle Änderung
|
case 1: //manuelle Änderung
|
||||||
return "kommen"
|
return "kommen"
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ type CompoundDay struct {
|
|||||||
DayParts []IWorkDay
|
DayParts []IWorkDay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSubmittedAndAccepted implements IWorkDay.
|
||||||
|
func (c *CompoundDay) IsSubmittedAndAccepted() bool {
|
||||||
|
var isSubmittedAndAccepted = true
|
||||||
|
for _, day := range c.DayParts {
|
||||||
|
_isSubmittedAndAccepted := day.IsSubmittedAndAccepted()
|
||||||
|
isSubmittedAndAccepted = isSubmittedAndAccepted && _isSubmittedAndAccepted
|
||||||
|
slog.Info("Result from IsSubmittedCheck", "Result", _isSubmittedAndAccepted, "compount", day.ToString())
|
||||||
|
}
|
||||||
|
return isSubmittedAndAccepted
|
||||||
|
}
|
||||||
|
|
||||||
func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay {
|
func NewCompondDay(date time.Time, dayParts ...IWorkDay) *CompoundDay {
|
||||||
return &CompoundDay{Day: date, DayParts: dayParts}
|
return &CompoundDay{Day: date, DayParts: dayParts}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type IWorkDay interface {
|
|||||||
GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration)
|
GetTimes(User, WorktimeBase, bool) (work, pause, overtime time.Duration)
|
||||||
GetOvertime(User, WorktimeBase, bool) time.Duration
|
GetOvertime(User, WorktimeBase, bool) time.Duration
|
||||||
IsEmpty() bool
|
IsEmpty() bool
|
||||||
|
IsSubmittedAndAccepted() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type DayType int
|
type DayType int
|
||||||
@@ -54,7 +55,9 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, absentDay := range absences {
|
for _, absentDay := range absences {
|
||||||
|
if weekDay := absentDay.Date().Weekday(); weekDay == time.Saturday || weekDay == time.Sunday {
|
||||||
|
continue
|
||||||
|
}
|
||||||
// Check if there is already a day
|
// Check if there is already a day
|
||||||
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
|
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ type PublicHoliday struct {
|
|||||||
worktime int8
|
worktime int8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSubmittedAndAccepted implements IWorkDay.
|
||||||
|
func (p *PublicHoliday) IsSubmittedAndAccepted() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// IsEmpty implements [IWorkDay].
|
// IsEmpty implements [IWorkDay].
|
||||||
func (p *PublicHoliday) IsEmpty() bool {
|
func (p *PublicHoliday) IsEmpty() bool {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type User struct {
|
|||||||
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
|
func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User, error) {
|
||||||
var user User
|
var user User
|
||||||
var err error
|
var err error
|
||||||
if helper.GetEnv("GO_ENV", "production") == "debug" {
|
if helper.IsDebug() {
|
||||||
user, err = GetUserByPersonalNr(123)
|
user, err = GetUserByPersonalNr(123)
|
||||||
} else {
|
} else {
|
||||||
if !Session.Exists(ctx, "user") {
|
if !Session.Exists(ctx, "user") {
|
||||||
@@ -50,15 +50,15 @@ func GetUserFromSession(Session *scs.SessionManager, ctx context.Context) (User,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns the actual overtime for this moment
|
// Returns the actual overtime for this moment
|
||||||
func (u *User) GetReportedOvertime() (time.Duration, error) {
|
func (u *User) GetReportedOvertime(startDate time.Time) (time.Duration, error) {
|
||||||
var overtime time.Duration
|
var overtime time.Duration
|
||||||
|
|
||||||
qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1;")
|
qStr, err := DB.Prepare("SELECT COALESCE(SUM(EXTRACT(EPOCH FROM ueberstunden) * 1000000000)::BIGINT, 0) AS total_ueberstunden_ns FROM wochen_report WHERE personal_nummer = $1 AND woche_start::DATE <= $2::DATE;")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer qStr.Close()
|
defer qStr.Close()
|
||||||
err = qStr.QueryRow(u.PersonalNummer).Scan(&overtime)
|
err = qStr.QueryRow(u.PersonalNummer, startDate).Scan(&overtime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -292,10 +292,42 @@ func (u *User) GetNextWeek() WorkWeek {
|
|||||||
func (u *User) GetLastWorkWeekSubmission() time.Time {
|
func (u *User) GetLastWorkWeekSubmission() time.Time {
|
||||||
var lastSub time.Time
|
var lastSub time.Time
|
||||||
qStr, err := DB.Prepare(`
|
qStr, err := DB.Prepare(`
|
||||||
SELECT COALESCE(
|
SELECT new_week
|
||||||
(SELECT woche_start + INTERVAL '1 week' FROM wochen_report WHERE personal_nummer = $1 ORDER BY woche_start DESC LIMIT 1),
|
FROM (
|
||||||
(SELECT timestamp FROM anwesenheit WHERE card_uid = $2 ORDER BY timestamp LIMIT 1)
|
-- Highest priority
|
||||||
) AS letzte_buchung;
|
SELECT
|
||||||
|
woche_start AS new_week,
|
||||||
|
1 AS priority
|
||||||
|
FROM wochen_report
|
||||||
|
WHERE personal_nummer = $1
|
||||||
|
AND bestaetigt IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- Fallback if #1 returns nothing
|
||||||
|
SELECT
|
||||||
|
woche_start + INTERVAL '1 week' AS new_week,
|
||||||
|
2 AS priority
|
||||||
|
FROM wochen_report wo
|
||||||
|
WHERE personal_nummer = $1
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM wochen_report wi
|
||||||
|
WHERE wi.woche_start = wo.woche_start + INTERVAL '1 week'
|
||||||
|
AND wi.personal_nummer = wo.personal_nummer
|
||||||
|
)
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- Final fallback
|
||||||
|
SELECT
|
||||||
|
timestamp AS new_week,
|
||||||
|
3 AS priority
|
||||||
|
FROM anwesenheit
|
||||||
|
WHERE card_uid = $2
|
||||||
|
) t
|
||||||
|
ORDER BY priority, new_week
|
||||||
|
LIMIT 1;
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("Error preparing query statement.", "error", err)
|
slog.Debug("Error preparing query statement.", "error", err)
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"arbeitszeitmessung/helper"
|
"arbeitszeitmessung/helper"
|
||||||
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WorkDay struct {
|
type WorkDay struct {
|
||||||
@@ -148,12 +151,21 @@ func (d *WorkDay) Type() DayType {
|
|||||||
return DayTypeWorkday
|
return DayTypeWorkday
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *WorkDay) GenerateKurzArbeitBookings(u User) (time.Time, time.Time) {
|
func (d *WorkDay) GenerateKurzArbeitBookings(u User, weekBase WorktimeBase) (time.Time, time.Time) {
|
||||||
var timeFrom, timeTo time.Time
|
var timeFrom, timeTo time.Time
|
||||||
if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
|
if d.GetWorktime(u, WorktimeBaseDay, false) >= u.ArbeitszeitProTag() {
|
||||||
return timeFrom, timeTo
|
return timeFrom, timeTo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.IsEmpty() {
|
||||||
|
switch weekBase {
|
||||||
|
case WorktimeBaseDay:
|
||||||
|
return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProTag())
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
return d.Day.Add(time.Hour * 8), d.Day.Add(time.Hour * 8).Add(u.ArbeitszeitProWocheFrac(0.2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute)
|
timeFrom = d.Bookings[len(d.Bookings)-1].Timestamp.Add(time.Minute)
|
||||||
timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false))
|
timeTo = timeFrom.Add(u.ArbeitszeitProTag() - d.GetWorktime(u, WorktimeBaseDay, false))
|
||||||
slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String())
|
slog.Debug("Added duration as Kurzarbeit", "date", d.Date().String(), "duration", timeTo.Sub(timeFrom).String())
|
||||||
@@ -166,7 +178,7 @@ func (d *WorkDay) GetKurzArbeit() *Absence {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *WorkDay) ToString() string {
|
func (d *WorkDay) ToString() string {
|
||||||
return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime))
|
return fmt.Sprintf("WorkDay: %s with %d bookings and worktime: %s. Is KurzArbeit %v", d.Date().Format(time.DateOnly), len(d.Bookings), helper.FormatDuration(d.workTime), d.IsKurzArbeit())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *WorkDay) IsWorkDay() bool {
|
func (d *WorkDay) IsWorkDay() bool {
|
||||||
@@ -425,3 +437,38 @@ func (d *WorkDay) GetDayProgress(u User) int8 {
|
|||||||
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
|
progress := (workTime.Seconds() / u.ArbeitszeitProTag().Seconds()) * 100
|
||||||
return int8(progress)
|
return int8(progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *WorkDay) IsSubmittedAndAccepted() bool {
|
||||||
|
var isKurzArbeitAccepted bool
|
||||||
|
if d.IsKurzArbeit() {
|
||||||
|
isKurzArbeitAccepted = d.kurzArbeitAbsence.IsSubmittedAndAccepted()
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.IsEmpty() {
|
||||||
|
return isKurzArbeitAccepted
|
||||||
|
}
|
||||||
|
|
||||||
|
qStr, err := DB.Prepare(`SELECT bestaetigt from wochen_report WHERE anwesenheiten @> $1 AND $2 >= woche_start AND $2 < woche_start + INTERVAL '1 week';`) // @> array contains
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Error when preparing SQL Statement", "error", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
defer qStr.Close()
|
||||||
|
var isSubmittedAndChecked bool = false
|
||||||
|
|
||||||
|
var bookingsIds []int
|
||||||
|
for _, booking := range d.Bookings {
|
||||||
|
bookingsIds = append(bookingsIds, booking.CounterId)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = qStr.QueryRow(pq.Array(bookingsIds), d.Date()).Scan(&isSubmittedAndChecked)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Unexpected error when executing SQL Statement", "error", err, "BookingsIds", bookingsIds)
|
||||||
|
}
|
||||||
|
return isSubmittedAndChecked
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package models
|
|||||||
// this type is based on the "wochen_report" table
|
// this type is based on the "wochen_report" table
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"arbeitszeitmessung/helper"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
@@ -24,25 +25,34 @@ type WorkWeek struct {
|
|||||||
Days []IWorkDay
|
Days []IWorkDay
|
||||||
User User
|
User User
|
||||||
WeekStart time.Time
|
WeekStart time.Time
|
||||||
|
weekEnd time.Time
|
||||||
Worktime time.Duration
|
Worktime time.Duration
|
||||||
WorktimeVirtual time.Duration
|
WorktimeVirtual time.Duration
|
||||||
Overtime time.Duration
|
Overtime time.Duration
|
||||||
Status WeekStatus
|
Status WeekStatus
|
||||||
|
WeekBase WorktimeBase
|
||||||
|
Kurzarbeit time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type WeekStatus int8
|
type WeekStatus int8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WeekStatusNone WeekStatus = iota
|
WeekStatusNone WeekStatus = iota
|
||||||
|
WeekStatusCorrected
|
||||||
WeekStatusSent
|
WeekStatusSent
|
||||||
WeekStatusAccepted
|
WeekStatusAccepted
|
||||||
WeekStatusDifferences
|
WeekStatusDifferences
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek {
|
func NewWorkWeekSimple(user User, tsMonday time.Time, populate bool) WorkWeek {
|
||||||
|
return NewWorkWeek(user, tsMonday, tsMonday.Add(6*24*time.Hour), populate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorkWeek(user User, tsStart, tsEnd time.Time, populate bool) WorkWeek {
|
||||||
var week WorkWeek = WorkWeek{
|
var week WorkWeek = WorkWeek{
|
||||||
User: user,
|
User: user,
|
||||||
WeekStart: tsMonday,
|
WeekStart: tsStart,
|
||||||
|
weekEnd: tsEnd,
|
||||||
Status: WeekStatusNone,
|
Status: WeekStatusNone,
|
||||||
}
|
}
|
||||||
if populate {
|
if populate {
|
||||||
@@ -52,13 +62,25 @@ func NewWorkWeek(user User, tsMonday time.Time, populate bool) WorkWeek {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) {
|
func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Duration) {
|
||||||
slog.Debug("Populating Workweek for user", "user", w.User)
|
|
||||||
slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String()))
|
slog.Debug("Got Days with overtime and worktime", slog.String("worktime", worktime.String()), slog.String("overtime", overtime.String()))
|
||||||
w.Days = GetDays(w.User, w.WeekStart, w.WeekStart.Add(6*24*time.Hour), false)
|
w.Days = GetDays(w.User, w.WeekStart, w.weekEnd, false)
|
||||||
|
slog.Debug("Populating Workweek for user", "user", w.User.Name, "Days", lenWorkDays(w.Days), "Start", w.WeekStart, "End", w.weekEnd, "workdays", helper.GetWorkingDays(w.WeekStart, w.weekEnd))
|
||||||
|
|
||||||
|
if lenWorkDays(w.Days) == helper.GetWorkingDays(w.WeekStart, w.weekEnd) {
|
||||||
|
w.WeekBase = WorktimeBaseWeek
|
||||||
|
} else {
|
||||||
|
w.WeekBase = WorktimeBaseDay
|
||||||
|
}
|
||||||
|
|
||||||
for _, day := range w.Days {
|
for _, day := range w.Days {
|
||||||
w.Worktime += day.GetWorktime(w.User, WorktimeBaseDay, false)
|
dWorkTime := day.GetWorktime(w.User, w.WeekBase, false)
|
||||||
w.WorktimeVirtual += day.GetWorktime(w.User, WorktimeBaseDay, true)
|
dWorkTimeVirtual := day.GetWorktime(w.User, w.WeekBase, true)
|
||||||
|
if dWorkTime < dWorkTimeVirtual {
|
||||||
|
w.Kurzarbeit += dWorkTimeVirtual - dWorkTime
|
||||||
|
}
|
||||||
|
w.Worktime += dWorkTime
|
||||||
|
w.WorktimeVirtual += dWorkTimeVirtual
|
||||||
|
slog.Debug("Calculated Worktime", "Day", day.ToString(), "worktime", w.Worktime.String())
|
||||||
}
|
}
|
||||||
slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String())
|
slog.Debug("Got worktime for user", "worktime", w.Worktime.String(), "virtualWorkTime", w.WorktimeVirtual.String())
|
||||||
|
|
||||||
@@ -78,6 +100,16 @@ func (w *WorkWeek) PopulateWithDays(worktime time.Duration, overtime time.Durati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lenWorkDays(workDays []IWorkDay) int {
|
||||||
|
var lenght int
|
||||||
|
for _, day := range workDays {
|
||||||
|
if !day.IsEmpty() || day.IsKurzArbeit() {
|
||||||
|
lenght += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lenght
|
||||||
|
}
|
||||||
|
|
||||||
func (w *WorkWeek) CheckStatus() WeekStatus {
|
func (w *WorkWeek) CheckStatus() WeekStatus {
|
||||||
if w.Status != WeekStatusNone {
|
if w.Status != WeekStatusNone {
|
||||||
return w.Status
|
return w.Status
|
||||||
@@ -86,25 +118,31 @@ func (w *WorkWeek) CheckStatus() WeekStatus {
|
|||||||
log.Println("Cannot access Database!")
|
log.Println("Cannot access Database!")
|
||||||
return w.Status
|
return w.Status
|
||||||
}
|
}
|
||||||
qStr, err := DB.Prepare(`SELECT bestaetigt FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`)
|
qStr, err := DB.Prepare(`SELECT bestaetigt, id FROM wochen_report WHERE woche_start = $1::DATE AND personal_nummer = $2;`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error preparing SQL statement", err)
|
log.Println("Error preparing SQL statement", err)
|
||||||
return w.Status
|
return w.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
defer qStr.Close()
|
defer qStr.Close()
|
||||||
var beastatigt bool
|
var beastatigt sql.NullBool
|
||||||
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt)
|
err = qStr.QueryRow(w.WeekStart, w.User.PersonalNummer).Scan(&beastatigt, &w.Id)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return w.Status
|
return w.Status
|
||||||
}
|
}
|
||||||
|
slog.Info("Bestätigt query res", "Best", beastatigt, "week", w.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error querying database", err)
|
log.Println("Error querying database", err)
|
||||||
return w.Status
|
return w.Status
|
||||||
}
|
}
|
||||||
if beastatigt {
|
switch {
|
||||||
|
case beastatigt.Bool:
|
||||||
w.Status = WeekStatusAccepted
|
w.Status = WeekStatusAccepted
|
||||||
} else {
|
case beastatigt.Valid:
|
||||||
w.Status = WeekStatusSent
|
w.Status = WeekStatusSent
|
||||||
|
default:
|
||||||
|
w.Status = WeekStatusCorrected
|
||||||
|
|
||||||
}
|
}
|
||||||
return w.Status
|
return w.Status
|
||||||
}
|
}
|
||||||
@@ -206,23 +244,33 @@ func (w *WorkWeek) SendWeek() error {
|
|||||||
return ErrRunningWeek
|
return ErrRunningWeek
|
||||||
}
|
}
|
||||||
|
|
||||||
if w.CheckStatus() != WeekStatusNone {
|
switch w.CheckStatus() {
|
||||||
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`)
|
case WeekStatusNone:
|
||||||
if err != nil {
|
|
||||||
slog.Warn("Error preparing SQL statement", "error", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden, anwesenheiten, abwesenheiten) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000), $5, $6);`)
|
qStr, err = DB.Prepare(`INSERT INTO wochen_report (personal_nummer, woche_start, arbeitszeit, ueberstunden, anwesenheiten, abwesenheiten) VALUES ($1, $2, make_interval(secs => $3::numeric / 1000000000), make_interval(secs => $4::numeric / 1000000000), $5, $6);`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Error preparing SQL statement", "error", err)
|
slog.Warn("Error preparing SQL statement", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case WeekStatusCorrected:
|
||||||
|
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = FALSE, arbeitszeit = make_interval(secs => $3::numeric / 1000000000), ueberstunden = make_interval(secs => $4::numeric / 1000000000), anwesenheiten=$5, abwesenheiten=$6 WHERE personal_nummer = $1 AND woche_start = $2;`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Error preparing SQL statement", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case WeekStatusSent, WeekStatusAccepted:
|
||||||
|
qStr, err = DB.Prepare(`UPDATE "wochen_report" SET bestaetigt = null WHERE personal_nummer = $1 AND woche_start = $2 AND ($3::numeric IS NULL OR TRUE) AND ($4::numeric IS NULL OR TRUE) AND ($5::int[] IS NULL OR TRUE) AND ($6::int[] IS NULL OR TRUE);`)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Error preparing SQL statement", "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings))
|
_, err = qStr.Exec(w.User.PersonalNummer, w.WeekStart, int64(w.Worktime), int64(w.Overtime), pq.Array(anwBookings), pq.Array(awBookings))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error executing query!", err)
|
slog.Error("Error executing query!", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func TestNewWorkWeekNoPopulate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
workWeek := models.NewWorkWeek(testUser, monday, false)
|
workWeek := models.NewWorkWeekSimple(testUser, monday, false)
|
||||||
|
|
||||||
if workWeek.User != testUser || workWeek.WeekStart != monday {
|
if workWeek.User != testUser || workWeek.WeekStart != monday {
|
||||||
t.Error("No populate workweek does not have right values!")
|
t.Error("No populate workweek does not have right values!")
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
--color-neutral-300: oklch(87% 0 0);
|
--color-neutral-300: oklch(87% 0 0);
|
||||||
--color-neutral-400: oklch(70.8% 0 0);
|
--color-neutral-400: oklch(70.8% 0 0);
|
||||||
--color-neutral-500: oklch(55.6% 0 0);
|
--color-neutral-500: oklch(55.6% 0 0);
|
||||||
--color-neutral-600: oklch(43.9% 0 0);
|
|
||||||
--color-neutral-700: oklch(37.1% 0 0);
|
--color-neutral-700: oklch(37.1% 0 0);
|
||||||
--color-neutral-800: oklch(26.9% 0 0);
|
--color-neutral-800: oklch(26.9% 0 0);
|
||||||
--color-black: #000;
|
--color-black: #000;
|
||||||
@@ -30,8 +29,6 @@
|
|||||||
--text-sm--line-height: calc(1.25 / 0.875);
|
--text-sm--line-height: calc(1.25 / 0.875);
|
||||||
--text-xl: 1.25rem;
|
--text-xl: 1.25rem;
|
||||||
--text-xl--line-height: calc(1.75 / 1.25);
|
--text-xl--line-height: calc(1.75 / 1.25);
|
||||||
--text-2xl: 1.5rem;
|
|
||||||
--text-2xl--line-height: calc(2 / 1.5);
|
|
||||||
--font-weight-bold: 700;
|
--font-weight-bold: 700;
|
||||||
--radius-md: 0.375rem;
|
--radius-md: 0.375rem;
|
||||||
--default-transition-duration: 150ms;
|
--default-transition-duration: 150ms;
|
||||||
@@ -205,15 +202,9 @@
|
|||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.top-1 {
|
|
||||||
top: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1/2 * 100%);
|
top: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
.top-2 {
|
|
||||||
top: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.top-2\.5 {
|
.top-2\.5 {
|
||||||
top: calc(var(--spacing) * 2.5);
|
top: calc(var(--spacing) * 2.5);
|
||||||
}
|
}
|
||||||
@@ -223,15 +214,9 @@
|
|||||||
.right-1 {
|
.right-1 {
|
||||||
right: calc(var(--spacing) * 1);
|
right: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
.right-2 {
|
|
||||||
right: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.right-2\.5 {
|
.right-2\.5 {
|
||||||
right: calc(var(--spacing) * 2.5);
|
right: calc(var(--spacing) * 2.5);
|
||||||
}
|
}
|
||||||
.left-1 {
|
|
||||||
left: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.left-1\/2 {
|
.left-1\/2 {
|
||||||
left: calc(1/2 * 100%);
|
left: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
@@ -253,6 +238,9 @@
|
|||||||
.-my-1 {
|
.-my-1 {
|
||||||
margin-block: calc(var(--spacing) * -1);
|
margin-block: calc(var(--spacing) * -1);
|
||||||
}
|
}
|
||||||
|
.my-2 {
|
||||||
|
margin-block: calc(var(--spacing) * 2);
|
||||||
|
}
|
||||||
.mt-1 {
|
.mt-1 {
|
||||||
margin-top: calc(var(--spacing) * 1);
|
margin-top: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -320,6 +308,32 @@
|
|||||||
mask-size: 100% 100%;
|
mask-size: 100% 100%;
|
||||||
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");
|
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M7.616 20q-.672 0-1.144-.472T6 18.385V6H5V5h4v-.77h6V5h4v1h-1v12.385q0 .69-.462 1.153T16.384 20zM17 6H7v12.385q0 .269.173.442t.443.173h8.769q.23 0 .423-.192t.192-.424zM9.808 17h1V8h-1zm3.384 0h1V8h-1zM7 6v13z'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
.icon-\[material-symbols-light--edit-calendar-rounded\] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
background-color: currentColor;
|
||||||
|
-webkit-mask-image: var(--svg);
|
||||||
|
mask-image: var(--svg);
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-size: 100% 100%;
|
||||||
|
mask-size: 100% 100%;
|
||||||
|
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M5.616 21q-.691 0-1.153-.462T4 19.385V6.615q0-.69.463-1.152T5.616 5h1.769V3.308q0-.23.155-.384q.156-.155.386-.155t.383.155t.153.384V5h7.154V3.27q0-.213.143-.357q.144-.144.357-.144t.356.144t.144.356V5h1.769q.69 0 1.153.463T20 6.616v4.601q0 .213-.144.356t-.357.144t-.356-.144t-.143-.356v-.602H5v8.77q0 .23.192.423t.423.192h5.731q.213 0 .357.144t.143.357t-.143.356t-.357.143zm8.615-.808V19.12q0-.153.056-.296q.055-.144.186-.275l5.09-5.065q.149-.13.306-.19t.315-.062q.172 0 .338.064q.166.065.301.194l.925.944q.123.148.188.308q.064.159.064.319t-.052.322t-.2.31l-5.065 5.066q-.131.13-.275.186q-.143.056-.297.056h-1.073q-.343 0-.575-.232t-.232-.576m5.96-4.177l.925-.956l-.925-.944l-.95.95z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
.icon-\[material-symbols-light--lock\] {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
background-color: currentColor;
|
||||||
|
-webkit-mask-image: var(--svg);
|
||||||
|
mask-image: var(--svg);
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-size: 100% 100%;
|
||||||
|
mask-size: 100% 100%;
|
||||||
|
--svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M6.616 21q-.667 0-1.141-.475T5 19.386v-8.77q0-.666.475-1.14T6.615 9H8V7q0-1.671 1.165-2.835Q10.329 3 12 3t2.836 1.165T16 7v2h1.385q.666 0 1.14.475t.475 1.14v8.77q0 .666-.475 1.14t-1.14.475zM12 16.5q.633 0 1.066-.434q.434-.433.434-1.066t-.434-1.066T12 13.5t-1.066.434Q10.5 14.367 10.5 15t.434 1.066q.433.434 1.066.434M9 9h6V7q0-1.25-.875-2.125T12 4t-2.125.875T9 7z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
.icon-\[material-symbols-light--more-time\] {
|
.icon-\[material-symbols-light--more-time\] {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.25em;
|
width: 1.25em;
|
||||||
@@ -395,12 +409,13 @@
|
|||||||
width: calc(var(--spacing) * 5);
|
width: calc(var(--spacing) * 5);
|
||||||
height: calc(var(--spacing) * 5);
|
height: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
|
.size-6 {
|
||||||
|
width: calc(var(--spacing) * 6);
|
||||||
|
height: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
.h-2 {
|
.h-2 {
|
||||||
height: calc(var(--spacing) * 2);
|
height: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.h-3 {
|
|
||||||
height: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.h-3\.5 {
|
.h-3\.5 {
|
||||||
height: calc(var(--spacing) * 3.5);
|
height: calc(var(--spacing) * 3.5);
|
||||||
}
|
}
|
||||||
@@ -425,9 +440,6 @@
|
|||||||
.w-2 {
|
.w-2 {
|
||||||
width: calc(var(--spacing) * 2);
|
width: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.w-3 {
|
|
||||||
width: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.w-3\.5 {
|
.w-3\.5 {
|
||||||
width: calc(var(--spacing) * 3.5);
|
width: calc(var(--spacing) * 3.5);
|
||||||
}
|
}
|
||||||
@@ -437,9 +449,6 @@
|
|||||||
.w-5 {
|
.w-5 {
|
||||||
width: calc(var(--spacing) * 5);
|
width: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
.w-9 {
|
|
||||||
width: calc(var(--spacing) * 9);
|
|
||||||
}
|
|
||||||
.w-9\/10 {
|
.w-9\/10 {
|
||||||
width: calc(9/10 * 100%);
|
width: calc(9/10 * 100%);
|
||||||
}
|
}
|
||||||
@@ -452,9 +461,6 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.flex-shrink {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
.flex-shrink-0 {
|
.flex-shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -470,21 +476,10 @@
|
|||||||
.basis-\[content\] {
|
.basis-\[content\] {
|
||||||
flex-basis: content;
|
flex-basis: content;
|
||||||
}
|
}
|
||||||
.border-collapse {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.-translate-x-1 {
|
|
||||||
--tw-translate-x: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-x-1\/2 {
|
.-translate-x-1\/2 {
|
||||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
}
|
}
|
||||||
.-translate-y-1 {
|
|
||||||
--tw-translate-y: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-y-1\/2 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -495,9 +490,6 @@
|
|||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.resize {
|
|
||||||
resize: both;
|
|
||||||
}
|
|
||||||
.scroll-m-2 {
|
.scroll-m-2 {
|
||||||
scroll-margin: calc(var(--spacing) * 2);
|
scroll-margin: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -624,9 +616,6 @@
|
|||||||
.bg-red-600 {
|
.bg-red-600 {
|
||||||
background-color: var(--color-red-600);
|
background-color: var(--color-red-600);
|
||||||
}
|
}
|
||||||
.mask-repeat {
|
|
||||||
mask-repeat: repeat;
|
|
||||||
}
|
|
||||||
.p-1 {
|
.p-1 {
|
||||||
padding: calc(var(--spacing) * 1);
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -660,9 +649,6 @@
|
|||||||
.whitespace-nowrap {
|
.whitespace-nowrap {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.\!text-red-500 {
|
|
||||||
color: var(--color-red-500) !important;
|
|
||||||
}
|
|
||||||
.text-accent {
|
.text-accent {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
@@ -696,16 +682,9 @@
|
|||||||
.uppercase {
|
.uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.underline {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
opacity: 0%;
|
opacity: 0%;
|
||||||
}
|
}
|
||||||
.outline {
|
|
||||||
outline-style: var(--tw-outline-style);
|
|
||||||
outline-width: 1px;
|
|
||||||
}
|
|
||||||
.filter {
|
.filter {
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
}
|
}
|
||||||
@@ -1139,11 +1118,6 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
@property --tw-outline-style {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: solid;
|
|
||||||
}
|
|
||||||
@property --tw-blur {
|
@property --tw-blur {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -1216,7 +1190,6 @@
|
|||||||
--tw-border-style: solid;
|
--tw-border-style: solid;
|
||||||
--tw-divide-y-reverse: 0;
|
--tw-divide-y-reverse: 0;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
--tw-outline-style: solid;
|
|
||||||
--tw-blur: initial;
|
--tw-blur: initial;
|
||||||
--tw-brightness: initial;
|
--tw-brightness: initial;
|
||||||
--tw-contrast: initial;
|
--tw-contrast: initial;
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
#let table-header(..headers) = {
|
|
||||||
table.header(
|
|
||||||
..headers.pos().map(h => strong(h))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#let abrechnung(meta, days) = {
|
|
||||||
set page(paper: "a4", margin: (x:1.5cm, y:2.25cm),
|
|
||||||
footer:[#grid(
|
|
||||||
columns: (3fr, .65fr),
|
|
||||||
align: left + horizon,
|
|
||||||
inset: .5em,
|
|
||||||
[#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("static/logo.png")],
|
|
||||||
[Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp],
|
|
||||||
)
|
|
||||||
])
|
|
||||||
set text(font: "Noto Sans", size:10pt, fill: luma(10%))
|
|
||||||
set table(
|
|
||||||
stroke: 0.5pt + luma(10%),
|
|
||||||
inset: .5em,
|
|
||||||
align: center + horizon,
|
|
||||||
)
|
|
||||||
show text: it => {
|
|
||||||
if it.text == "0min"{
|
|
||||||
text(oklch(70.8%, 0, 0deg))[#it]
|
|
||||||
}else if it.text.starts-with("-"){
|
|
||||||
text(red)[#it]
|
|
||||||
}else{
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[= Abrechnung Arbeitszeit -- #meta.EmployeeName]
|
|
||||||
|
|
||||||
[Zeitraum: #meta.TimeRange]
|
|
||||||
|
|
||||||
table(
|
|
||||||
columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, 1.25fr),
|
|
||||||
fill: (x, y) =>
|
|
||||||
if y == 0 { oklch(87%, 0, 0deg) },
|
|
||||||
table-header(
|
|
||||||
[Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Pause], [Überstunden]
|
|
||||||
),
|
|
||||||
.. for day in days {
|
|
||||||
(
|
|
||||||
[#day.Date],
|
|
||||||
if day.DayParts.len() == 0{
|
|
||||||
table.cell(colspan: 3)[Keine Buchungen]
|
|
||||||
}else if not day.DayParts.first().IsWorkDay{
|
|
||||||
table.cell(colspan: 3)[#day.DayParts.first().WorkType]
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
table.cell(colspan: 3, inset: 0em)[
|
|
||||||
|
|
||||||
#table(
|
|
||||||
columns: (1fr, 1fr, 1fr),
|
|
||||||
.. for Zeit in day.DayParts {
|
|
||||||
(
|
|
||||||
[#Zeit.BookingFrom],
|
|
||||||
[#Zeit.BookingTo],
|
|
||||||
[#Zeit.WorkType],
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[#day.Worktime],
|
|
||||||
[#day.Pausetime],
|
|
||||||
[#day.Overtime],
|
|
||||||
)
|
|
||||||
if day.IsFriday {
|
|
||||||
( table.cell(colspan: 7, fill: oklch(87%, 0, 0deg))[Wochenende], ) // note the trailing comma
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
table(
|
|
||||||
columns: (3fr, 1fr),
|
|
||||||
align: right,
|
|
||||||
inset: (x: .25em, y:.75em),
|
|
||||||
stroke: none,
|
|
||||||
table.hline(start: 0, end: 2, stroke: stroke(dash:"dashed", thickness:.5pt)),
|
|
||||||
[Arbeitszeit :], table.cell(align: left)[#meta.WorkTime],
|
|
||||||
[Überstunden :], table.cell(align: left)[#meta.Overtime],
|
|
||||||
[Überstunden :],table.cell(align: left)[#meta.OvertimeTotal],
|
|
||||||
table.hline(start: 0, end: 2),
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -47,6 +47,12 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
|
|||||||
<div class="grid grid-cols-5 gap-2 lg:grid-cols-1">
|
<div class="grid grid-cols-5 gap-2 lg:grid-cols-1">
|
||||||
if !onlyAccept {
|
if !onlyAccept {
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
|
if week.CheckStatus() == models.WeekStatusCorrected {
|
||||||
|
<span class="flex flex-row gap-2 items-center">
|
||||||
|
<div class="icon-[material-symbols-light--edit-calendar-rounded]"></div>
|
||||||
|
laufende Korrektur
|
||||||
|
</span>
|
||||||
|
}
|
||||||
<span class="flex flex-row gap-2 items-center">
|
<span class="flex flex-row gap-2 items-center">
|
||||||
@statusCheckMark(week.CheckStatus(), models.WeekStatusSent)
|
@statusCheckMark(week.CheckStatus(), models.WeekStatusSent)
|
||||||
Gesendet
|
Gesendet
|
||||||
@@ -60,7 +66,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
|
|||||||
<div class="flex flex-row gap-2 col-span-3">
|
<div class="flex flex-row gap-2 col-span-3">
|
||||||
@timeGaugeComponent(int8(progress), false)
|
@timeGaugeComponent(int8(progress), false)
|
||||||
<div>
|
<div>
|
||||||
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDuration(week.Worktime)) }</p>
|
<p>Arbeitszeit: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Worktime, true)) }</p>
|
||||||
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p>
|
<p>Überstunden: { fmt.Sprintf("%s", helper.FormatDurationFill(week.Overtime, true)) }</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
templ changeButtonComponent(id string, workDay bool) {
|
templ changeButtonComponent(id string, workDay bool, disabled bool) {
|
||||||
|
if disabled {
|
||||||
|
<button class="h-10 change-button-component btn w-auto group/button" type="button" disabled>
|
||||||
|
<p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
|
||||||
|
</button>
|
||||||
|
} else {
|
||||||
<button class="h-10 change-button-component btn w-auto group/button" type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay) }>
|
<button class="h-10 change-button-component btn w-auto group/button" type="button" onclick={ templ.JSFuncCall("editWorkday", templ.JSExpression("this"), templ.JSExpression("event"), id, workDay) }>
|
||||||
<p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
|
<p class="hidden md:block group-[.edit]/button:hidden">Ändern</p>
|
||||||
<p class="hidden group-[.edit]/button:md:block">Speichern</p>
|
<p class="hidden group-[.edit]/button:md:block">Speichern</p>
|
||||||
@@ -17,6 +22,7 @@ templ changeButtonComponent(id string, workDay bool) {
|
|||||||
</button>
|
</button>
|
||||||
<button class="h-10 hidden group-[.edit]:flex btn basis-[content] items-center" onclick={ templ.JSFuncCall("clearEditState") }><span class="size-5 icon-[material-symbols-light--cancel-outline]"></span></button>
|
<button class="h-10 hidden group-[.edit]:flex btn basis-[content] items-center" onclick={ templ.JSFuncCall("clearEditState") }><span class="size-5 icon-[material-symbols-light--cancel-outline]"></span></button>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
templ newAbsenceComponent() {
|
templ newAbsenceComponent() {
|
||||||
<div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center ">
|
<div class="no-booking-component hidden group-[.edit]:flex flex-col gap-2 align-center ">
|
||||||
@@ -85,9 +91,6 @@ templ bookingComponent(booking models.Booking) {
|
|||||||
fehlerhafte Buchung, wird nicht zur Berechnung verwendet!
|
fehlerhafte Buchung, wird nicht zur Berechnung verwendet!
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
if booking.IsSubmittedAndChecked() {
|
|
||||||
<p>submitted</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,7 +143,10 @@ templ defaultDayComponent(day models.IWorkDay) {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-cell flex flex-row gap-2 items-end ">
|
<div class="grid-cell flex flex-row gap-2 items-end ">
|
||||||
@changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true)
|
@changeButtonComponent("time-"+day.Date().Format(time.DateOnly), true, day.IsSubmittedAndAccepted())
|
||||||
|
if day.IsSubmittedAndAccepted() {
|
||||||
|
<span class="size-6 my-2 icon-[material-symbols-light--lock]"></span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# cron-timing: 05 01 * * 1
|
# cron-timing: 05 01 * * 1
|
||||||
container_name="arbeitszeitmessung-main-db-1"
|
container_name="arbeitszeitmessung-main-db-1"
|
||||||
filename=backup-$(date '+%d%m%Y').sql
|
filename=backup-$(date '+%d%m%Y').sql
|
||||||
|
backup_folder=__BACKUP_FOLDER__
|
||||||
database_name=__DATABASE__
|
database_name=__DATABASE__
|
||||||
docker exec $container_name pg_dump $database_name > /home/pi/arbeitszeitmessung-backup/$filename
|
docker exec $container_name pg_dump $database_name > $backup_folder/$filename
|
||||||
echo "created backup file: "$filename
|
echo "created backup file: "$filename
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e # Exit on error
|
|
||||||
|
|
||||||
echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD';
|
|
||||||
GRANT USAGE, CREATE ON SCHEMA public TO migrate;
|
|
||||||
GRANT CONNECT ON DATABASE arbeitszeitmessung TO migrate;
|
|
||||||
EOSQL
|
|
||||||
|
|
||||||
# psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
|
|
||||||
# GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER;
|
|
||||||
# GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER;
|
|
||||||
# GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage TO $POSTGRES_API_USER;
|
|
||||||
# GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER;
|
|
||||||
# EOSQL
|
|
||||||
|
|
||||||
echo "User creation and permissions setup complete!"
|
|
||||||
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
|
|
||||||
-- privilege roles
|
|
||||||
DO \$\$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_base') THEN
|
|
||||||
CREATE ROLE app_base NOLOGIN;
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
\$\$;
|
|
||||||
|
|
||||||
-- dynamic login role
|
|
||||||
DO \$\$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_API_USER') THEN
|
|
||||||
CREATE ROLE $POSTGRES_API_USER
|
|
||||||
LOGIN
|
|
||||||
ENCRYPTED PASSWORD '$POSTGRES_API_PASS';
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
\$\$;
|
|
||||||
|
|
||||||
-- grant base privileges
|
|
||||||
GRANT app_base TO $POSTGRES_API_USER;
|
|
||||||
GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER;
|
|
||||||
GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER;
|
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
|
|
||||||
EOSQL
|
|
||||||
|
|
||||||
# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung
|
|
||||||
@@ -4,9 +4,10 @@ POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (f
|
|||||||
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
|
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
|
||||||
POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
|
POSTGRES_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
|
||||||
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
|
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
|
||||||
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
|
POSTGRES_PORT=5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
|
||||||
TZ=Europe/Berlin # Zeitzone
|
TZ=Europe/Berlin # Zeitzone
|
||||||
API_TOKEN=dont_access # API Token für ESP Endpoints
|
API_TOKEN=dont_access # API Token für ESP Endpoints
|
||||||
WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
|
WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
|
||||||
LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
|
LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
|
||||||
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen
|
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen
|
||||||
|
BACKUP_FOLDER=__ROOT__/backup # Pfad für DB Backup Datein
|
||||||
|
|||||||
147
Readme.md
147
Readme.md
@@ -2,124 +2,53 @@
|
|||||||
|
|
||||||
[](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung)
|
[](https://sonar.letsstein.de/dashboard?id=arbeitszeitmessung)
|
||||||
|
|
||||||
bis jetzt ein einfaches Backend mit PostgreSQL Datenbank und GO Webserver um Arbeitszeitbuchungen per HTTP PUT einzufügen
|
---
|
||||||
|
|
||||||
|
Eine open-source Software zur Arbeitszeitmessung
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- manuelle Korrektur von einzelnen Buchungen
|
||||||
|
- Buchung von benutzerdefinierten Abwesenheiten
|
||||||
|
- automatische gesetzlicher Feiertage
|
||||||
|
- Pflege eigener Feiertage
|
||||||
|
|
||||||
|
- wöchentliches Abrechnungssystem
|
||||||
|
- Kontrolle der Arbeitszeiten durch direkte Führungskraft
|
||||||
|
|
||||||
|
- Ausgabe der Arbeitszeiten je Monat in PDF Format
|
||||||
|
|
||||||
|
- Anwesenheitsübersicht
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.letsstein.de/tom/arbeitszeitmessung arbeitszeitmessung
|
git clone https://git.letsstein.de/tom/arbeitszeitmessung arbeitszeitmessung
|
||||||
|
|
||||||
cd arbeitszeitmessung/Docker
|
cd arbeitszeitmessung
|
||||||
# .env Datei anpassen
|
|
||||||
docker compose up -d
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## PREVIEW
|
### Konfiguration:
|
||||||
|
|
||||||
Zeitverwaltungsansicht (/time):
|
- Datenbank
|
||||||
|
- `POSTGRES_USER` Postgres ADMIN Nutzername
|
||||||

|
- `POSTGRES_PASSWORD` Postgres ADMIN Passwort
|
||||||
|
- `POSTGRES_API_USER` Postgres API Nutzername für Webanwendung
|
||||||
Ansicht der Führungskraft (/team):
|
- `POSTGRES_API_PASS` Postgres API Passwort für Webanwendung
|
||||||
|
- `POSTGRES_PATH` Datebank Pfad
|
||||||

|
- `POSTGRES_DB` Postgres Datenbank Name
|
||||||
|
- `POSTGRES_PORT` Postgres Port für administration
|
||||||
Nutzeransicht (/user):
|
- System
|
||||||
|
- `TZ` Zeitzone
|
||||||

|
- `LOG_LEVEL` Welche Log-Nachrichten werden in der Konsole erscheinen
|
||||||
|
- Web/API
|
||||||
## Buchungstypen
|
- `API_TOKEN` API Token für ESP Endpoints
|
||||||
|
- `WEB_PORT` Port unter welchem Webserver erreichbar ist
|
||||||
1 - Kommen
|
- Ordnerstruktur
|
||||||
2 - Gehen
|
- `BACKUP_FOLDER` Pfad für DB Backup Datein
|
||||||
3 - Kommen Manuell
|
- `LOG_PATH` Pfad für Audit Logs
|
||||||
4 - Gehen Manuell
|
|
||||||
254 - Automatisch abgemeldet
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
Nutzung der API
|
|
||||||
wenn die `dev-docker-compose.yml` Datei gestartet wird, ist direkt ein SwaggerUI Server mit entsprechender Datei inbegriffen.
|
|
||||||
|
|
||||||
### Buchungen [/time]
|
|
||||||
|
|
||||||
#### [GET] Anfrage
|
|
||||||
|
|
||||||
Parameter: cardID (string)
|
|
||||||
Antwort: `200`
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"cradID": "test_card",
|
|
||||||
"readerID": "test_reader",
|
|
||||||
"bookingTyp": 2,
|
|
||||||
"loggedTime": "2024-09-05T08:37:53.117641Z",
|
|
||||||
"id": 5
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cradID": "test_card",
|
|
||||||
"readerID": "mytest",
|
|
||||||
"bookingTyp": 1,
|
|
||||||
"loggedTime": "2024-09-05T08:51:12.670827Z",
|
|
||||||
"id": 6
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Antwort `500`
|
|
||||||
Serverfehler
|
|
||||||
|
|
||||||
#### [PUT] Anfrage
|
|
||||||
|
|
||||||
Parameter: id (int)
|
|
||||||
Body: (veränderte Parameter)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cradID": "test_card",
|
|
||||||
"readerID": "mytest",
|
|
||||||
"bookingTyp": 1,
|
|
||||||
"loggedTime": "2024-09-05T08:51:12.670827Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Antwort `200`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cradID": "test_card",
|
|
||||||
"readerID": "mytest",
|
|
||||||
"bookingTyp": 1,
|
|
||||||
"loggedTime": "2024-09-05T08:51:12.670827Z",
|
|
||||||
"id": 6
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Neue Buchung [/time/new]
|
|
||||||
|
|
||||||
#### [PUT] Anfrage
|
|
||||||
|
|
||||||
Parameter:
|
|
||||||
|
|
||||||
- cardID (string)
|
|
||||||
- readerID (string)
|
|
||||||
- bookingType (string)
|
|
||||||
|
|
||||||
Antwort `202` Akzeptiert und eingefügt
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"cradID": "test_card",
|
|
||||||
"readerID": "mytest",
|
|
||||||
"bookingTyp": 1,
|
|
||||||
"loggedTime": "2024-09-05T08:51:12.670827Z",
|
|
||||||
"id": 6
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Antwort `409` Konflikt
|
|
||||||
Die vorherige Buchung am selben Tag hat den gleichen Buchungstyp
|
|
||||||
|
|
||||||
# Filestrukture
|
# Filestrukture
|
||||||
|
|
||||||
|
|||||||
43
install.sh
43
install.sh
@@ -64,9 +64,9 @@ if [ ! -f $envFile ]; then
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
if [ -z "$comment" ]; then
|
if [ -z "$comment" ]; then
|
||||||
printf "Value for $key - $comment (default: $default_value"
|
|
||||||
else
|
|
||||||
printf "Value for $key (default: $default_value"
|
printf "Value for $key (default: $default_value"
|
||||||
|
else
|
||||||
|
printf "Value for $key - $comment (default: $default_value"
|
||||||
fi
|
fi
|
||||||
if [ -n "$regex" ]; then
|
if [ -n "$regex" ]; then
|
||||||
printf ", must match: %s" "$regex"
|
printf ", must match: %s" "$regex"
|
||||||
@@ -113,27 +113,12 @@ LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2)
|
|||||||
if [ -z "$LOG_PATH" ]; then
|
if [ -z "$LOG_PATH" ]; then
|
||||||
echo "LOG_PATH not found in .env using default $(pwd)/logs"
|
echo "LOG_PATH not found in .env using default $(pwd)/logs"
|
||||||
LOG_PATH=$(pwd)/logs
|
LOG_PATH=$(pwd)/logs
|
||||||
else
|
|
||||||
LOG_PATH=Docker/$LOG_PATH
|
|
||||||
fi
|
fi
|
||||||
mkdir -p $LOG_PATH
|
mkdir -p $LOG_PATH
|
||||||
echo "Created logs folder at $LOG_PATH"
|
echo "Created logs folder at $LOG_PATH"
|
||||||
|
|
||||||
###########################################################################
|
###########################################################################
|
||||||
|
|
||||||
echo -e "\n\n"
|
|
||||||
echo "Start containers with docker compose up -d? [y/N]"
|
|
||||||
read -r start_containers
|
|
||||||
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
|
|
||||||
cd Docker
|
|
||||||
docker compose up -d
|
|
||||||
echo "Containers started."
|
|
||||||
else
|
|
||||||
echo "You can start them manually with: docker compose up -d"
|
|
||||||
fi
|
|
||||||
|
|
||||||
###########################################################################
|
|
||||||
|
|
||||||
echo -e "\n\n"
|
echo -e "\n\n"
|
||||||
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
|
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
|
||||||
read -r setup_cron
|
read -r setup_cron
|
||||||
@@ -146,13 +131,20 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
|
|||||||
|
|
||||||
POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2)
|
POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2)
|
||||||
if [ -z "$POSTGRES_DB" ]; then
|
if [ -z "$POSTGRES_DB" ]; then
|
||||||
echo "arbeitszeitmessung not found in .env using default arbeitszeitmessung"
|
echo "POSTGRES_DB not found in .env using default arbeitszeitmessung"
|
||||||
POSTGRES_DB="arbeitszeitmessung"
|
POSTGRES_DB="arbeitszeitmessung"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
BACKUP_FOLDER=$(grep -E '^BACKUP_FOLDER=' $envFile | cut -d= -f2)
|
||||||
|
if [ -z "$BACKUP_FOLDER" ]; then
|
||||||
|
echo "BACKUP_FOLDER not found in .env using default $(pwd)/backup"
|
||||||
|
BACKUP_FOLDER="$(pwd)/backup"
|
||||||
|
fi
|
||||||
|
|
||||||
sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript
|
sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript
|
||||||
sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript
|
sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript
|
||||||
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript
|
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript
|
||||||
|
sed -i "s/__BACKUP_FOLDER__/$BACKUP_FOLDER" $autoBackupScript
|
||||||
|
|
||||||
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript
|
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript
|
||||||
|
|
||||||
@@ -184,3 +176,18 @@ if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
|
|||||||
else
|
else
|
||||||
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
|
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
echo -e "\n\n"
|
||||||
|
echo "Start containers with docker compose up -d? [y/N]"
|
||||||
|
read -r start_containers
|
||||||
|
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
|
||||||
|
cd Docker
|
||||||
|
docker compose up -d
|
||||||
|
echo "Containers started."
|
||||||
|
else
|
||||||
|
echo "You can start them manually with: docker compose up -d"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Installation finished, you can re-run the script any time!"
|
||||||
|
|||||||
Reference in New Issue
Block a user