Compare commits
14 Commits
2.0.0-rc.2
...
41c34c42cf
| Author | SHA1 | Date | |
|---|---|---|---|
| 41c34c42cf | |||
| 6998d07c6b | |||
| a5f5c37225 | |||
| fdda0ea669 | |||
| c10ab98997 | |||
| 8dc8c4eed3 | |||
| 3f49da49b6 | |||
| 18b2cbc074 | |||
| 560c539b19 | |||
| 502955d32f | |||
| cfd77ae28d | |||
| 1daf4db167 | |||
| 3322f7e9bc | |||
| 4b9824c714 |
@@ -6,6 +6,7 @@ on:
|
|||||||
- "*"
|
- "*"
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- dev/main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
webserver:
|
webserver:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,3 +40,5 @@ atlas.hcl
|
|||||||
.scannerwork
|
.scannerwork
|
||||||
Backend/logs
|
Backend/logs
|
||||||
.worktime.txt
|
.worktime.txt
|
||||||
|
|
||||||
|
*_templ.go
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
db
|
db
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
*_templ.go
|
||||||
|
|||||||
@@ -1,24 +1,34 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM --platform=$BUILDPLATFORM golang:alpine AS build
|
FROM --platform=$BUILDPLATFORM golang:alpine AS base
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV CGO_ENABLED=0 \
|
ENV CGO_ENABLED=0 \
|
||||||
GOOS=$TARGETOS \
|
GOOS=$TARGETOS \
|
||||||
GOARCH=$TARGETARCH
|
GOARCH=$TARGETARCH
|
||||||
|
|
||||||
|
FROM base AS fetch-stage
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY go.mod go.sum /app/
|
COPY go.mod go.sum /app/
|
||||||
RUN go mod download && go mod verify
|
RUN go mod download && go mod verify
|
||||||
|
|
||||||
COPY . .
|
FROM ghcr.io/a-h/templ:latest AS generate-stage
|
||||||
|
COPY --chown=65532:65532 . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN ["templ", "generate"]
|
||||||
|
|
||||||
|
FROM base AS build
|
||||||
|
COPY --from=generate-stage /app /app
|
||||||
|
WORKDIR /app
|
||||||
RUN go build -o server .
|
RUN go build -o server .
|
||||||
|
|
||||||
FROM alpine:3.22
|
FROM alpine:3.22
|
||||||
RUN apk add --no-cache tzdata typst
|
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 migrations /app/migrations
|
||||||
|
|
||||||
|
COPY doc /doc
|
||||||
|
|
||||||
COPY /static /app/static
|
COPY /static /app/static
|
||||||
ENTRYPOINT ["./server"]
|
ENTRYPOINT ["./server"]
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import (
|
|||||||
"arbeitszeitmessung/models"
|
"arbeitszeitmessung/models"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,6 +21,45 @@ func OpenDatabase() (models.IDatabase, error) {
|
|||||||
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password")
|
dbPassword := helper.GetEnv("POSTGRES_API_PASS", "password")
|
||||||
dbTz := helper.GetEnv("TZ", "Europe/Berlin")
|
dbTz := helper.GetEnv("TZ", "Europe/Berlin")
|
||||||
|
|
||||||
connStr := fmt.Sprintf("postgres://%s:%s@%s:5432/%s?sslmode=disable&TimeZone=%s", dbUser, dbPassword, dbHost, dbName, dbTz)
|
connStr := fmt.Sprintf(
|
||||||
|
"host=%s user=%s dbname=%s password=%s sslmode=disable TimeZone=%s",
|
||||||
|
dbHost, dbUser, dbName, dbPassword, dbTz)
|
||||||
|
|
||||||
return sql.Open("postgres", connStr)
|
return sql.Open("postgres", connStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Migrate() error {
|
||||||
|
dbHost := helper.GetEnv("POSTGRES_HOST", "localhost")
|
||||||
|
dbName := helper.GetEnv("POSTGRES_DB", "arbeitszeitmessung")
|
||||||
|
dbPassword := helper.GetEnv("POSTGRES_PASSWORD", "password")
|
||||||
|
dbTz := helper.GetEnv("TZ", "Europe/Berlin")
|
||||||
|
|
||||||
|
connStr := fmt.Sprintf(
|
||||||
|
"host=%s user=%s dbname=%s password=%s sslmode=disable TimeZone=%s",
|
||||||
|
dbHost, "migrate", dbName, dbPassword, dbTz)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", connStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := postgres.WithInstance(db, &postgres.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithDatabaseInstance("file:///app/migrations", "postgres", driver)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Connected to database. Running migrations now.")
|
||||||
|
|
||||||
|
// Migrate all the way up ...
|
||||||
|
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Finished migrations starting webserver.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Relevant for arduino inputs -> creates new Booking from get and put method
|
// Relevant for arduino inputs -> creates new Booking from get and put method
|
||||||
@@ -36,6 +37,7 @@ func createBooking(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
booking := (*models.Booking).FromUrlParams(nil, r.URL.Query())
|
booking := (*models.Booking).FromUrlParams(nil, r.URL.Query())
|
||||||
|
booking.Timestamp = time.Now()
|
||||||
if booking.Verify() {
|
if booking.Verify() {
|
||||||
err := booking.Insert()
|
err := booking.Insert()
|
||||||
if errors.Is(models.SameBookingError{}, err) {
|
if errors.Is(models.SameBookingError{}, err) {
|
||||||
|
|||||||
@@ -160,19 +160,13 @@ func updateBooking(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out), 1)
|
newBooking := (*models.Booking).New(nil, user.CardUID, 0, int16(check_in_out), 1)
|
||||||
newBooking.Timestamp = timestamp
|
newBooking.Timestamp = timestamp
|
||||||
|
if newBooking.Verify() {
|
||||||
err = newBooking.InsertWithTimestamp()
|
err = newBooking.InsertWithTimestamp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error inserting booking %v -> %v\n", newBooking, err)
|
log.Printf("Error inserting booking %v -> %v\n", newBooking, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case "change":
|
case "change":
|
||||||
// absenceType, err := strconv.Atoi(r.FormValue("absence"))
|
|
||||||
// if err != nil {
|
|
||||||
// log.Println("Error parsing absence type.", err)
|
|
||||||
// absenceType = 0
|
|
||||||
// }
|
|
||||||
// if absenceType != 0 {
|
|
||||||
// createAbsence(absenceType, user, loc, r)
|
|
||||||
// }
|
|
||||||
for index, possibleBooking := range r.PostForm {
|
for index, possibleBooking := range r.PostForm {
|
||||||
if len(index) > 7 && index[:7] == "booking" {
|
if len(index) > 7 && index[:7] == "booking" {
|
||||||
booking_id, err := strconv.Atoi(index[8:])
|
booking_id, err := strconv.Atoi(index[8:])
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ github.com/Dadido3/go-typst v0.8.0 h1:uTLYprhkrBjwsCXRRuyYUFL0fpYHa2kIYoOB/CGqVN
|
|||||||
github.com/Dadido3/go-typst v0.8.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/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
|
||||||
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
||||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||||
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
||||||
@@ -39,6 +37,8 @@ github.com/golang-migrate/migrate/v4 v4.18.3 h1:EYGkoOsvgHHfm5U/naS1RP/6PL/Xv3S4
|
|||||||
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
github.com/golang-migrate/migrate/v4 v4.18.3/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -78,7 +78,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
|
|||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/wlbr/feiertage v1.10.0/go.mod h1:wJOHvMa6sI5L1FkrTOX/GSoO0hpK3S2YqGLPi8Q84I0=
|
|
||||||
github.com/wlbr/feiertage v1.17.0 h1:AEck/iUQu19iU0xNEoSQTeSTGXF1Ju0tbAwEi/Lmwqk=
|
github.com/wlbr/feiertage v1.17.0 h1:AEck/iUQu19iU0xNEoSQTeSTGXF1Ju0tbAwEi/Lmwqk=
|
||||||
github.com/wlbr/feiertage v1.17.0/go.mod h1:TVZgmSZgGW/jSxexZ56qdlR6cDj+F/FO8bkw8U6kYxM=
|
github.com/wlbr/feiertage v1.17.0/go.mod h1:TVZgmSZgGW/jSxexZ56qdlR6cDj+F/FO8bkw8U6kYxM=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"arbeitszeitmessung/helper"
|
"arbeitszeitmessung/helper"
|
||||||
"arbeitszeitmessung/models"
|
"arbeitszeitmessung/models"
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -44,6 +45,17 @@ func main() {
|
|||||||
models.DB, err = OpenDatabase()
|
models.DB, err = OpenDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error while opening the database", "Error", err)
|
slog.Error("Error while opening the database", "Error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer models.DB.(*sql.DB).Close()
|
||||||
|
|
||||||
|
if helper.GetEnv("GO_ENV", "production") != "debug" {
|
||||||
|
err = Migrate()
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Failed to migrate the database to newest version", "Error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs := http.FileServer(http.Dir("./static"))
|
fs := http.FileServer(http.Dir("./static"))
|
||||||
@@ -70,6 +82,8 @@ func main() {
|
|||||||
|
|
||||||
serverSessionMiddleware := endpoints.Session.LoadAndSave(server)
|
serverSessionMiddleware := endpoints.Session.LoadAndSave(server)
|
||||||
|
|
||||||
|
serverSessionMiddleware = loggingMiddleware(serverSessionMiddleware)
|
||||||
|
|
||||||
// starting the http server
|
// starting the http server
|
||||||
slog.Info("Server is running at http://localhost:8080")
|
slog.Info("Server is running at http://localhost:8080")
|
||||||
slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware))
|
slog.Error("Error starting Server", "Error", http.ListenAndServe(":8080", serverSessionMiddleware))
|
||||||
@@ -79,7 +93,24 @@ func ParamsMiddleware(next http.HandlerFunc) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
queryParams := r.URL.Query()
|
queryParams := r.URL.Query()
|
||||||
ctx := context.WithValue(r.Context(), "urlParams", queryParams)
|
ctx := context.WithValue(r.Context(), "urlParams", queryParams)
|
||||||
|
if len(queryParams) > 0 {
|
||||||
slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams))
|
slog.Debug("ParamsMiddleware added urlParams", slog.Any("urlParams", queryParams))
|
||||||
|
}
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loggingMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Log the method and the requested URL
|
||||||
|
slog.Info("Started request", slog.String("Method", r.Method), slog.String("Path", r.URL.Path))
|
||||||
|
|
||||||
|
// Call the next handler in the chain
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
// Log how long it took
|
||||||
|
slog.Info("Completet Request", slog.String("Time", time.Since(start).String()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE migrate
|
||||||
|
IN SCHEMA public
|
||||||
|
GRANT SELECT ON TABLES TO app_base;
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES FOR ROLE migrate
|
||||||
|
IN SCHEMA public
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCES TO app_base;
|
||||||
|
|
||||||
-- create "abwesenheit" table
|
-- create "abwesenheit" table
|
||||||
CREATE TABLE "abwesenheit" (
|
CREATE TABLE "abwesenheit" (
|
||||||
"counter_id" bigserial NOT NULL,
|
"counter_id" bigserial NOT NULL,
|
||||||
@@ -6,6 +14,7 @@ CREATE TABLE "abwesenheit" (
|
|||||||
"datum" timestamptz NULL DEFAULT (now())::date,
|
"datum" timestamptz NULL DEFAULT (now())::date,
|
||||||
PRIMARY KEY ("counter_id")
|
PRIMARY KEY ("counter_id")
|
||||||
);
|
);
|
||||||
|
|
||||||
-- create "anwesenheit" table
|
-- create "anwesenheit" table
|
||||||
CREATE TABLE "anwesenheit" (
|
CREATE TABLE "anwesenheit" (
|
||||||
"counter_id" bigserial NOT NULL,
|
"counter_id" bigserial NOT NULL,
|
||||||
@@ -55,3 +64,6 @@ CREATE TABLE "wochen_report" (
|
|||||||
PRIMARY KEY ("id"),
|
PRIMARY KEY ("id"),
|
||||||
CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start")
|
CONSTRAINT "wochen_report_personal_nummer_woche_start_key" UNIQUE ("personal_nummer", "woche_start")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
GRANT INSERT, UPDATE ON abwesenheit, anwesenheit, wochen_report, user_password TO app_base;
|
||||||
|
GRANT DELETE ON abwesenheit to app_base;
|
||||||
@@ -3,8 +3,3 @@
|
|||||||
DROP FUNCTION update_zuletzt_geandert;
|
DROP FUNCTION update_zuletzt_geandert;
|
||||||
|
|
||||||
DROP TRIGGER IF EXISTS pass_hash_update ON user_password;
|
DROP TRIGGER IF EXISTS pass_hash_update ON user_password;
|
||||||
|
|
||||||
|
|
||||||
-- revert: Adds crypto extension
|
|
||||||
|
|
||||||
DROP EXTENSION IF EXISTS pgcrypto;
|
|
||||||
@@ -17,5 +17,3 @@ FOR EACH ROW
|
|||||||
EXECUTE FUNCTION update_zuletzt_geandert();
|
EXECUTE FUNCTION update_zuletzt_geandert();
|
||||||
|
|
||||||
-- Adds crypto extension
|
-- Adds crypto extension
|
||||||
|
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
||||||
@@ -11,3 +11,5 @@ CREATE TABLE "s_feiertage" (
|
|||||||
);
|
);
|
||||||
-- create index "feiertage_unique_pro_jahr" to table: "s_feiertage"
|
-- create index "feiertage_unique_pro_jahr" to table: "s_feiertage"
|
||||||
CREATE UNIQUE INDEX "feiertage_unique_pro_jahr" ON "s_feiertage" ((EXTRACT(year FROM datum)), "name");
|
CREATE UNIQUE INDEX "feiertage_unique_pro_jahr" ON "s_feiertage" ((EXTRACT(year FROM datum)), "name");
|
||||||
|
|
||||||
|
GRANT INSERT, UPDATE ON s_feiertage TO app_base;
|
||||||
@@ -128,6 +128,9 @@ func (b *Booking) InsertWithTimestamp() error {
|
|||||||
if b.Timestamp.IsZero() {
|
if b.Timestamp.IsZero() {
|
||||||
return b.Insert()
|
return b.Insert()
|
||||||
}
|
}
|
||||||
|
if !checkLastBooking(*b) {
|
||||||
|
return SameBookingError{}
|
||||||
|
}
|
||||||
stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`))
|
stmt, err := DB.Prepare((`INSERT INTO anwesenheit (card_uid, geraet_id, check_in_out, anwesenheit_typ, timestamp) VALUES ($1, $2, $3, $4, $5) RETURNING counter_id`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -242,12 +245,13 @@ func (b *Booking) Update(nb Booking) {
|
|||||||
|
|
||||||
func checkLastBooking(b Booking) bool {
|
func checkLastBooking(b Booking) bool {
|
||||||
var check_in_out int
|
var check_in_out int
|
||||||
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 ORDER BY "timestamp" DESC LIMIT 1;`))
|
slog.Info("Checking with timestamp:", "timestamp", b.Timestamp.String())
|
||||||
|
stmt, err := DB.Prepare((`SELECT check_in_out FROM "anwesenheit" WHERE "card_uid" = $1 AND "timestamp"::DATE <= $2::DATE ORDER BY "timestamp" DESC LIMIT 1;`))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error preparing query: %v", err)
|
log.Fatalf("Error preparing query: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
err = stmt.QueryRow(b.CardUID).Scan(&check_in_out)
|
err = stmt.QueryRow(b.CardUID, b.Timestamp).Scan(&check_in_out)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,19 +49,16 @@ func GetDays(user User, tsFrom, tsTo time.Time, orderedForward bool) []IWorkDay
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, absentDay := range absences {
|
for _, absentDay := range absences {
|
||||||
// Kurzarbeit should be integrated in workday
|
|
||||||
|
// Check if there is already a day
|
||||||
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
|
existingDay, ok := allDays[absentDay.Date().Format(time.DateOnly)]
|
||||||
if !ok {
|
|
||||||
allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch {
|
switch {
|
||||||
case absentDay.AbwesenheitTyp.WorkTime < 0:
|
case absentDay.AbwesenheitTyp.WorkTime < 0:
|
||||||
if workDay, ok := allDays[absentDay.Date().Format(time.DateOnly)].(*WorkDay); ok {
|
if workDay, ok := allDays[absentDay.Date().Format(time.DateOnly)].(*WorkDay); ok {
|
||||||
workDay.kurzArbeit = true
|
workDay.kurzArbeit = true
|
||||||
workDay.kurzArbeitAbsence = absentDay
|
workDay.kurzArbeitAbsence = absentDay
|
||||||
}
|
}
|
||||||
case !existingDay.IsEmpty():
|
case ok && !existingDay.IsEmpty():
|
||||||
allDays[absentDay.Date().Format(time.DateOnly)] = NewCompondDay(absentDay.Date(), existingDay, &absentDay)
|
allDays[absentDay.Date().Format(time.DateOnly)] = NewCompondDay(absentDay.Date(), existingDay, &absentDay)
|
||||||
default:
|
default:
|
||||||
allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay
|
allDays[absentDay.Date().Format(time.DateOnly)] = &absentDay
|
||||||
|
|||||||
@@ -175,21 +175,38 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
|
|||||||
qStr, err := DB.Prepare(`
|
qStr, err := DB.Prepare(`
|
||||||
WITH all_days AS (
|
WITH all_days AS (
|
||||||
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
|
SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
|
||||||
ordered_bookings AS (
|
normalized_bookings AS (
|
||||||
|
SELECT *
|
||||||
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
a.timestamp::DATE AS work_date,
|
a.card_uid,
|
||||||
a.timestamp,
|
a.timestamp,
|
||||||
|
a.timestamp::DATE AS work_date,
|
||||||
a.check_in_out,
|
a.check_in_out,
|
||||||
a.counter_id,
|
a.counter_id,
|
||||||
a.anwesenheit_typ,
|
a.anwesenheit_typ,
|
||||||
sat.anwesenheit_name AS anwesenheit_typ_name,
|
sat.anwesenheit_name AS anwesenheit_typ_name,
|
||||||
LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp,
|
LAG(a.check_in_out) OVER (
|
||||||
LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check
|
PARTITION BY a.card_uid, a.timestamp::DATE
|
||||||
|
ORDER BY a.timestamp
|
||||||
|
) AS prev_check
|
||||||
FROM anwesenheit a
|
FROM anwesenheit a
|
||||||
LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
|
LEFT JOIN s_anwesenheit_typen sat
|
||||||
|
ON a.anwesenheit_typ = sat.anwesenheit_id
|
||||||
WHERE a.card_uid = $1
|
WHERE a.card_uid = $1
|
||||||
AND a.timestamp::DATE >= $2
|
AND a.timestamp::DATE >= $2
|
||||||
AND a.timestamp::DATE <= $3
|
AND a.timestamp::DATE <= $3
|
||||||
|
) t
|
||||||
|
WHERE prev_check IS NULL OR prev_check <> check_in_out
|
||||||
|
),
|
||||||
|
ordered_bookings AS (
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
LAG(timestamp) OVER (
|
||||||
|
PARTITION BY card_uid, work_date
|
||||||
|
ORDER BY timestamp
|
||||||
|
) AS prev_timestamp
|
||||||
|
FROM normalized_bookings
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
d.work_date,
|
d.work_date,
|
||||||
@@ -228,6 +245,62 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
|
|||||||
GROUP BY d.work_date
|
GROUP BY d.work_date
|
||||||
ORDER BY d.work_date ASC;`)
|
ORDER BY d.work_date ASC;`)
|
||||||
|
|
||||||
|
// qStr, err := DB.Prepare(`
|
||||||
|
// WITH all_days AS (
|
||||||
|
// SELECT generate_series($2::DATE, $3::DATE - INTERVAL '1 day', INTERVAL '1 day')::DATE AS work_date),
|
||||||
|
// ordered_bookings AS (
|
||||||
|
// SELECT
|
||||||
|
// a.timestamp::DATE AS work_date,
|
||||||
|
// a.timestamp,
|
||||||
|
// a.check_in_out,
|
||||||
|
// a.counter_id,
|
||||||
|
// a.anwesenheit_typ,
|
||||||
|
// sat.anwesenheit_name AS anwesenheit_typ_name,
|
||||||
|
// LAG(a.timestamp) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_timestamp,
|
||||||
|
// LAG(a.check_in_out) OVER (PARTITION BY a.card_uid, a.timestamp::DATE ORDER BY a.timestamp) AS prev_check
|
||||||
|
// FROM anwesenheit a
|
||||||
|
// LEFT JOIN s_anwesenheit_typen sat ON a.anwesenheit_typ = sat.anwesenheit_id
|
||||||
|
// WHERE a.card_uid = $1
|
||||||
|
// AND a.timestamp::DATE >= $2
|
||||||
|
// AND a.timestamp::DATE <= $3
|
||||||
|
// )
|
||||||
|
// SELECT
|
||||||
|
// d.work_date,
|
||||||
|
// COALESCE(MIN(b.timestamp), NOW()) AS time_from,
|
||||||
|
// COALESCE(MAX(b.timestamp), NOW()) AS time_to,
|
||||||
|
// COALESCE(
|
||||||
|
// EXTRACT(EPOCH FROM SUM(
|
||||||
|
// CASE
|
||||||
|
// WHEN b.prev_check IN (1, 3) AND b.check_in_out IN (2, 4, 254)
|
||||||
|
// THEN b.timestamp - b.prev_timestamp
|
||||||
|
// ELSE INTERVAL '0'
|
||||||
|
// END
|
||||||
|
// )), 0
|
||||||
|
// ) AS total_work_seconds,
|
||||||
|
// COALESCE(
|
||||||
|
// EXTRACT(EPOCH FROM SUM(
|
||||||
|
// CASE
|
||||||
|
// WHEN b.prev_check IN (2, 4, 254) AND b.check_in_out IN (1, 3)
|
||||||
|
// THEN b.timestamp - b.prev_timestamp
|
||||||
|
// ELSE INTERVAL '0'
|
||||||
|
// END
|
||||||
|
// )), 0
|
||||||
|
// ) AS total_pause_seconds,
|
||||||
|
// COALESCE(jsonb_agg(jsonb_build_object(
|
||||||
|
// 'check_in_out', b.check_in_out,
|
||||||
|
// 'timestamp', b.timestamp,
|
||||||
|
// 'counter_id', b.counter_id,
|
||||||
|
// 'anwesenheit_typ', b.anwesenheit_typ,
|
||||||
|
// 'anwesenheit_typ', jsonb_build_object(
|
||||||
|
// 'anwesenheit_id', b.anwesenheit_typ,
|
||||||
|
// 'anwesenheit_name', b.anwesenheit_typ_name
|
||||||
|
// )
|
||||||
|
// ) ORDER BY b.timestamp), '[]'::jsonb) AS bookings
|
||||||
|
// FROM all_days d
|
||||||
|
// LEFT JOIN ordered_bookings b ON d.work_date = b.work_date
|
||||||
|
// GROUP BY d.work_date
|
||||||
|
// ORDER BY d.work_date ASC;`)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error preparing SQL statement", err)
|
log.Println("Error preparing SQL statement", err)
|
||||||
return workDays
|
return workDays
|
||||||
@@ -258,7 +331,7 @@ func GetWorkDays(user User, tsFrom, tsTo time.Time) []WorkDay {
|
|||||||
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
|
if len(workDay.Bookings) == 1 && workDay.Bookings[0].CounterId == 0 {
|
||||||
workDay.Bookings = []Booking{}
|
workDay.Bookings = []Booking{}
|
||||||
}
|
}
|
||||||
if len(workDay.Bookings) > 1 || !helper.IsWeekend(workDay.Date()) {
|
if len(workDay.Bookings) >= 1 || !helper.IsWeekend(workDay.Date()) {
|
||||||
workDays = append(workDays, workDay)
|
workDays = append(workDays, workDay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,14 +78,8 @@
|
|||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
border-color: var(--color-neutral-800);
|
border-color: var(--color-neutral-800);
|
||||||
transition-property:
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
color, background-color, border-color, outline-color,
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
|
||||||
text-decoration-color, fill, stroke, --tw-gradient-from,
|
|
||||||
--tw-gradient-via, --tw-gradient-to;
|
|
||||||
transition-timing-function: var(
|
|
||||||
--tw-ease,
|
|
||||||
var(--default-transition-timing-function)
|
|
||||||
);
|
|
||||||
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
transition-duration: var(--tw-duration, var(--default-transition-duration));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -205,45 +205,24 @@
|
|||||||
.top-0 {
|
.top-0 {
|
||||||
top: calc(var(--spacing) * 0);
|
top: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
.top-1 {
|
|
||||||
top: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.top-1\/2 {
|
.top-1\/2 {
|
||||||
top: calc(1/2 * 100%);
|
top: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
.top-2 {
|
|
||||||
top: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.top-2\.5 {
|
.top-2\.5 {
|
||||||
top: calc(var(--spacing) * 2.5);
|
top: calc(var(--spacing) * 2.5);
|
||||||
}
|
}
|
||||||
.top-25 {
|
|
||||||
top: calc(var(--spacing) * 25);
|
|
||||||
}
|
|
||||||
.top-26 {
|
|
||||||
top: calc(var(--spacing) * 26);
|
|
||||||
}
|
|
||||||
.top-\[0\.125rem\] {
|
.top-\[0\.125rem\] {
|
||||||
top: 0.125rem;
|
top: 0.125rem;
|
||||||
}
|
}
|
||||||
.right-1 {
|
.right-1 {
|
||||||
right: calc(var(--spacing) * 1);
|
right: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
.right-2 {
|
|
||||||
right: calc(var(--spacing) * 2);
|
|
||||||
}
|
|
||||||
.right-2\.5 {
|
.right-2\.5 {
|
||||||
right: calc(var(--spacing) * 2.5);
|
right: calc(var(--spacing) * 2.5);
|
||||||
}
|
}
|
||||||
.left-1 {
|
|
||||||
left: calc(var(--spacing) * 1);
|
|
||||||
}
|
|
||||||
.left-1\/2 {
|
.left-1\/2 {
|
||||||
left: calc(1/2 * 100%);
|
left: calc(1/2 * 100%);
|
||||||
}
|
}
|
||||||
.z-10 {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.z-100 {
|
.z-100 {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
@@ -404,9 +383,6 @@
|
|||||||
.h-2 {
|
.h-2 {
|
||||||
height: calc(var(--spacing) * 2);
|
height: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.h-3 {
|
|
||||||
height: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.h-3\.5 {
|
.h-3\.5 {
|
||||||
height: calc(var(--spacing) * 3.5);
|
height: calc(var(--spacing) * 3.5);
|
||||||
}
|
}
|
||||||
@@ -431,9 +407,6 @@
|
|||||||
.w-2 {
|
.w-2 {
|
||||||
width: calc(var(--spacing) * 2);
|
width: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
.w-3 {
|
|
||||||
width: calc(var(--spacing) * 3);
|
|
||||||
}
|
|
||||||
.w-3\.5 {
|
.w-3\.5 {
|
||||||
width: calc(var(--spacing) * 3.5);
|
width: calc(var(--spacing) * 3.5);
|
||||||
}
|
}
|
||||||
@@ -443,9 +416,6 @@
|
|||||||
.w-5 {
|
.w-5 {
|
||||||
width: calc(var(--spacing) * 5);
|
width: calc(var(--spacing) * 5);
|
||||||
}
|
}
|
||||||
.w-9 {
|
|
||||||
width: calc(var(--spacing) * 9);
|
|
||||||
}
|
|
||||||
.w-9\/10 {
|
.w-9\/10 {
|
||||||
width: calc(9/10 * 100%);
|
width: calc(9/10 * 100%);
|
||||||
}
|
}
|
||||||
@@ -458,9 +428,6 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.flex-shrink {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
.flex-shrink-0 {
|
.flex-shrink-0 {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -476,21 +443,10 @@
|
|||||||
.basis-\[content\] {
|
.basis-\[content\] {
|
||||||
flex-basis: content;
|
flex-basis: content;
|
||||||
}
|
}
|
||||||
.border-collapse {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
.-translate-x-1 {
|
|
||||||
--tw-translate-x: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-x-1\/2 {
|
.-translate-x-1\/2 {
|
||||||
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
}
|
}
|
||||||
.-translate-y-1 {
|
|
||||||
--tw-translate-y: calc(var(--spacing) * -1);
|
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
|
||||||
}
|
|
||||||
.-translate-y-1\/2 {
|
.-translate-y-1\/2 {
|
||||||
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
|
||||||
translate: var(--tw-translate-x) var(--tw-translate-y);
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
@@ -501,9 +457,6 @@
|
|||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.resize {
|
|
||||||
resize: both;
|
|
||||||
}
|
|
||||||
.scroll-m-2 {
|
.scroll-m-2 {
|
||||||
scroll-margin: calc(var(--spacing) * 2);
|
scroll-margin: calc(var(--spacing) * 2);
|
||||||
}
|
}
|
||||||
@@ -661,9 +614,6 @@
|
|||||||
.bg-red-600 {
|
.bg-red-600 {
|
||||||
background-color: var(--color-red-600);
|
background-color: var(--color-red-600);
|
||||||
}
|
}
|
||||||
.mask-repeat {
|
|
||||||
mask-repeat: repeat;
|
|
||||||
}
|
|
||||||
.p-1 {
|
.p-1 {
|
||||||
padding: calc(var(--spacing) * 1);
|
padding: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
@@ -740,16 +690,9 @@
|
|||||||
.uppercase {
|
.uppercase {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.underline {
|
|
||||||
text-decoration-line: underline;
|
|
||||||
}
|
|
||||||
.opacity-0 {
|
.opacity-0 {
|
||||||
opacity: 0%;
|
opacity: 0%;
|
||||||
}
|
}
|
||||||
.outline {
|
|
||||||
outline-style: var(--tw-outline-style);
|
|
||||||
outline-width: 1px;
|
|
||||||
}
|
|
||||||
.filter {
|
.filter {
|
||||||
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
|
||||||
}
|
}
|
||||||
@@ -1195,11 +1138,6 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
@property --tw-outline-style {
|
|
||||||
syntax: "*";
|
|
||||||
inherits: false;
|
|
||||||
initial-value: solid;
|
|
||||||
}
|
|
||||||
@property --tw-blur {
|
@property --tw-blur {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -1272,7 +1210,6 @@
|
|||||||
--tw-border-style: solid;
|
--tw-border-style: solid;
|
||||||
--tw-divide-y-reverse: 0;
|
--tw-divide-y-reverse: 0;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
--tw-outline-style: solid;
|
|
||||||
--tw-blur: initial;
|
--tw-blur: initial;
|
||||||
--tw-brightness: initial;
|
--tw-brightness: initial;
|
||||||
--tw-contrast: initial;
|
--tw-contrast: initial;
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ function checkAll(pattern, state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bookingForms = document.querySelectorAll("form.bookings");
|
const bookingForms = document.querySelectorAll("form.bookings");
|
||||||
for (form of bookingForms) {
|
for (let form of bookingForms) {
|
||||||
let selectKommenInput = form.querySelector("input[name='select_kommen']");
|
let selectKommenInput = form.querySelector("input[name='select_kommen']");
|
||||||
let kommenGehenSelector = form.querySelector("select");
|
let kommenGehenSelector = form.querySelector("select");
|
||||||
if (selectKommenInput) {
|
if (selectKommenInput) {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
|
templ TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) {
|
||||||
@Base()
|
@Base()
|
||||||
@headerComponent()
|
@headerComponent()
|
||||||
<div class="grid-main divide-y-1">
|
<div class="grid-main divide-y-1 @container">
|
||||||
<div class="grid-sub lg:divide-x-1 max-md:divide-y-1 responsive @container">
|
<div class="grid-sub lg:divide-x-1 max-md:divide-y-1 responsive">
|
||||||
<div class="grid-cell col-span-full bg-neutral-300 lg:border-0">
|
<div class="grid-cell col-span-full bg-neutral-300 lg:border-0">
|
||||||
<h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2>
|
<h2 class="text-xl uppercase font-bold">Eigene Abrechnung</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@ templ workWeekComponent(week models.WorkWeek, onlyAccept bool) {
|
|||||||
year, kw := week.WeekStart.ISOWeek()
|
year, kw := week.WeekStart.ISOWeek()
|
||||||
progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
|
progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
|
||||||
}}
|
}}
|
||||||
<div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container">
|
<div class="employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1">
|
||||||
<div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2">
|
<div class="grid-cell flex flex-col max-md:bg-neutral-300 gap-2">
|
||||||
if !onlyAccept {
|
if !onlyAccept {
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func TeamPage(weeks []models.WorkWeek, userWeek models.WorkWeek) templ.Component
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid-main divide-y-1\"><div class=\"grid-sub lg:divide-x-1 max-md:divide-y-1 responsive @container\"><div class=\"grid-cell col-span-full bg-neutral-300 lg:border-0\"><h2 class=\"text-xl uppercase font-bold\">Eigene Abrechnung</h2></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"grid-main divide-y-1 @container\"><div class=\"grid-sub lg:divide-x-1 max-md:divide-y-1 responsive\"><div class=\"grid-cell col-span-full bg-neutral-300 lg:border-0\"><h2 class=\"text-xl uppercase font-bold\">Eigene Abrechnung</h2></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ func workWeekComponent(week models.WorkWeek, onlyAccept bool) templ.Component {
|
|||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
year, kw := week.WeekStart.ISOWeek()
|
year, kw := week.WeekStart.ISOWeek()
|
||||||
progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
|
progress := (float32(week.WorktimeVirtual.Hours()) / week.User.ArbeitszeitPerWoche) * 100
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1 @container\"><div class=\"grid-cell flex flex-col max-md:bg-neutral-300 gap-2\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"employeComponent grid-sub responsive lg:divide-x-1 max-md:divide-y-1\"><div class=\"grid-cell flex flex-col max-md:bg-neutral-300 gap-2\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
6
Cron/autoBackup.sh
Executable file
6
Cron/autoBackup.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
# cron-timing: 05 01 * * 1
|
||||||
|
container_name="arbeitszeitmessung-main-db-1"
|
||||||
|
filename=backup-$(date '+%d%m%Y').sql
|
||||||
|
database_name=__DATABASE__
|
||||||
|
docker exec $container_name pg_dump $database_name > /home/pi/arbeitszeitmessung-backup/$filename
|
||||||
|
echo "created backup file: "$filename
|
||||||
3
Cron/autoHolidays.sh
Executable file
3
Cron/autoHolidays.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
# Calls endpoint to write all public Holidays for the current year inside a database.
|
||||||
|
port=__PORT__
|
||||||
|
curl localhost:$port/auto/feiertage
|
||||||
4
Cron/autoLogout.sh
Executable file
4
Cron/autoLogout.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
# cron-timing: 55 23 * * *
|
||||||
|
# Calls endpoint to log out all users, still logged in for today
|
||||||
|
port=__PORT__
|
||||||
|
curl localhost:$port/auto/logout
|
||||||
56
DB/initdb/01_create_user.sh
Executable file
56
DB/initdb/01_create_user.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD';
|
||||||
|
GRANT USAGE, CREATE ON SCHEMA public TO migrate;
|
||||||
|
GRANT CONNECT ON DATABASE arbeitszeitmessung TO migrate;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
# psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
|
||||||
|
# GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER;
|
||||||
|
# GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER;
|
||||||
|
# GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage TO $POSTGRES_API_USER;
|
||||||
|
# GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER;
|
||||||
|
# EOSQL
|
||||||
|
|
||||||
|
echo "User creation and permissions setup complete!"
|
||||||
|
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
|
||||||
|
-- privilege roles
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_base') THEN
|
||||||
|
CREATE ROLE app_base NOLOGIN;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
-- dynamic login role
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_API_USER') THEN
|
||||||
|
CREATE ROLE $POSTGRES_API_USER
|
||||||
|
LOGIN
|
||||||
|
ENCRYPTED PASSWORD '$POSTGRES_API_PASS';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
-- grant base privileges
|
||||||
|
GRANT app_base TO $POSTGRES_API_USER;
|
||||||
|
GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER;
|
||||||
|
GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER;
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
INSERT INTO "s_personal_daten" ("personal_nummer", "aktiv_beschaeftigt", "vorname", "nachname", "geburtsdatum", "plz", "adresse", "geschlecht", "card_uid", "hauptbeschaeftigungs_ort", "arbeitszeit_per_tag", "arbeitszeit_per_woche", "arbeitszeit_min_start", "arbeitszeit_max_ende", "vorgesetzter_pers_nr") VALUES
|
|
||||||
(123, 't', 'Kim', 'Mustermensch', '2003-02-01', '08963', 'Altenburger Str. 44A', 1, 'aaaa-aaaa', 1, 8, 40, '07:00:00', '20:00:00', 0);
|
|
||||||
|
|
||||||
INSERT INTO "user_password" ("personal_nummer", "pass_hash") VALUES
|
|
||||||
(123, crypt('max_pass', gen_salt('bf')));
|
|
||||||
|
|
||||||
INSERT INTO "s_anwesenheit_typen" ("anwesenheit_id", "anwesenheit_name") VALUES (1, 'Büro');
|
|
||||||
INSERT INTO "s_abwesenheit_typen" ("abwesenheit_id", "abwesenheit_name", "arbeitszeit_equivalent") VALUES (1, 'Urlaub', 100), (2, 'Krank', 100), (3, 'Kurzarbeit', -1), (4, 'Urlaub untertags', 50);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -e # Exit on error
|
|
||||||
|
|
||||||
echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER"
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
CREATE USER $POSTGRES_API_USER WITH ENCRYPTED PASSWORD '$POSTGRES_API_PASS';
|
|
||||||
EOSQL
|
|
||||||
|
|
||||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
|
||||||
GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER;
|
|
||||||
GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER;
|
|
||||||
GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER;
|
|
||||||
GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER;
|
|
||||||
GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage TO $POSTGRES_API_USER;
|
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER;
|
|
||||||
EOSQL
|
|
||||||
|
|
||||||
echo "User creation and permissions setup complete!"
|
|
||||||
|
|
||||||
# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung
|
|
||||||
56
DBB/initdb/01_create_user.sh
Executable file
56
DBB/initdb/01_create_user.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
echo "Creating PostgreSQL user and setting permissions... $POSTGRES_USER for API user $POSTGRES_API_USER"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
CREATE ROLE migrate LOGIN ENCRYPTED PASSWORD '$POSTGRES_PASSWORD';
|
||||||
|
GRANT USAGE, CREATE ON SCHEMA public TO migrate;
|
||||||
|
GRANT CONNECT ON DATABASE arbeitszeitmessung TO migrate;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
# psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
|
||||||
|
# GRANT SELECT, INSERT, UPDATE ON anwesenheit, abwesenheit, user_password, wochen_report, s_feiertage TO $POSTGRES_API_USER;
|
||||||
|
# GRANT DELETE ON abwesenheit TO $POSTGRES_API_USER;
|
||||||
|
# GRANT SELECT ON s_personal_daten, s_abwesenheit_typen, s_anwesenheit_typen, s_feiertage TO $POSTGRES_API_USER;
|
||||||
|
# GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $POSTGRES_API_USER;
|
||||||
|
# EOSQL
|
||||||
|
|
||||||
|
echo "User creation and permissions setup complete!"
|
||||||
|
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
|
||||||
|
|
||||||
|
-- privilege roles
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_base') THEN
|
||||||
|
CREATE ROLE app_base NOLOGIN;
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
-- dynamic login role
|
||||||
|
DO \$\$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_API_USER') THEN
|
||||||
|
CREATE ROLE $POSTGRES_API_USER
|
||||||
|
LOGIN
|
||||||
|
ENCRYPTED PASSWORD '$POSTGRES_API_PASS';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
\$\$;
|
||||||
|
|
||||||
|
-- grant base privileges
|
||||||
|
GRANT app_base TO $POSTGRES_API_USER;
|
||||||
|
GRANT CONNECT ON DATABASE $POSTGRES_DB TO $POSTGRES_API_USER;
|
||||||
|
GRANT USAGE ON SCHEMA public TO $POSTGRES_API_USER;
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
# psql -v ON_ERROR_STOP=1 --username root --dbname arbeitszeitmessung
|
||||||
@@ -3,7 +3,7 @@ services:
|
|||||||
db:
|
db:
|
||||||
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
|
- 5432:5432
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,14 @@ services:
|
|||||||
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
|
- ${POSTGRES_PATH}/initdb:/docker-entrypoint-initdb.d
|
||||||
ports:
|
ports:
|
||||||
- ${POSTGRES_PORT}:5432
|
- ${POSTGRES_PORT}:5432
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready", "-U", "${POSTGRES_USER}", "--dbname", "${POSTGRES_DB}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: git.letsstein.de/tom/arbeitszeitmessung-webserver
|
image: git.letsstein.de/tom/arbeitszeitmessung-webserver:dev
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -24,12 +29,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- ${WEB_PORT}:8080
|
- ${WEB_PORT}:8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
db:
|
||||||
|
condition: service_healthy
|
||||||
volumes:
|
volumes:
|
||||||
- ${LOG_PATH}:/app/logs
|
- ${LOG_PATH}:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# document-creator:
|
|
||||||
# image: git.letsstein.de/tom/arbeitszeitmessung-doc-creator
|
|
||||||
# container_name: ${TYPST_CONTAINER}
|
|
||||||
# restart: unless-stopped
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ POSTGRES_USER=root # Postgres ADMIN Nutzername
|
|||||||
POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort
|
POSTGRES_PASSWORD=very_secure # Postgres ADMIN Passwort
|
||||||
POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung)
|
POSTGRES_API_USER=api_nutzer # Postgres API Nutzername (für Arbeitszeitmessung)
|
||||||
POSTGRES_API_PASS=password # Postgres API Passwort (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_PATH=__ROOT__/DB # Datebank Pfad (relativ zu Docker Ordner oder absoluter pfad mit /...)
|
||||||
LOG_PATH=../logs # Pfad für Logdatein
|
|
||||||
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
|
POSTGRES_DB=arbeitszeitmessung # Postgres Datenbank Name
|
||||||
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port will not be exposed by default. regex:^[0-9]{1,5}$
|
POSTGRES_PORT=127.0.0.1:5432 # Postgres Port normalerweise nicht freigegeben. regex:^[0-9]{1,5}$
|
||||||
TZ=Europe/Berlin # Zeitzone
|
TZ=Europe/Berlin # Zeitzone
|
||||||
API_TOKEN=dont_access # API Token für ESP Endpoints
|
API_TOKEN=dont_access # API Token für ESP Endpoints
|
||||||
WEB_PORT=8000 # Port from which Arbeitszeitmessung should be accessable regex:^[0-9]{1,5}$
|
WEB_PORT=8000 # Port unter welchem Webserver erreichbar ist. regex:^[0-9]{1,5}$
|
||||||
TYPST_CONTAINER=arbeitszeitmessung-doc-creator # Name of the pdf compiler container
|
LOG_PATH=__ROOT__/logs # Pfad für Audit Logs
|
||||||
|
LOG_LEVEL=warn # Welche Log-Nachrichten werden in der Konsole erscheinen
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
FROM ghcr.io/typst/typst:0.14.0
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY ./templates /app/templates
|
|
||||||
COPY ./static /app/static
|
|
||||||
|
|
||||||
ENTRYPOINT ["sh", "-c", "while true; do sleep 3600; done"]
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,97 +0,0 @@
|
|||||||
#let table-header(..headers) = {
|
|
||||||
table.header(
|
|
||||||
..headers.pos().map(h => strong(h))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#let abrechnung(meta, days) = {
|
|
||||||
set page(paper: "a4", margin: (x:1.5cm, y:2.25cm),
|
|
||||||
footer:[#grid(
|
|
||||||
columns: (3fr, .65fr),
|
|
||||||
align: left + horizon,
|
|
||||||
inset: .5em,
|
|
||||||
[#meta.EmployeeName -- #meta.TimeRange], grid.cell(rowspan: 2)[#image("/static/logo.png")],
|
|
||||||
[Arbeitszeitrechnung maschinell erstellt am #meta.CurrentTimestamp],
|
|
||||||
)
|
|
||||||
])
|
|
||||||
set text(font: "Noto Sans", size:10pt, fill: luma(10%))
|
|
||||||
set table(
|
|
||||||
stroke: 0.5pt + luma(10%),
|
|
||||||
inset: .5em,
|
|
||||||
align: center + horizon,
|
|
||||||
)
|
|
||||||
show text: it => {
|
|
||||||
if it.text == "0min"{
|
|
||||||
text(oklch(70.8%, 0, 0deg))[#it]
|
|
||||||
}else if it.text.starts-with("-"){
|
|
||||||
text(red)[#it]
|
|
||||||
}else{
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[= Abrechnung Arbeitszeit -- #meta.EmployeeName]
|
|
||||||
|
|
||||||
[Zeitraum: #meta.TimeRange]
|
|
||||||
|
|
||||||
table(
|
|
||||||
columns: (1fr, 1fr, 1fr, 1fr, 1fr, 1fr, .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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
66
Jenkinsfile
vendored
66
Jenkinsfile
vendored
@@ -1,66 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
environment {
|
|
||||||
DOCKER_USERNAME = 'jenkins'
|
|
||||||
DOCKER_PASSWORD = credentials('gitea_jenkins')
|
|
||||||
SONAR_TOKEN = credentials('sonarcube_token')
|
|
||||||
POSTGRES_USER = 'postgres'
|
|
||||||
POSTGRES_PASSWORD = 'password'
|
|
||||||
POSTGRES_DB = 'arbeitszeitmessung'
|
|
||||||
}
|
|
||||||
|
|
||||||
agent any
|
|
||||||
|
|
||||||
stages {
|
|
||||||
stage('Test') {
|
|
||||||
agent {
|
|
||||||
docker {
|
|
||||||
image ''
|
|
||||||
args ''
|
|
||||||
args ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
sh '''
|
|
||||||
docker run -d --rm \
|
|
||||||
--name test-db \
|
|
||||||
-e POSTGRES_USER={$POSTGRES_USER} \
|
|
||||||
-e POSTGRES_PASSWORD={$POSTGRES_PASSWORD} \
|
|
||||||
-e POSTGRES_DB={$POSTGRES_DB} \
|
|
||||||
-v ./DB/initdb:/docker-entrypoint-initdb.d\
|
|
||||||
-p "5432:5432" \
|
|
||||||
postgres:16
|
|
||||||
'''
|
|
||||||
// docker.image('golang:1.24.5').withRun(
|
|
||||||
// '-u root:root --network=host'
|
|
||||||
// ) { go ->
|
|
||||||
// // wait for DB to start
|
|
||||||
// sh '''
|
|
||||||
// cd Backend \
|
|
||||||
// go mod download && go mod tidy \
|
|
||||||
// go test ./... -v
|
|
||||||
|
|
||||||
// '''
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('SonarCube Analysis') {
|
|
||||||
steps {
|
|
||||||
sh 'make scan'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stage('Building image arbeitszeit-backend') {
|
|
||||||
when {
|
|
||||||
anyOf {
|
|
||||||
changeset 'Jenkinsfile'
|
|
||||||
changeset 'Makefile'
|
|
||||||
changeset 'Backend/**'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh 'make backend'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
Makefile
2
Makefile
@@ -44,7 +44,7 @@ generateFrontend:
|
|||||||
|
|
||||||
|
|
||||||
backend: generateFrontend login_registry
|
backend: generateFrontend login_registry
|
||||||
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:latest Backend --push
|
docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung-webserver:dev Backend --push
|
||||||
# docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push
|
# docker buildx build --platform linux/amd64,linux/arm64 -t ${IMAGE_REGISTRY}/${PACKAGE_OWNER}/arbeitszeitmessung:${GIT_COMMIT} Backend //--push
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
|||||||
26
Readme.md
26
Readme.md
@@ -120,3 +120,29 @@ Antwort `202` Akzeptiert und eingefügt
|
|||||||
|
|
||||||
Antwort `409` Konflikt
|
Antwort `409` Konflikt
|
||||||
Die vorherige Buchung am selben Tag hat den gleichen Buchungstyp
|
Die vorherige Buchung am selben Tag hat den gleichen Buchungstyp
|
||||||
|
|
||||||
|
# Filestrukture
|
||||||
|
|
||||||
|
```
|
||||||
|
├── Backend (Webserver)
|
||||||
|
│ ├── doc (Templates for Document Creator --> typst used to create PDF Reports)
|
||||||
|
│ │ ├── static
|
||||||
|
│ │ └── templates
|
||||||
|
│ ├── endpoints (HTML Server endpoints (see main.go for Routes))
|
||||||
|
│ ├── helper (Helper classes)
|
||||||
|
│ │ ├── logs
|
||||||
|
│ │ └── paramParser
|
||||||
|
│ ├── logs (Log Folder, no sourcecode)
|
||||||
|
│ ├── migrations (DB Migrations Folder, no direct sourcecode)
|
||||||
|
│ ├── models (DB Models and their function)
|
||||||
|
│ ├── src (Tailwind src --> used to config css formatter)
|
||||||
|
│ ├── static (Webserver static, used to server static content, e.g. JS and CSS files)
|
||||||
|
│ │ └── css
|
||||||
|
│ └── templates (HTML Templates for every page written in templ and compiled to go)
|
||||||
|
├── Cron (all Cron Scripts)
|
||||||
|
├── DB (local Database mount Point)
|
||||||
|
│ └── initdb (initialization scripts for DB)
|
||||||
|
├── Docker (Docker Files, only docker-compose.yaml used)
|
||||||
|
├── docs
|
||||||
|
└── └── images
|
||||||
|
```
|
||||||
|
|||||||
80
install.sh
80
install.sh
@@ -4,6 +4,10 @@ set -e
|
|||||||
envFile=Docker/.env
|
envFile=Docker/.env
|
||||||
envExample=Docker/env.example
|
envExample=Docker/env.example
|
||||||
|
|
||||||
|
autoBackupScript=Cron/autoBackup.sh
|
||||||
|
autoHolidaysScript=Cron/autoHolidays.sh
|
||||||
|
autoLogoutScript=Cron/autoLogout.sh
|
||||||
|
|
||||||
echo "Checking Docker installation..."
|
echo "Checking Docker installation..."
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
echo "Docker not found. Install Docker? [y/N]"
|
echo "Docker not found. Install Docker? [y/N]"
|
||||||
@@ -18,12 +22,16 @@ else
|
|||||||
echo "Docker is already installed."
|
echo "Docker is already installed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
echo "Checking Docker Compose..."
|
echo "Checking Docker Compose..."
|
||||||
if ! docker compose version >/dev/null 2>&1; then
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
echo "Docker Compose plugin missing. You may need to update Docker."
|
echo "Docker Compose plugin missing. You may need to update Docker."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
echo "Preparing .env file..."
|
echo "Preparing .env file..."
|
||||||
if [ ! -f $envFile ]; then
|
if [ ! -f $envFile ]; then
|
||||||
if [ -f $envExample ]; then
|
if [ -f $envExample ]; then
|
||||||
@@ -44,6 +52,9 @@ if [ ! -f $envFile ]; then
|
|||||||
raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
|
raw_val=$(printf "%s" "$rest" | sed 's/ *#.*//')
|
||||||
default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
|
default_value=$(printf "%s" "$raw_val" | sed 's/"//g')
|
||||||
|
|
||||||
|
# Replace __ROOT__ with script pwd
|
||||||
|
default_value="${default_value/__ROOT__/$(pwd)}"
|
||||||
|
|
||||||
regex=""
|
regex=""
|
||||||
if [[ "$comment" =~ regex:(.*)$ ]]; then
|
if [[ "$comment" =~ regex:(.*)$ ]]; then
|
||||||
regex="${BASH_REMATCH[1]}"
|
regex="${BASH_REMATCH[1]}"
|
||||||
@@ -96,13 +107,80 @@ else
|
|||||||
echo "Using existing .env. (found at $envFile)"
|
echo "Using existing .env. (found at $envFile)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
LOG_PATH=$(grep -E '^LOG_PATH=' $envFile | cut -d= -f2)
|
||||||
|
if [ -z "$LOG_PATH" ]; then
|
||||||
|
echo "LOG_PATH not found in .env using default $(pwd)/logs"
|
||||||
|
LOG_PATH=$(pwd)/logs
|
||||||
|
else
|
||||||
|
LOG_PATH=Docker/$LOG_PATH
|
||||||
|
fi
|
||||||
|
mkdir -p $LOG_PATH
|
||||||
|
echo "Created logs folder at $LOG_PATH"
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
echo -e "\n\n"
|
||||||
echo "Start containers with docker compose up -d? [y/N]"
|
echo "Start containers with docker compose up -d? [y/N]"
|
||||||
read -r start_containers
|
read -r start_containers
|
||||||
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
|
if [[ "$start_containers" =~ ^[Yy]$ ]]; then
|
||||||
cd Docker
|
cd Docker
|
||||||
mkdir ../logs
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
echo "Containers started."
|
echo "Containers started."
|
||||||
else
|
else
|
||||||
echo "You can start them manually with: docker compose up -d"
|
echo "You can start them manually with: docker compose up -d"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
###########################################################################
|
||||||
|
|
||||||
|
echo -e "\n\n"
|
||||||
|
echo "Setup Crontab for automatic logout, backup and holiday creation? [y/N]"
|
||||||
|
read -r setup_cron
|
||||||
|
if [[ "$setup_cron" =~ ^[Yy]$ ]]; then
|
||||||
|
WEB_PORT=$(grep -E '^WEB_PORT=' $envFile | cut -d= -f2)
|
||||||
|
if [ -z "$WEB_PORT" ]; then
|
||||||
|
echo "WEB_PORT not found in .env using default 8000"
|
||||||
|
WEB_PORT=8000
|
||||||
|
fi
|
||||||
|
|
||||||
|
POSTGRES_DB=$(grep -E '^POSTGRES_DB=' $envFile | cut -d= -f2)
|
||||||
|
if [ -z "$POSTGRES_DB" ]; then
|
||||||
|
echo "arbeitszeitmessung not found in .env using default arbeitszeitmessung"
|
||||||
|
POSTGRES_DB="arbeitszeitmessung"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sed -i "s/__PORT__/$WEB_PORT/" $autoHolidaysScript
|
||||||
|
sed -i "s/__PORT__/$WEB_PORT/" $autoLogoutScript
|
||||||
|
sed -i "s/__DATABASE__/$POSTGRES_DB/" $autoBackupScript
|
||||||
|
|
||||||
|
chmod +x $autoBackupScript $autoHolidaysScript $autoLogoutScript
|
||||||
|
|
||||||
|
# echo "Scripts build with PORT=$WEB_PORT and DATABSE=$POSTGRES_DB!"
|
||||||
|
echo "Adding rules to crontab."
|
||||||
|
|
||||||
|
cron_commands=$(mktemp /tmp/arbeitszeitmessung-cron.XXX)
|
||||||
|
|
||||||
|
for file in Cron/*; do
|
||||||
|
cron_timing=$(grep -E '^# cron-timing:' "$file" | sed 's/^# cron-timing:[[:space:]]*//')
|
||||||
|
|
||||||
|
if [ -z "$cron_timing" ]; then
|
||||||
|
echo "No cron-timing found in $file, so it's not added to crontab."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
( crontab -l ; echo "$cron_timing $(pwd)/$file" )| awk '!x[$0]++' | crontab -
|
||||||
|
echo "Added entry to crontab: $cron_timing $(pwd)/$file."
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if systemctl is-active --quiet cron.service ; then
|
||||||
|
echo "cron.service is running. Everything should be fine now."
|
||||||
|
else
|
||||||
|
echo "cron.service is not running. Please start and enable cron.service."
|
||||||
|
echo "For how to start a service, see: https://wiki.ubuntuusers.de/systemd/systemctl UNITNAME will be cron.service"
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "Please setup cron manually by executing crontab -e and adding all files from inside the Cron directory!"
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user