Compare commits
7 Commits
2.0.0-rc.1
...
2.0.0-rc.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ac6c5f9b8 | |||
| f9fc3d91d1 | |||
| f0de9961dc | |||
| 4ded8632e5 | |||
| b2af48463c | |||
| 0b72147e02 | |||
| d1b46cf894 |
@@ -39,34 +39,34 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
context: Backend
|
context: Backend
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
document-creator:
|
# document-creator:
|
||||||
name: Build Document Creator
|
# name: Build Document Creator
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
steps:
|
# steps:
|
||||||
- name: Checkout
|
# - name: Checkout
|
||||||
uses: actions/checkout@v4
|
# uses: actions/checkout@v4
|
||||||
- name: Login to GitHub Container Registry
|
# - name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
# uses: docker/login-action@v3
|
||||||
with:
|
# with:
|
||||||
registry: git.letsstein.de
|
# registry: git.letsstein.de
|
||||||
username: ${{ gitea.actor }}
|
# username: ${{ gitea.actor }}
|
||||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
# password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
- name: Set up QEMU
|
# - name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
# uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
# - name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
# uses: docker/setup-buildx-action@v3
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
# - name: Extract metadata (tags, labels) for Docker
|
||||||
id: meta
|
# id: meta
|
||||||
uses: docker/metadata-action@v5
|
# uses: docker/metadata-action@v5
|
||||||
with:
|
# with:
|
||||||
images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
|
# images: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
|
||||||
tags: |
|
# tags: |
|
||||||
type=raw,value=latest
|
# type=raw,value=latest
|
||||||
type=pep440,pattern={{version}}
|
# type=pep440,pattern={{version}}
|
||||||
- name: Build and push
|
# - name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
# uses: docker/build-push-action@v6
|
||||||
with:
|
# with:
|
||||||
platforms: linux/amd64,linux/arm64
|
# platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
# push: true
|
||||||
context: Backend
|
# context: DocumentCreator
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
# tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ COPY . .
|
|||||||
RUN go build -o server .
|
RUN go build -o server .
|
||||||
|
|
||||||
FROM alpine:3.22
|
FROM alpine:3.22
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata typst
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/server /app/server
|
COPY --from=build /app/server /app/server
|
||||||
|
COPY ./doc/static /doc/static
|
||||||
|
COPY ./doc/templates /doc/templates
|
||||||
|
|
||||||
COPY /static /app/static
|
COPY /static /app/static
|
||||||
ENTRYPOINT ["./server"]
|
ENTRYPOINT ["./server"]
|
||||||
|
|||||||
BIN
Backend/doc/static/logo.png
vendored
Normal file
BIN
Backend/doc/static/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
97
Backend/doc/templates/abrechnung.typ
vendored
Normal file
97
Backend/doc/templates/abrechnung.typ
vendored
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#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, .875fr, 1.25fr),
|
||||||
|
fill: (x, y) =>
|
||||||
|
if y == 0 { oklch(87%, 0, 0deg) },
|
||||||
|
table-header(
|
||||||
|
[Datum], [Kommen], [Gehen], [Arbeitsart], [Stunden], [Kurzarbeit], [Pause], [Überstunden]
|
||||||
|
),
|
||||||
|
.. for day in days {
|
||||||
|
(
|
||||||
|
[#day.Date],
|
||||||
|
if day.DayParts.len() == 0{
|
||||||
|
table.cell(colspan: 3)[Keine Buchungen]
|
||||||
|
}else if day.DayParts.len() == 1 and 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 {
|
||||||
|
(
|
||||||
|
if Zeit.IsWorkDay{
|
||||||
|
(
|
||||||
|
table.cell()[#Zeit.BookingFrom],
|
||||||
|
table.cell()[#Zeit.BookingTo],
|
||||||
|
table.cell()[#Zeit.WorkType],
|
||||||
|
)
|
||||||
|
}else{
|
||||||
|
(table.cell(colspan: 3)[#Zeit.WorkType],)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
[#day.Worktime],
|
||||||
|
[#day.Kurzarbeit],
|
||||||
|
[#day.Pausetime],
|
||||||
|
[#day.Overtime],
|
||||||
|
)
|
||||||
|
if day.IsFriday {
|
||||||
|
( table.cell(colspan: 8, 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],
|
||||||
|
[Kurzarbeit :], table.cell(align: left)[#meta.Kurzarbeit],
|
||||||
|
[Überstunden :], table.cell(align: left)[#meta.Overtime],
|
||||||
|
[Überstunden lfd. :],table.cell(align: left)[#meta.OvertimeTotal],
|
||||||
|
table.hline(start: 0, end: 2),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func autoLogout(w http.ResponseWriter) {
|
func autoLogout(w http.ResponseWriter) {
|
||||||
users, err := models.GetAllUsers()
|
users, err := models.GetAllUsers()
|
||||||
var logged_out_users []models.User
|
var loggedOutUsers []models.User
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error getting user list %v\n", err)
|
fmt.Printf("Error getting user list %v\n", err)
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func autoLogout(w http.ResponseWriter) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error logging out user %v\n", err)
|
fmt.Printf("Error logging out user %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
logged_out_users = append(logged_out_users, user)
|
loggedOutUsers = append(loggedOutUsers, user)
|
||||||
log.Printf("Automaticaly logged out user %s, %s ", user.Name, user.Vorname)
|
log.Printf("Automaticaly logged out user %s, %s ", user.Name, user.Vorname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,6 @@ func autoLogout(w http.ResponseWriter) {
|
|||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(logged_out_users)
|
json.NewEncoder(w).Encode(loggedOutUsers)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -198,8 +198,9 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
|
|||||||
var markup bytes.Buffer
|
var markup bytes.Buffer
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
|
|
||||||
typstCLI := typst.DockerExec{
|
typstCLI := typst.CLI{
|
||||||
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
|
WorkingDirectory: "/doc/",
|
||||||
|
// 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 {
|
||||||
@@ -225,8 +226,9 @@ func renderPDFSingle(data []typstData) (bytes.Buffer, error) {
|
|||||||
func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) {
|
func renderPDFMulti(data []typstData) ([]bytes.Buffer, error) {
|
||||||
var outputMulti []bytes.Buffer
|
var outputMulti []bytes.Buffer
|
||||||
|
|
||||||
typstRender := typst.DockerExec{
|
typstRender := typst.CLI{
|
||||||
ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
|
WorkingDirectory: "/doc/",
|
||||||
|
// ContainerName: helper.GetEnv("TYPST_CONTAINER", "arbeitszeitmessung-doc-creator"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range data {
|
for _, d := range data {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ func getBookings(w http.ResponseWriter, r *http.Request) {
|
|||||||
if day.Date().Before(lastSub) {
|
if day.Date().Before(lastSub) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, false)
|
aggregatedOvertime += day.GetOvertime(user, models.WorktimeBaseDay, true)
|
||||||
}
|
}
|
||||||
if reportedOvertime, err := user.GetReportedOvertime(); err == nil {
|
if reportedOvertime, err := user.GetReportedOvertime(); err == nil {
|
||||||
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
|
user.Overtime = (reportedOvertime + aggregatedOvertime).Round(time.Minute)
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
var logLevel slog.LevelVar
|
var logLevel slog.LevelVar
|
||||||
logLevel.Set(slog.LevelWarn)
|
switch helper.GetEnv("LOG_LEVEL", "warn") {
|
||||||
|
case "debug":
|
||||||
|
logLevel.Set(slog.LevelDebug)
|
||||||
|
case "info":
|
||||||
|
logLevel.Set(slog.LevelInfo)
|
||||||
|
case "warn":
|
||||||
|
logLevel.Set(slog.LevelWarn)
|
||||||
|
case "error":
|
||||||
|
logLevel.Set(slog.LevelError)
|
||||||
|
}
|
||||||
|
|
||||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &logLevel}))
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &logLevel}))
|
||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (c *CompoundDay) GetWorkDay() WorkDay {
|
|||||||
|
|
||||||
// IsEmpty implements [IWorkDay].
|
// IsEmpty implements [IWorkDay].
|
||||||
func (c *CompoundDay) IsEmpty() bool {
|
func (c *CompoundDay) IsEmpty() bool {
|
||||||
return len(c.DayParts) > 0
|
return len(c.DayParts) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date implements [IWorkDay].
|
// Date implements [IWorkDay].
|
||||||
@@ -47,12 +47,16 @@ func (c *CompoundDay) GetDayProgress(u User) int8 {
|
|||||||
|
|
||||||
// GetOvertime implements [IWorkDay].
|
// GetOvertime implements [IWorkDay].
|
||||||
func (c *CompoundDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
|
func (c *CompoundDay) GetOvertime(u User, base WorktimeBase, includeKurzarbeit bool) time.Duration {
|
||||||
|
work := c.GetWorktime(u, base, includeKurzarbeit)
|
||||||
|
var targetHours time.Duration
|
||||||
|
|
||||||
var overtime time.Duration
|
switch base {
|
||||||
for _, day := range c.DayParts {
|
case WorktimeBaseDay:
|
||||||
overtime += day.GetOvertime(u, base, includeKurzarbeit)
|
targetHours = u.ArbeitszeitProTagFrac(1)
|
||||||
|
case WorktimeBaseWeek:
|
||||||
|
targetHours = u.ArbeitszeitProWocheFrac(.2)
|
||||||
}
|
}
|
||||||
return overtime
|
return (work - targetHours).Round(time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPausetime implements [IWorkDay].
|
// GetPausetime implements [IWorkDay].
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ templ defaultDayComponent(day models.IWorkDay) {
|
|||||||
if pause > 0 {
|
if pause > 0 {
|
||||||
<p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ helper.FormatDuration(pause) }</p>
|
<p class="text-neutral-500 flex flex-row items-center"><span class="icon-[material-symbols-light--motion-photos-paused-outline]"></span>{ helper.FormatDuration(pause) }</p>
|
||||||
}
|
}
|
||||||
if overtime != 0 && day.IsEmpty() == false {
|
if !day.IsEmpty() && overtime != 0 {
|
||||||
<p class="text-neutral-500 flex flex-row items-center">
|
<p class="text-neutral-500 flex flex-row items-center">
|
||||||
<span class="icon-[material-symbols-light--more-time]"></span>
|
<span class="icon-[material-symbols-light--more-time]"></span>
|
||||||
{ helper.FormatDuration(overtime) }
|
{ helper.FormatDuration(overtime) }
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ func defaultDayComponent(day models.IWorkDay) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if overtime != 0 && day.IsEmpty() == false {
|
if !day.IsEmpty() && overtime != 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"text-neutral-500 flex flex-row items-center\"><span class=\"icon-[material-symbols-light--more-time]\"></span> ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<p class=\"text-neutral-500 flex flex-row items-center\"><span class=\"icon-[material-symbols-light--more-time]\"></span> ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ services:
|
|||||||
- ${WEB_PORT}:8080
|
- ${WEB_PORT}:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
- document-creator
|
|
||||||
volumes:
|
volumes:
|
||||||
- ../logs:/app/Backend/logs
|
- ${LOG_PATH}:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
document-creator:
|
# document-creator:
|
||||||
image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
|
# image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
|
||||||
container_name: ${TYPST_CONTAINER}
|
# container_name: ${TYPST_CONTAINER}
|
||||||
restart: unless-stopped
|
# restart: unless-stopped
|
||||||
|
|||||||
Reference in New Issue
Block a user