Compare commits
8 Commits
a634b7a69e
...
1.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 74bce88cc0 | |||
| 5a5e776e8b | |||
| 02b5d88d34 | |||
| 6ab48eb534 | |||
| 7e5eaebca9 | |||
| ac59d2642f | |||
| cf5238f024 | |||
| 4bc5594dc5 |
@@ -2,7 +2,8 @@ name: Arbeitszeitmessung Deploy
|
|||||||
run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung
|
run-name: ${{ gitea.actor }} is building and deploying arbeitszeitmesssung
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: "*"
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
testing:
|
testing:
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ jobs:
|
|||||||
POSTGRES_DB: arbeitszeitmessung
|
POSTGRES_DB: arbeitszeitmessung
|
||||||
env:
|
env:
|
||||||
POSTGRES_HOST: postgres
|
POSTGRES_HOST: postgres
|
||||||
POSTGRES_USER: root
|
POSTGRES_API_USER: root
|
||||||
POSTGRES_PASSWORD: password
|
POSTGRES_API_PASS: password
|
||||||
POSTGRES_DB: arbeitszeitmessung
|
POSTGRES_DB: arbeitszeitmessung
|
||||||
POSTGRES_PORT: 5432
|
POSTGRES_PORT: 5432
|
||||||
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
|
RUNNER_TOOL_CACHE: /toolcache # Runner Tool Cache
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ func OpenDatabase() (models.IDatabase, error) {
|
|||||||
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
|
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
|
||||||
dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer")
|
dbUser := helper.GetEnv("POSTGRES_API_USER", "api_nutzer")
|
||||||
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password")
|
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password")
|
||||||
|
dbTz := helper.GetEnv("TZ", "Europe/Berlin")
|
||||||
|
|
||||||
connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=Europe/Berlin", dbUser, dbPassword, dbHost, dbName)
|
connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", dbUser, dbPassword, dbHost, dbName, dbTz)
|
||||||
return sql.Open("postgres", connStr)
|
return sql.Open("postgres", connStr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import (
|
|||||||
"github.com/Dadido3/go-typst"
|
"github.com/Dadido3/go-typst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const DE_DATE string = "02.01.2006"
|
||||||
|
|
||||||
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) {
|
func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, error) {
|
||||||
var typstDays []typstDay
|
var typstDays []typstDay
|
||||||
for _, day := range days {
|
for _, day := range days {
|
||||||
var thisTypstDay typstDay
|
var thisTypstDay typstDay
|
||||||
work, pause, overtime := day.GetAllWorkTimesVirtual(u)
|
work, pause, overtime := day.GetTimesVirtual(u, models.WorktimeBaseWeek)
|
||||||
thisTypstDay.Date = day.Date().Format("02.01.2006")
|
thisTypstDay.Date = day.Date().Format(DE_DATE)
|
||||||
thisTypstDay.Worktime = helper.FormatDurationFill(work, true)
|
thisTypstDay.Worktime = helper.FormatDurationFill(work, true)
|
||||||
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
|
thisTypstDay.Pausetime = helper.FormatDurationFill(pause, true)
|
||||||
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
|
thisTypstDay.Overtime = helper.FormatDurationFill(overtime, true)
|
||||||
@@ -70,7 +72,7 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) {
|
|||||||
// Import the template and invoke the template function with the custom data.
|
// Import the template and invoke the template function with the custom data.
|
||||||
// Show is used to replace the current document with whatever content the template function in `template.typ` returns.
|
// Show is used to replace the current document with whatever content the template function in `template.typ` returns.
|
||||||
markup.WriteString(`
|
markup.WriteString(`
|
||||||
#import "template.typ": abrechnung
|
#import "templates/abrechnung.typ": abrechnung
|
||||||
#show: doc => abrechnung(meta, days)
|
#show: doc => abrechnung(meta, days)
|
||||||
`)
|
`)
|
||||||
|
|
||||||
@@ -82,17 +84,83 @@ func renderPDF(days []typstDay, metadata typstMetadata) (bytes.Buffer, error) {
|
|||||||
// defer f.Close()
|
// defer f.Close()
|
||||||
//
|
//
|
||||||
|
|
||||||
typstCLI := typst.CLI{}
|
// typstCLI := typst.CLI{}
|
||||||
|
typstCLI := typst.DockerExec{
|
||||||
|
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
|
||||||
|
}
|
||||||
if err := typstCLI.Compile(&markup, &output, nil); err != nil {
|
if err := typstCLI.Compile(&markup, &output, nil); err != nil {
|
||||||
return output, err
|
return output, err
|
||||||
}
|
}
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PDFCreateController(w http.ResponseWriter, r *http.Request) {
|
||||||
|
helper.RequiresLogin(Session, w, r)
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
user, err := models.GetUserFromSession(Session, r.Context())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error getting user!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pp := paramParser.New(r.URL.Query())
|
||||||
|
startDate := pp.ParseTimestampFallback("start_date", time.DateOnly, time.Now())
|
||||||
|
var members []models.User = make([]models.User, 0)
|
||||||
|
output, err := createReports(user, members, startDate)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("Could not create pdf report", slog.Any("Error", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-type", "application/pdf")
|
||||||
|
output.WriteTo(w)
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed!", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createReports(user models.User, employes []models.User, startDate time.Time) (bytes.Buffer, error) {
|
||||||
|
if startDate.Day() > 1 {
|
||||||
|
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
|
||||||
|
}
|
||||||
|
endDate := startDate.AddDate(0, 1, -1)
|
||||||
|
return createEmployeReport(user, startDate, endDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEmployeReport(employee models.User, startDate, endDate time.Time) (bytes.Buffer, error) {
|
||||||
|
targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate))
|
||||||
|
workingDays := models.GetDays(employee, startDate, endDate, false)
|
||||||
|
|
||||||
|
slog.Debug("Baseline Working hours", "targetHours", targetHours.Hours())
|
||||||
|
|
||||||
|
var actualHours time.Duration
|
||||||
|
for _, day := range workingDays {
|
||||||
|
actualHours += day.TimeWorkVirtual(employee)
|
||||||
|
}
|
||||||
|
worktimeBalance := actualHours - targetHours
|
||||||
|
|
||||||
|
typstDays, err := convertDaysToTypst(workingDays, employee)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("Failed to convert days!")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := typstMetadata{
|
||||||
|
EmployeeName: fmt.Sprintf("%s %s", employee.Vorname, employee.Name),
|
||||||
|
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
|
||||||
|
Overtime: helper.FormatDurationFill(worktimeBalance, true),
|
||||||
|
WorkTime: helper.FormatDurationFill(actualHours, true),
|
||||||
|
OvertimeTotal: "",
|
||||||
|
CurrentTimestamp: time.Now().Format("02.01.2006 - 15:04 Uhr"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderPDF(typstDays, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
func PDFHandler(w http.ResponseWriter, r *http.Request) {
|
func PDFHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
helper.RequiresLogin(Session, w, r)
|
helper.RequiresLogin(Session, w, r)
|
||||||
pp := paramParser.New(r.URL.Query())
|
startDate := time.Now()
|
||||||
startDate := pp.ParseTimestamp("start_date", time.DateOnly, time.Now())
|
|
||||||
if startDate.Day() > 1 {
|
if startDate.Day() > 1 {
|
||||||
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
|
startDate = startDate.AddDate(0, 0, -(startDate.Day() - 1))
|
||||||
}
|
}
|
||||||
@@ -118,7 +186,7 @@ func PDFHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
metadata := typstMetadata{
|
metadata := typstMetadata{
|
||||||
EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name),
|
EmployeeName: fmt.Sprintf("%s %s", user.Vorname, user.Name),
|
||||||
TimeRange: fmt.Sprintf("%s - %s", startDate.Format("02.01.2006"), endDate.Format("02.01.2006")),
|
TimeRange: fmt.Sprintf("%s - %s", startDate.Format(DE_DATE), endDate.Format(DE_DATE)),
|
||||||
Overtime: helper.FormatDurationFill(aggregatedOvertime, true),
|
Overtime: helper.FormatDurationFill(aggregatedOvertime, true),
|
||||||
WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true),
|
WorkTime: helper.FormatDurationFill(aggregatedWorkTime, true),
|
||||||
OvertimeTotal: "",
|
OvertimeTotal: "",
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package endpoints
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"arbeitszeitmessung/helper"
|
"arbeitszeitmessung/helper"
|
||||||
|
"arbeitszeitmessung/helper/paramParser"
|
||||||
"arbeitszeitmessung/models"
|
"arbeitszeitmessung/models"
|
||||||
"arbeitszeitmessung/templates"
|
"arbeitszeitmessung/templates"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,17 +30,17 @@ func submitReport(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Println("Error parsing form", err)
|
log.Println("Error parsing form", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
userPN, _ := strconv.Atoi(r.FormValue("user"))
|
pp := paramParser.New(r.Form)
|
||||||
_weekTs := r.FormValue("week")
|
userPN, err := pp.ParseInt("user")
|
||||||
weekTs, err := time.Parse(time.DateOnly, _weekTs)
|
weekTs := pp.ParseTimestampFallback("week", time.DateOnly, time.Now())
|
||||||
user, err := models.GetUserByPersonalNr(userPN)
|
user, err := models.GetUserByPersonalNr(userPN)
|
||||||
workWeek := models.NewWorkWeek(user, weekTs, true)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Could not get user!")
|
log.Println("Could not get user!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workWeek := models.NewWorkWeek(user, weekTs, true)
|
||||||
|
|
||||||
switch r.FormValue("method") {
|
switch r.FormValue("method") {
|
||||||
case "send":
|
case "send":
|
||||||
err = workWeek.SendWeek()
|
err = workWeek.SendWeek()
|
||||||
@@ -62,14 +62,11 @@ func showWeeks(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
submissionDate := r.URL.Query().Get("submission_date")
|
|
||||||
lastSub := user.GetLastWorkWeekSubmission()
|
pp := paramParser.New(r.URL.Query())
|
||||||
if submissionDate != "" {
|
submissionDate := pp.ParseTimestampFallback("submission_date", time.DateOnly, user.GetLastWorkWeekSubmission())
|
||||||
submissionDate, err := time.Parse(time.DateOnly, submissionDate)
|
lastSub := helper.GetMonday(submissionDate)
|
||||||
if err == nil {
|
|
||||||
lastSub = helper.GetMonday(submissionDate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userWeek := models.NewWorkWeek(user, lastSub, true)
|
userWeek := models.NewWorkWeek(user, lastSub, true)
|
||||||
|
|
||||||
var workWeeks []models.WorkWeek
|
var workWeeks []models.WorkWeek
|
||||||
|
|||||||
@@ -2,14 +2,15 @@ package endpoints
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"arbeitszeitmessung/helper"
|
"arbeitszeitmessung/helper"
|
||||||
|
"arbeitszeitmessung/helper/paramParser"
|
||||||
"arbeitszeitmessung/models"
|
"arbeitszeitmessung/models"
|
||||||
"arbeitszeitmessung/templates"
|
"arbeitszeitmessung/templates"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -67,26 +68,15 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
pp := paramParser.New(r.URL.Query())
|
||||||
|
|
||||||
// TODO add config for timeoffset
|
// TODO add config for timeoffset
|
||||||
tsFrom, err := parseTimestamp(r, "time_from", time.Now().AddDate(0, -1, 0).Format(time.DateOnly))
|
tsFrom := pp.ParseTimestampFallback("time_from", time.DateOnly, time.Now().AddDate(0, -1, 0))
|
||||||
if err != nil {
|
tsTo := pp.ParseTimestampFallback("time_to", time.DateOnly, time.Now())
|
||||||
log.Println("Error parsing 'from' time", err)
|
|
||||||
http.Error(w, "Timestamp 'from' cannot be parsed!", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tsTo, err := parseTimestamp(r, "time_to", time.Now().Format(time.DateOnly))
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error parsing 'to' time", err)
|
|
||||||
http.Error(w, "Timestamp 'to' cannot be parsed!", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside
|
tsTo = tsTo.AddDate(0, 0, 1) // so that today is inside
|
||||||
|
|
||||||
days := models.GetDays(user, tsFrom, tsTo, true)
|
days := models.GetDays(user, tsFrom, tsTo, true)
|
||||||
sort.Slice(days, func(i, j int) bool {
|
|
||||||
return days[i].Date().After(days[j].Date())
|
|
||||||
})
|
|
||||||
|
|
||||||
lastSub := user.GetLastWorkWeekSubmission()
|
lastSub := user.GetLastWorkWeekSubmission()
|
||||||
var aggregatedOvertime time.Duration
|
var aggregatedOvertime time.Duration
|
||||||
@@ -116,6 +106,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func updateBooking(w http.ResponseWriter, r *http.Request) {
|
func updateBooking(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
|
pp := paramParser.New(r.Form)
|
||||||
var loc *time.Location
|
var loc *time.Location
|
||||||
loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin"))
|
loc, err := time.LoadLocation(helper.GetEnv("TZ", "Europe/Berlin"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -136,10 +127,9 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var check_in_out int
|
check_in_out, err := pp.ParseInt("check_in_out")
|
||||||
check_in_out, err = strconv.Atoi(r.FormValue("check_in_out"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error parsing check_in_out", err)
|
slog.Warn("Error parsing check_in_out")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require github.com/a-h/templ v0.3.943
|
|||||||
require github.com/alexedwards/scs/v2 v2.8.0
|
require github.com/alexedwards/scs/v2 v2.8.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Dadido3/go-typst v0.3.0
|
github.com/Dadido3/go-typst v0.8.0
|
||||||
github.com/golang-migrate/migrate/v4 v4.18.3
|
github.com/golang-migrate/migrate/v4 v4.18.3
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/Dadido3/go-typst v0.3.0 h1:Itix2FtQgBiOuHUNqgGUAK11Oo2WMlZGGGpCiQNK1IA=
|
github.com/Dadido3/go-typst v0.8.0 h1:uTLYprhkrBjwsCXRRuyYUFL0fpYHa2kIYoOB/CGqVNs=
|
||||||
github.com/Dadido3/go-typst v0.3.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA=
|
github.com/Dadido3/go-typst v0.8.0/go.mod h1:QYis9sT70u65kn1SkFfyPRmHsPxgoxWbAixwfPReOZA=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
|
github.com/a-h/templ v0.3.943 h1:o+mT/4yqhZ33F3ootBiHwaY4HM5EVaOJfIshvd5UNTY=
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package paramParser
|
package paramParser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,22 +12,82 @@ type ParamsParser struct {
|
|||||||
urlParams url.Values
|
urlParams url.Values
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(_urlParams url.Values) ParamsParser {
|
type NoValueError struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NoValueError) Error() string {
|
||||||
|
return fmt.Sprintf("No value found for key %s", e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(params url.Values) ParamsParser {
|
||||||
return ParamsParser{
|
return ParamsParser{
|
||||||
urlParams: _urlParams,
|
urlParams: params,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ParamsParser) ParseTimestamp(key string, format string, fallback time.Time) time.Time {
|
func (p *ParamsParser) ParseTimestampFallback(key string, format string, fallback time.Time) time.Time {
|
||||||
paramTimestamp := p.urlParams.Get(key)
|
if !p.urlParams.Has(key) {
|
||||||
if paramTimestamp == "" {
|
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
paramTimestamp := p.urlParams.Get(key)
|
||||||
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
|
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
|
||||||
return timestamp
|
return timestamp
|
||||||
} else {
|
} else {
|
||||||
slog.Warn("Error parsing HTTP Params to timestamp", slog.Any("key", key), slog.Any("error", err))
|
slog.Warn("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err))
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParamsParser) ParseTimestamp(key string, format string) (time.Time, error) {
|
||||||
|
if !p.urlParams.Has(key) {
|
||||||
|
return time.Time{}, &NoValueError{Key: key}
|
||||||
|
}
|
||||||
|
paramTimestamp := p.urlParams.Get(key)
|
||||||
|
if timestamp, err := time.Parse(format, paramTimestamp); err == nil {
|
||||||
|
return timestamp, nil
|
||||||
|
} else {
|
||||||
|
slog.Debug("Error parsing HTTP Params to time.Time", slog.Any("key", key), slog.Any("error", err))
|
||||||
|
return timestamp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParamsParser) ParseStringFallback(key string, fallback string) string {
|
||||||
|
if !p.urlParams.Has(key) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return p.urlParams.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParamsParser) ParseString(key string, fallback string) (string, error) {
|
||||||
|
if !p.urlParams.Has(key) {
|
||||||
|
return "", &NoValueError{Key: key}
|
||||||
|
}
|
||||||
|
return p.urlParams.Get(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParamsParser) ParseIntFallback(key string, fallback int) int {
|
||||||
|
if !p.urlParams.Has(key) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
paramInt := p.urlParams.Get(key)
|
||||||
|
if result, err := strconv.Atoi(paramInt); err == nil {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
slog.Warn("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ParamsParser) ParseInt(key string) (int, error) {
|
||||||
|
if !p.urlParams.Has(key) {
|
||||||
|
return 0, &NoValueError{Key: key}
|
||||||
|
}
|
||||||
|
paramInt := p.urlParams.Get(key)
|
||||||
|
if result, err := strconv.Atoi(paramInt); err == nil {
|
||||||
|
return result, nil
|
||||||
|
} else {
|
||||||
|
slog.Debug("Error parsing HTTP Params to Int", slog.Any("key", key), slog.Any("error", err))
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,3 +55,23 @@ func FormatDurationFill(d time.Duration, fill bool) string {
|
|||||||
func IsSameDate(a, b time.Time) bool {
|
func IsSameDate(a, b time.Time) bool {
|
||||||
return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour))
|
return a.Truncate(24 * time.Hour).Equal(b.Truncate(24 * time.Hour))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetWorkingDays(startDate, endDate time.Time) int {
|
||||||
|
if endDate.Before(startDate) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var count int = 0
|
||||||
|
for d := startDate.Truncate(24 * time.Hour); !d.After(endDate); d = d.Add(24 * time.Hour) {
|
||||||
|
if !IsWeekend(d) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatGermanDayOfWeek(t time.Time) string {
|
||||||
|
return days[t.Weekday()][:2]
|
||||||
|
}
|
||||||
|
|
||||||
|
var days = [...]string{
|
||||||
|
"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -17,7 +18,7 @@ func TestGetMonday(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatDuration(t *testing.T) {
|
func TestFormatDuration(t *testing.T) {
|
||||||
durations := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
}{
|
}{
|
||||||
@@ -27,11 +28,57 @@ func TestFormatDuration(t *testing.T) {
|
|||||||
{"-1h 30min", time.Duration(-90 * time.Minute)},
|
{"-1h 30min", time.Duration(-90 * time.Minute)},
|
||||||
{"", 0},
|
{"", 0},
|
||||||
}
|
}
|
||||||
for _, d := range durations {
|
for _, tc := range testCases {
|
||||||
t.Run(d.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
if FormatDuration(d.duration) != d.name {
|
if FormatDuration(tc.duration) != tc.name {
|
||||||
t.Error("Format missmatch in Formatduration.")
|
t.Error("Format missmatch in Formatduration.")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetWorkingDays(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
start string
|
||||||
|
end string
|
||||||
|
days int
|
||||||
|
}{
|
||||||
|
{"2025-10-01", "2025-10-02", 2},
|
||||||
|
{"2025-10-02", "2025-10-01", 0},
|
||||||
|
{"2025-10-01", "2025-10-31", 23},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("WorkingDayTest: %d days", tc.days), func(t *testing.T) {
|
||||||
|
startDate, _ := time.Parse(time.DateOnly, tc.start)
|
||||||
|
endDate, _ := time.Parse(time.DateOnly, tc.end)
|
||||||
|
if GetWorkingDays(startDate, endDate) != tc.days {
|
||||||
|
t.Error("Calculated workdays do not match target")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatGermanDayOfWeek(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
date string
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
{"2025-12-01", "Mo"},
|
||||||
|
{"2025-12-02", "Di"},
|
||||||
|
{"2025-12-03", "Mi"},
|
||||||
|
{"2025-12-04", "Do"},
|
||||||
|
{"2025-12-05", "Fr"},
|
||||||
|
{"2025-12-06", "Sa"},
|
||||||
|
{"2025-12-07", "So"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("FormatWeekDayTest: %s date", tc.date), func(t *testing.T) {
|
||||||
|
date, _ := time.Parse(time.DateOnly, tc.date)
|
||||||
|
if FormatGermanDayOfWeek(date) != tc.result {
|
||||||
|
t.Error("Formatted workday did not match!")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func main() {
|
|||||||
server.HandleFunc("/team", endpoints.TeamHandler)
|
server.HandleFunc("/team", endpoints.TeamHandler)
|
||||||
server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
|
server.HandleFunc("/presence", endpoints.TeamPresenceHandler)
|
||||||
server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
|
server.Handle("/pdf", ParamsMiddleware(endpoints.PDFFormHandler))
|
||||||
server.HandleFunc("/pdf/generate", endpoints.PDFHandler)
|
server.HandleFunc("/pdf/generate", endpoints.PDFCreateController)
|
||||||
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
|
server.Handle("/", http.RedirectHandler("/time", http.StatusPermanentRedirect))
|
||||||
server.Handle("/static/", http.StripPrefix("/static/", fs))
|
server.Handle("/static/", http.StripPrefix("/static/", fs))
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,55 @@ func (a *Absence) IsMultiDay() bool {
|
|||||||
return !a.DateFrom.Equal(a.DateTo)
|
return !a.DateFrom.Equal(a.DateTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetWorktimeReal(u User, base WorktimeBase) time.Duration {
|
||||||
|
if a.AbwesenheitTyp.WorkTime <= 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch base {
|
||||||
|
case WorktimeBaseDay:
|
||||||
|
return u.ArbeitszeitProTag()
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
return u.ArbeitszeitProWoche() / 5
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
func (a *Absence) GetPausetimeReal(u User, base WorktimeBase) time.Duration {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetOvertimeReal(u User, base WorktimeBase) time.Duration {
|
||||||
|
if a.AbwesenheitTyp.WorkTime > 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch base {
|
||||||
|
case WorktimeBaseDay:
|
||||||
|
return -u.ArbeitszeitProTag()
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
return -u.ArbeitszeitProWoche() / 5
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration {
|
||||||
|
return a.GetWorktimeReal(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration {
|
||||||
|
return a.GetPausetimeReal(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration {
|
||||||
|
return a.GetOvertimeReal(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
|
||||||
|
return a.GetWorktimeReal(u, base), a.GetPausetimeReal(u, base), a.GetOvertimeReal(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Absence) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
|
||||||
|
return a.GetWorktimeVirtual(u, base), a.GetPausetimeVirtual(u, base), a.GetOvertimeVirtual(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Absence) TimeWorkVirtual(u User) time.Duration {
|
func (a *Absence) TimeWorkVirtual(u User) time.Duration {
|
||||||
return a.TimeWorkReal(u)
|
return a.TimeWorkReal(u)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ type IWorkDay interface {
|
|||||||
IsKurzArbeit() bool
|
IsKurzArbeit() bool
|
||||||
GetDayProgress(User) int8
|
GetDayProgress(User) int8
|
||||||
RequiresAction() bool
|
RequiresAction() bool
|
||||||
|
GetWorktimeReal(User, WorktimeBase) time.Duration
|
||||||
|
GetPausetimeReal(User, WorktimeBase) time.Duration
|
||||||
|
GetOvertimeReal(User, WorktimeBase) time.Duration
|
||||||
|
GetWorktimeVirtual(User, WorktimeBase) time.Duration
|
||||||
|
GetPausetimeVirtual(User, WorktimeBase) time.Duration
|
||||||
|
GetOvertimeVirtual(User, WorktimeBase) time.Duration
|
||||||
|
GetTimesReal(User, WorktimeBase) (work, pause, overtime time.Duration)
|
||||||
|
GetTimesVirtual(User, WorktimeBase) (work, pause, overtime time.Duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkDay struct {
|
type WorkDay struct {
|
||||||
@@ -37,6 +45,13 @@ type WorkDay struct {
|
|||||||
kurzArbeitAbsence Absence
|
kurzArbeitAbsence Absence
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorktimeBase string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorktimeBaseWeek WorktimeBase = "week"
|
||||||
|
WorktimeBaseDay WorktimeBase = "day"
|
||||||
|
)
|
||||||
|
|
||||||
func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay {
|
func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay {
|
||||||
var allDays map[string]IWorkDay = make(map[string]IWorkDay)
|
var allDays map[string]IWorkDay = make(map[string]IWorkDay)
|
||||||
|
|
||||||
@@ -66,6 +81,108 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
|
|||||||
return sortedDays
|
return sortedDays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets the time as is in the db (with corrected pause times)
|
||||||
|
func (d *WorkDay) GetWorktimeReal(u User, base WorktimeBase) time.Duration {
|
||||||
|
work, pause := calcWorkPause(d.Bookings)
|
||||||
|
work, pause = correctWorkPause(work, pause)
|
||||||
|
return work
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the corrected pause times based on db entries
|
||||||
|
func (d *WorkDay) GetPausetimeReal(u User, base WorktimeBase) time.Duration {
|
||||||
|
work, pause := calcWorkPause(d.Bookings)
|
||||||
|
work, pause = correctWorkPause(work, pause)
|
||||||
|
return pause
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the overtime based on the db entries
|
||||||
|
func (d *WorkDay) GetOvertimeReal(u User, base WorktimeBase) time.Duration {
|
||||||
|
work, pause := calcWorkPause(d.Bookings)
|
||||||
|
work, pause = correctWorkPause(work, pause)
|
||||||
|
|
||||||
|
var targetHours time.Duration
|
||||||
|
switch base {
|
||||||
|
case WorktimeBaseDay:
|
||||||
|
targetHours = u.ArbeitszeitProTag()
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
targetHours = u.ArbeitszeitProWoche() / 5
|
||||||
|
}
|
||||||
|
return work - targetHours
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the worktime based on absence or kurzarbeit
|
||||||
|
func (d *WorkDay) GetWorktimeVirtual(u User, base WorktimeBase) time.Duration {
|
||||||
|
if !d.IsKurzArbeit() {
|
||||||
|
return d.GetWorktimeReal(u, base)
|
||||||
|
}
|
||||||
|
switch base {
|
||||||
|
case WorktimeBaseDay:
|
||||||
|
return u.ArbeitszeitProTag()
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
return u.ArbeitszeitProWoche() / 5
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *WorkDay) GetPausetimeVirtual(u User, base WorktimeBase) time.Duration {
|
||||||
|
return d.GetPausetimeReal(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *WorkDay) GetOvertimeVirtual(u User, base WorktimeBase) time.Duration {
|
||||||
|
work := d.GetWorktimeVirtual(u, base)
|
||||||
|
|
||||||
|
var targetHours time.Duration
|
||||||
|
switch base {
|
||||||
|
case WorktimeBaseDay:
|
||||||
|
targetHours = u.ArbeitszeitProTag()
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
targetHours = u.ArbeitszeitProWoche() / 5
|
||||||
|
}
|
||||||
|
return work - targetHours
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *WorkDay) GetTimesReal(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
|
||||||
|
return d.GetWorktimeReal(u, base), d.GetPausetimeReal(u, base), d.GetOvertimeReal(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *WorkDay) GetTimesVirtual(u User, base WorktimeBase) (work, pause, overtime time.Duration) {
|
||||||
|
return d.GetWorktimeVirtual(u, base), d.GetPausetimeVirtual(u, base), d.GetOvertimeVirtual(u, base)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcWorkPause(bookings []Booking) (work, pause time.Duration) {
|
||||||
|
var lastBooking Booking
|
||||||
|
for _, b := range bookings {
|
||||||
|
if b.CheckInOut%2 == 1 {
|
||||||
|
if !lastBooking.Timestamp.IsZero() {
|
||||||
|
pause += b.Timestamp.Sub(lastBooking.Timestamp)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
work += b.Timestamp.Sub(lastBooking.Timestamp)
|
||||||
|
}
|
||||||
|
lastBooking = b
|
||||||
|
}
|
||||||
|
if len(bookings)%2 == 1 {
|
||||||
|
work += time.Since(lastBooking.Timestamp.Local())
|
||||||
|
}
|
||||||
|
return work, pause
|
||||||
|
}
|
||||||
|
|
||||||
|
func correctWorkPause(workIn, pauseIn time.Duration) (work, pause time.Duration) {
|
||||||
|
if workIn <= 6*time.Hour || pauseIn > 45*time.Minute {
|
||||||
|
return workIn, pauseIn
|
||||||
|
}
|
||||||
|
|
||||||
|
var diff time.Duration
|
||||||
|
if workIn <= (9*time.Hour) && pauseIn < 30*time.Minute {
|
||||||
|
diff = 30*time.Minute - pauseIn
|
||||||
|
} else if pauseIn < 45*time.Minute {
|
||||||
|
diff = 45*time.Minute - pauseIn
|
||||||
|
}
|
||||||
|
work = workIn - diff
|
||||||
|
pause = pauseIn + diff
|
||||||
|
return work, pause
|
||||||
|
}
|
||||||
|
|
||||||
func sortDays(days map[string]IWorkDay, forward bool) []IWorkDay {
|
func sortDays(days map[string]IWorkDay, forward bool) []IWorkDay {
|
||||||
var sortedDays []IWorkDay
|
var sortedDays []IWorkDay
|
||||||
for _, day := range days {
|
for _, day := range days {
|
||||||
@@ -127,7 +244,7 @@ func (d *WorkDay) TimeWorkReal(u User) time.Duration {
|
|||||||
if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 {
|
if helper.IsSameDate(d.Date(), time.Now()) && len(d.Bookings)%2 == 1 {
|
||||||
d.realWorkTime += time.Since(lastBooking.Timestamp.Local())
|
d.realWorkTime += time.Since(lastBooking.Timestamp.Local())
|
||||||
}
|
}
|
||||||
slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String()))
|
// slog.Debug("Calculated RealWorkTime for user", "user", u, slog.String("worktime", d.realWorkTime.String()))
|
||||||
return d.realWorkTime
|
return d.realWorkTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ templ PDFForm(teamMembers []models.User) {
|
|||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-sub divide-x-1 responsive">
|
<div class="grid-sub divide-x-1 responsive">
|
||||||
<div class="grid-cell">Direktvorschau oder Download</div>
|
<div class="grid-cell">PDFs Bündeln</div>
|
||||||
<div class="grid-cell col-span-3 flex gap-2 flex-col md:flex-row">
|
<div class="grid-cell col-span-3 flex gap-2 flex-col md:flex-row">
|
||||||
<button class="btn" type="button" name="action" value="download">Download</button>
|
<button class="btn" type="button" name="action" value="download">Einzeln</button>
|
||||||
<button class="btn" type="button" name="action" value="preview" onclick="">Vorschau</button>
|
<button class="btn" type="button" name="action" value="preview" onclick="">Bündel</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func PDFForm(teamMembers []models.User) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">Direktvorschau oder Download</div><div class=\"grid-cell col-span-3 flex gap-2 flex-col md:flex-row\"><button class=\"btn\" type=\"button\" name=\"action\" value=\"download\">Download</button> <button class=\"btn\" type=\"button\" name=\"action\" value=\"preview\" onclick=\"\">Vorschau</button></div></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div></div></div><div class=\"grid-sub divide-x-1 responsive\"><div class=\"grid-cell\">PDFs Bündeln</div><div class=\"grid-cell col-span-3 flex gap-2 flex-col md:flex-row\"><button class=\"btn\" type=\"button\" name=\"action\" value=\"download\">Einzeln</button> <button class=\"btn\" type=\"button\" name=\"action\" value=\"preview\" onclick=\"\">Bündel</button></div></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ templ defaultWeekDayComponent(u models.User, day models.IWorkDay) {
|
|||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
@timeGaugeComponent(day.GetDayProgress(u), false)
|
@timeGaugeComponent(day.GetDayProgress(u), false)
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<p class=""><span class="font-bold uppercase hidden md:inline">{ day.Date().Format("Mon") }:</span> { day.Date().Format("02.01.2006") }</p>
|
<p class=""><span class="font-bold uppercase hidden md:inline">{ helper.FormatGermanDayOfWeek(day.Date()) }:</span> { day.Date().Format("02.01.2006") }</p>
|
||||||
if day.IsWorkDay() {
|
if day.IsWorkDay() {
|
||||||
{{
|
{{
|
||||||
workDay, _ := day.(*models.WorkDay)
|
workDay, _ := day.(*models.WorkDay)
|
||||||
|
|||||||
@@ -151,9 +151,9 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("Mon"))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 92}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 108}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -166,7 +166,7 @@ func defaultWeekDayComponent(u models.User, day models.IWorkDay) templ.Component
|
|||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var8 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 136}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/teamComponents.templ`, Line: 35, Col: 152}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ templ defaultDayComponent(day models.IWorkDay) {
|
|||||||
@timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour)))
|
@timeGaugeComponent(day.GetDayProgress(user), day.Date().Equal(time.Now().Truncate(24*time.Hour)))
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<span class="font-bold uppercase hidden md:inline">{ day.Date().Format("Mon") }:</span> { day.Date().Format("02.01.2006") }
|
<span class="font-bold uppercase hidden md:inline">{ helper.FormatGermanDayOfWeek(day.Date()) }:</span> { day.Date().Format("02.01.2006") }
|
||||||
</p>
|
</p>
|
||||||
if day.IsWorkDay() {
|
if day.IsWorkDay() {
|
||||||
{{
|
{{
|
||||||
|
|||||||
@@ -269,9 +269,9 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var12 string
|
var templ_7745c5c3_Var12 string
|
||||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("Mon"))
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(helper.FormatGermanDayOfWeek(day.Date()))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 82}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 98}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
@@ -284,7 +284,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
|
|||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(day.Date().Format("02.01.2006"))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 126}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/timePage.templ`, Line: 101, Col: 142}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
POSTGRES_USER=root
|
|
||||||
POSTGRES_PASSWORD=very_secure
|
|
||||||
POSTGRES_API_USER=api_nutzer
|
|
||||||
POSTGRES_API_PASS=password
|
|
||||||
POSTGRES_PATH=../DB
|
|
||||||
POSTGRES_DB=arbeitszeitmessung
|
|
||||||
EXPOSED_PORT=8000
|
|
||||||
TZ=Europe/Berlin
|
|
||||||
PGTZ=Europe/Berlin
|
|
||||||
API_TOKEN=dont_access
|
|
||||||
EMPTY_DAYS=false
|
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
name: arbeitszeitmessung-dev
|
name: arbeitszeitmessung-dev
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:16
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
PGDATA: /var/lib/postgresql/data/pg_data
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${POSTGRES_PATH}:/var/lib/postgresql/data
|
- ${POSTGRES_PATH}:/var/lib/postgresql/data
|
||||||
# - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
|
# - ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
|
||||||
@@ -19,21 +13,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8001:8080
|
- 8001:8080
|
||||||
backend:
|
backend:
|
||||||
image: git.letsstein.de/tom/arbeitszeitmessung
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_HOST: db
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
|
||||||
EXPOSED_PORT: ${EXPOSED_PORT}
|
|
||||||
NO_CORS: true
|
NO_CORS: true
|
||||||
ports:
|
|
||||||
- ${EXPOSED_PORT}:8080
|
|
||||||
volumes:
|
|
||||||
- ../logs:/app/Backend/logs
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
|
|
||||||
swagger:
|
swagger:
|
||||||
image: swaggerapi/swagger-ui
|
image: swaggerapi/swagger-ui
|
||||||
|
|||||||
@@ -6,28 +6,31 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
PGTZ: ${TZ}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
|
||||||
PGDATA: /var/lib/postgresql/data/pg_data
|
PGDATA: /var/lib/postgresql/data/pg_data
|
||||||
volumes:
|
volumes:
|
||||||
- ${POSTGRES_PATH}:/var/lib/postgresql/data
|
- ${POSTGRES_PATH}:/var/lib/postgresql/data
|
||||||
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
|
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- ${POSTGRES_PORT}:5432
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: git.letsstein.de/tom/arbeitszeitmessung
|
image: git.letsstein.de/tom/arbeitszeitmessung-webserver
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_HOST: db
|
POSTGRES_HOST: db
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
EXPOSED_PORT: ${EXPOSED_PORT}
|
|
||||||
ports:
|
ports:
|
||||||
- ${EXPOSED_PORT}:8080
|
- ${WEB_PORT}:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
|
- document-creator
|
||||||
volumes:
|
volumes:
|
||||||
- ../logs:/app/Backend/logs
|
- ../logs:/app/Backend/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
document-creator:
|
||||||
|
image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
|
||||||
|
container_name: ${TYPST_CONTAINER}
|
||||||
|
restart: unless-stopped
|
||||||
|
|||||||
12
Docker/env.example
Normal file
12
Docker/env.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
POSTGRES_USER=root # Postgres ADMIN Nutzername
|
||||||
|
POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort
|
||||||
|
POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung)
|
||||||
|
POSTGRES_API_PASS=password # Postgres API Passwort (für Arbeitszeitmessung)
|
||||||
|
POSTGRES_PATH=../DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
|
||||||
|
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
|
||||||
|
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port will not be exposed by default.
|
||||||
|
TZ=Europe/Berlin # Zeitzone
|
||||||
|
PGTZ=Europe/Berlin # Zeitzone
|
||||||
|
API_TOKEN=dont_access # API Token für ESP Endpoints
|
||||||
|
WEB_PORT=8000 # Port from which Arbeitszeitmessung should be accessable regex:^[0-9]{1,5}$
|
||||||
|
TYPST_CONTAINER=arbeitszeitmessung-doc-creator # Name of the pdf compiler container
|
||||||
6
DocumentCreator/Dockerfile
Normal file
6
DocumentCreator/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM ghcr.io/typst/typst:0.14.0
|
||||||
|
|
||||||
|
COPY ./templates ./templates
|
||||||
|
COPY ./static ./static
|
||||||
|
|
||||||
|
ENTRYPOINT ["sh", "-c", "while true; do sleep 3600; done"]
|
||||||
BIN
DocumentCreator/static/logo.png
Normal file
BIN
DocumentCreator/static/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
92
DocumentCreator/templates/abrechnung.typ
Normal file
92
DocumentCreator/templates/abrechnung.typ
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#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),
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
8
Makefile
8
Makefile
@@ -52,3 +52,11 @@ test:
|
|||||||
|
|
||||||
scan: test
|
scan: test
|
||||||
$(MAKE) -C Backend scan
|
$(MAKE) -C Backend scan
|
||||||
|
|
||||||
|
Docker/.env:
|
||||||
|
cp Docker/env.example Docker/.env
|
||||||
|
echo "Konfigurations Datei erstellt (./Docker/.env), bitte zuerst ausfüllen und danach erneut 'make install' ausführen"
|
||||||
|
exit 0
|
||||||
|
|
||||||
|
install: Docker/.env
|
||||||
|
echo "Install"
|
||||||
|
|||||||
16
db.sql
16
db.sql
@@ -176,15 +176,15 @@ sample_bookings AS (
|
|||||||
(d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts,
|
(d.work_date + make_time(16, floor(random()*50)::int, 0))::timestamptz AS ts,
|
||||||
1 AS anwesenheit_typ
|
1 AS anwesenheit_typ
|
||||||
FROM days d
|
FROM days d
|
||||||
),
|
)
|
||||||
ins_anw AS (
|
|
||||||
-- insert only bookings up to now (prevents future times on today)
|
-- insert only bookings up to now (prevents future times on today)
|
||||||
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
|
INSERT INTO anwesenheit ("timestamp", card_uid, check_in_out, geraet_id)
|
||||||
SELECT ts, card_uid, check_in_out, geraet_id
|
SELECT ts, card_uid, check_in_out, geraet_id
|
||||||
FROM sample_bookings
|
FROM sample_bookings
|
||||||
WHERE ts <= NOW()
|
WHERE ts <= NOW()
|
||||||
RETURNING 1
|
RETURNING 1;
|
||||||
)
|
|
||||||
-- now insert absences (uses the same days CTE)
|
-- now insert absences (uses the same days CTE)
|
||||||
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
|
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
|
||||||
SELECT
|
SELECT
|
||||||
@@ -247,15 +247,13 @@ all_bookings AS (
|
|||||||
SELECT * FROM base_bookings
|
SELECT * FROM base_bookings
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT * FROM pause_bookings
|
SELECT * FROM pause_bookings
|
||||||
),
|
)
|
||||||
ins_anw AS (
|
|
||||||
INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
|
INSERT INTO anwesenheit ("timestamp", "card_uid", "check_in_out", "geraet_id", "anwesenheit_typ")
|
||||||
SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
|
SELECT ts, card_uid, check_in_out, geraet_id, 1 as anwesenheit_typ
|
||||||
FROM all_bookings
|
FROM all_bookings
|
||||||
WHERE ts <= NOW()
|
WHERE ts <= NOW()
|
||||||
ORDER BY work_date, ts
|
ORDER BY work_date, ts;
|
||||||
RETURNING 1
|
|
||||||
)
|
|
||||||
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
|
INSERT INTO abwesenheit (card_uid, abwesenheit_typ, datum)
|
||||||
SELECT
|
SELECT
|
||||||
d.card_uid,
|
d.card_uid,
|
||||||
|
|||||||
108
install.sh
Executable file
108
install.sh
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
envFile=Docker/.env
|
||||||
|
envExample=Docker/env.example
|
||||||
|
|
||||||
|
echo "Checking Docker installation..."
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "Docker not found. Install Docker? [y/N]"
|
||||||
|
read -r install_docker
|
||||||
|
if [[ "$install_docker" =~ ^[Yy]$ ]]; then
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
else
|
||||||
|
echo "Docker is required. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Docker is already installed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Checking Docker Compose..."
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
echo "Docker Compose plugin missing. You may need to update Docker."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Preparing .env file..."
|
||||||
|
if [ ! -f $envFile ]; then
|
||||||
|
if [ -f $envExample ]; then
|
||||||
|
echo ".env not found. Creating interactively from .env.example."
|
||||||
|
> $envFile
|
||||||
|
|
||||||
|
while IFS= read -r line; do
|
||||||
|
|
||||||
|
#ignore empty lines and comments
|
||||||
|
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
|
||||||
|
|
||||||
|
|
||||||
|
key=$(printf "%s" "$line" | cut -d '=' -f 1)
|
||||||
|
rest=$(printf "%s" "$line" | cut -d '=' -f 2-)
|
||||||
|
|
||||||
|
# extract inline comment portion
|
||||||
|
comment=$(printf "%s" "$rest" | sed -n 's/.*# \(.*\)$/\1/p')
|
||||||
|
raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
|
||||||
|
default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
|
||||||
|
|
||||||
|
regex=""
|
||||||
|
if [[ "$comment" =~ regex:(.*)$ ]]; then
|
||||||
|
regex="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
comment=$(printf "%s" "$comment" | sed 's/ regex:.*//')
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if [ -z "$comment" ]; then
|
||||||
|
printf "Value for $key - $comment (default: $default_value"
|
||||||
|
else
|
||||||
|
printf "Value for $key (default: $default_value"
|
||||||
|
fi
|
||||||
|
if [ -n "$regex" ]; then
|
||||||
|
printf ", must match: %s" "$regex"
|
||||||
|
fi
|
||||||
|
printf "):\n"
|
||||||
|
|
||||||
|
read user_input < /dev/tty
|
||||||
|
|
||||||
|
# empty input -> take default
|
||||||
|
[ -z "$user_input" ] && user_input="$default_value"
|
||||||
|
|
||||||
|
printf "\e[A$user_input\n"
|
||||||
|
|
||||||
|
# validate
|
||||||
|
if [ -n "$regex" ]; then
|
||||||
|
if [[ "$user_input" =~ $regex ]]; then
|
||||||
|
echo "$key=$user_input" >> $envFile
|
||||||
|
break
|
||||||
|
else
|
||||||
|
printf "Invalid value. Does not match regex: %s\n" "$regex"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "$key=$user_input" >> $envFile
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
done < $envExample
|
||||||
|
|
||||||
|
echo ".env created."
|
||||||
|
else
|
||||||
|
echo "No .env or .env.example found."
|
||||||
|
echo "Creating an empty .env file for manual editing."
|
||||||
|
touch $envFile
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Using existing .env. (found at $envFile)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Start containers with docker compose up -d? [y/N]"
|
||||||
|
read -r start_containers
|
||||||
|
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
|
||||||
|
cd Docker
|
||||||
|
mkdir ../logs
|
||||||
|
docker compose up -d
|
||||||
|
echo "Containers started."
|
||||||
|
else
|
||||||
|
echo "You can start them manually with: docker compose up -d"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user