5 Commits

Author SHA1 Message Date
74bce88cc0 fixed #60
Some checks failed
Arbeitszeitmessung Deploy / Run Go Tests (push) Successful in 1m29s
Tests / Run Go Tests (push) Has been cancelled
Arbeitszeitmessung Deploy / Build Go Image and Upload (push) Successful in 2m7s
2025-12-02 16:53:43 +01:00
5a5e776e8b fixed #59 2025-12-02 16:53:33 +01:00
02b5d88d34 added typst as doc creator and put it into compose container 2025-12-02 16:50:37 +01:00
6ab48eb534 dev/install (#58)
All checks were successful
Tests / Run Go Tests (push) Successful in 1m24s
improved ci/cd pipeline + interactive install script

Reviewed-on: #58
Co-authored-by: Tom Tröger <t.troeger.02@gmail.com>
Co-committed-by: Tom Tröger <t.troeger.02@gmail.com>
2025-11-30 21:25:47 +01:00
7e5eaebca9 added worktime base to work time calculations
Some checks failed
Tests / Run Go Tests (push) Failing after 1m4s
2025-10-31 23:57:42 +01:00
23 changed files with 463 additions and 60 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -20,7 +20,7 @@ func convertDaysToTypst(days []models.IWorkDay, u models.User) ([]typstDay, erro
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(DE_DATE) 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)
@@ -72,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)
`) `)
@@ -84,7 +84,10 @@ 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
} }
@@ -129,6 +132,8 @@ func createEmployeReport(employee models.User, startDate, endDate time.Time) (by
targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate)) targetHours := (employee.ArbeitszeitProWoche() / 5) * time.Duration(helper.GetWorkingDays(startDate, endDate))
workingDays := models.GetDays(employee, startDate, endDate, false) workingDays := models.GetDays(employee, startDate, endDate, false)
slog.Debug("Baseline Working hours", "targetHours", targetHours.Hours())
var actualHours time.Duration var actualHours time.Duration
for _, day := range workingDays { for _, day := range workingDays {
actualHours += day.TimeWorkVirtual(employee) actualHours += day.TimeWorkVirtual(employee)

View File

@@ -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
) )

View File

@@ -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=

View File

@@ -68,3 +68,10 @@ func GetWorkingDays(startDate, endDate time.Time) int {
} }
return count return count
} }
func FormatGermanDayOfWeek(t time.Time) string {
return days[t.Weekday()][:2]
}
var days = [...]string{
"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}

View File

@@ -18,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
}{ }{
@@ -28,9 +28,9 @@ 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.")
} }
}) })
@@ -58,3 +58,27 @@ func TestGetWorkingDays(t *testing.T) {
}) })
} }
} }
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!")
}
})
}
}

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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() {
{{ {{

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

View 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"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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),
)
}

View File

@@ -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"

108
install.sh Executable file
View 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