Compare commits
18 Commits
docu
..
ce39b7ba34
| Author | SHA1 | Date | |
|---|---|---|---|
| ce39b7ba34 | |||
| d1720ea33d | |||
| 7f275c774e | |||
| 3b0b2b4340 | |||
| 3193204dac | |||
| f567d086ec | |||
| 17cb4505be | |||
| 4dda83904a | |||
| 115f2667f6 | |||
| 86f4c757e3 | |||
| b2328b4e0c | |||
| 837cd55a33 | |||
| 9749d5658c | |||
| 0402b8ac65 | |||
| 56a6f3cfc4 | |||
| 98d21724ee | |||
| 2f469c1830 | |||
| 609bc904ea |
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
|
_ "github.com/timetracker/backend/docs" // This line is important for swag to work
|
||||||
|
"github.com/timetracker/backend/internal/models"
|
||||||
|
_ "gorm.io/driver/postgres"
|
||||||
|
// GORM IMPORTS MARKER
|
||||||
|
)
|
||||||
|
|
||||||
|
// @title Time Tracker API
|
||||||
|
// @version 1.0
|
||||||
|
// @description This is a simple time tracker API.
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /
|
||||||
|
|
||||||
|
// @Summary Say hello
|
||||||
|
// @Description Get a hello message
|
||||||
|
// @ID hello
|
||||||
|
// @Produce plain
|
||||||
|
// @Success 200 {string} string "Hello from the Time Tracker Backend!"
|
||||||
|
// @Router / [get]
|
||||||
|
func helloHandler(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, "Hello from the Time Tracker Backend!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
dbConfig := models.DatabaseConfig{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 5432,
|
||||||
|
User: "postgres",
|
||||||
|
Password: "password",
|
||||||
|
DBName: "mydatabase",
|
||||||
|
SSLMode: "disable", // Für Entwicklungsumgebung
|
||||||
|
}
|
||||||
|
|
||||||
|
// Datenbank initialisieren
|
||||||
|
if err := models.InitDB(dbConfig); err != nil {
|
||||||
|
log.Fatalf("Fehler bei der DB-Initialisierung: %v", err)
|
||||||
|
}
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
r.GET("/", helloHandler)
|
||||||
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
|
fmt.Println("Server listening on port 8080")
|
||||||
|
r.Run(":8080") // Use Gin's Run method
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||||
|
package docs
|
||||||
|
|
||||||
|
import "github.com/swaggo/swag"
|
||||||
|
|
||||||
|
const docTemplate = `{
|
||||||
|
"schemes": {{ marshal .Schemes }},
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "{{escape .Description}}",
|
||||||
|
"title": "{{.Title}}",
|
||||||
|
"contact": {},
|
||||||
|
"version": "{{.Version}}"
|
||||||
|
},
|
||||||
|
"host": "{{.Host}}",
|
||||||
|
"basePath": "{{.BasePath}}",
|
||||||
|
"paths": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get a hello message",
|
||||||
|
"produces": [
|
||||||
|
"text/plain"
|
||||||
|
],
|
||||||
|
"summary": "Say hello",
|
||||||
|
"operationId": "hello",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Hello from the Time Tracker Backend!",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||||
|
var SwaggerInfo = &swag.Spec{
|
||||||
|
Version: "1.0",
|
||||||
|
Host: "localhost:8080",
|
||||||
|
BasePath: "/",
|
||||||
|
Schemes: []string{},
|
||||||
|
Title: "Time Tracker API",
|
||||||
|
Description: "This is a simple time tracker API.",
|
||||||
|
InfoInstanceName: "swagger",
|
||||||
|
SwaggerTemplate: docTemplate,
|
||||||
|
LeftDelim: "{{",
|
||||||
|
RightDelim: "}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "This is a simple time tracker API.",
|
||||||
|
"title": "Time Tracker API",
|
||||||
|
"contact": {},
|
||||||
|
"version": "1.0"
|
||||||
|
},
|
||||||
|
"host": "localhost:8080",
|
||||||
|
"basePath": "/",
|
||||||
|
"paths": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get a hello message",
|
||||||
|
"produces": [
|
||||||
|
"text/plain"
|
||||||
|
],
|
||||||
|
"summary": "Say hello",
|
||||||
|
"operationId": "hello",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Hello from the Time Tracker Backend!",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
basePath: /
|
||||||
|
host: localhost:8080
|
||||||
|
info:
|
||||||
|
contact: {}
|
||||||
|
description: This is a simple time tracker API.
|
||||||
|
title: Time Tracker API
|
||||||
|
version: "1.0"
|
||||||
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
description: Get a hello message
|
||||||
|
operationId: hello
|
||||||
|
produces:
|
||||||
|
- text/plain
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Hello from the Time Tracker Backend!
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Say hello
|
||||||
|
swagger: "2.0"
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
module github.com/timetracker/backend
|
||||||
|
|
||||||
|
go 1.23.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/oklog/ulid/v2 v2.1.0
|
||||||
|
github.com/swaggo/files v1.0.1
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
|
github.com/swaggo/swag v1.16.4
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
golang.org/x/sync v0.12.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/bytedance/sonic v1.13.1 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.25.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
|
golang.org/x/crypto v0.36.0
|
||||||
|
golang.org/x/net v0.37.0 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
golang.org/x/tools v0.31.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/postgres v1.5.11
|
||||||
|
)
|
||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
|
||||||
|
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||||
|
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
||||||
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||||
|
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||||
|
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
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/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||||
|
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||||
|
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||||
|
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||||
|
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||||
|
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||||
|
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
|
||||||
|
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
BillingRate float64 `json:"billingRate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityCreateDto struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
BillingRate float64 `json:"billingRate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityUpdateDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt *time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
BillingRate *float64 `json:"billingRate"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type AuthDto struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CompanyDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompanyCreateDto struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompanyUpdateDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt *time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomerDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CompanyID int `json:"companyId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomerCreateDto struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CompanyID int `json:"companyId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomerUpdateDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt *time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
CompanyID *int `json:"companyId"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
CustomerID int `json:"customerId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectCreateDto struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
CustomerID int `json:"customerId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectUpdateDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt *time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
CustomerID *int `json:"customerId"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeEntryDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
ProjectID int `json:"projectId"`
|
||||||
|
ActivityID int `json:"activityId"`
|
||||||
|
Start time.Time `json:"start"`
|
||||||
|
End time.Time `json:"end"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Billable int `json:"billable"` // Percentage (0-100)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeEntryCreateDto struct {
|
||||||
|
UserID int `json:"userId"`
|
||||||
|
ProjectID int `json:"projectId"`
|
||||||
|
ActivityID int `json:"activityId"`
|
||||||
|
Start time.Time `json:"start"`
|
||||||
|
End time.Time `json:"end"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Billable int `json:"billable"` // Percentage (0-100)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TimeEntryUpdateDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt *time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||||
|
UserID *int `json:"userId"`
|
||||||
|
ProjectID *int `json:"projectId"`
|
||||||
|
ActivityID *int `json:"activityId"`
|
||||||
|
Start *time.Time `json:"start"`
|
||||||
|
End *time.Time `json:"end"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Billable *int `json:"billable"` // Percentage (0-100)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID ulid.ULID `json:"lastEditorID"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CompanyID int `json:"companyId"`
|
||||||
|
HourlyRate float64 `json:"hourlyRate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserCreateDto struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CompanyID int `json:"companyId"`
|
||||||
|
HourlyRate float64 `json:"hourlyRate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserUpdateDto struct {
|
||||||
|
ID ulid.ULID `json:"id"`
|
||||||
|
CreatedAt *time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt *time.Time `json:"updatedAt"`
|
||||||
|
LastEditorID *ulid.ULID `json:"lastEditorID"`
|
||||||
|
Email *string `json:"email"`
|
||||||
|
Password *string `json:"password"`
|
||||||
|
Role *string `json:"role"`
|
||||||
|
CompanyID *int `json:"companyId"`
|
||||||
|
HourlyRate *float64 `json:"hourlyRate"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Activity represents an activity in the system
|
||||||
|
type Activity struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
BillingRate float64 `gorm:"column:billing_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (Activity) TableName() string {
|
||||||
|
return "activities"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityUpdate contains the updatable fields of an Activity
|
||||||
|
type ActivityUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Use "-" to indicate that this field should be ignored
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
BillingRate *float64 `gorm:"column:billing_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityCreate contains the fields for creating a new Activity
|
||||||
|
type ActivityCreate struct {
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
BillingRate float64 `gorm:"column:billing_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActivityByID finds an Activity by its ID
|
||||||
|
func GetActivityByID(ctx context.Context, id ulid.ULID) (*Activity, error) {
|
||||||
|
var activity Activity
|
||||||
|
result := GetEngine(ctx).Where("id = ?", id).First(&activity)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllActivities returns all Activities
|
||||||
|
func GetAllActivities(ctx context.Context) ([]Activity, error) {
|
||||||
|
var activities []Activity
|
||||||
|
result := GetEngine(ctx).Find(&activities)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return activities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateActivity creates a new Activity
|
||||||
|
func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, error) {
|
||||||
|
activity := Activity{
|
||||||
|
Name: create.Name,
|
||||||
|
BillingRate: create.BillingRate,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&activity)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateActivity updates an existing Activity
|
||||||
|
func UpdateActivity(ctx context.Context, update ActivityUpdate) (*Activity, error) {
|
||||||
|
activity, err := GetActivityByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if activity == nil {
|
||||||
|
return nil, errors.New("activity not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generic update function
|
||||||
|
if err := UpdateModel(ctx, activity, update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load updated data from the database
|
||||||
|
return GetActivityByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteActivity deletes an Activity by its ID
|
||||||
|
func DeleteActivity(ctx context.Context, id ulid.ULID) error {
|
||||||
|
result := GetEngine(ctx).Delete(&Activity{}, id)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntityBase struct {
|
||||||
|
ID ulid.ULID `gorm:"type:uuid;primaryKey"`
|
||||||
|
CreatedAt time.Time `gorm:"index"`
|
||||||
|
UpdatedAt time.Time `gorm:"index"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate is called by GORM before creating a record
|
||||||
|
func (eb *EntityBase) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if eb.ID.Compare(ulid.ULID{}) == 0 { // If ID is empty
|
||||||
|
// Generate a new ULID
|
||||||
|
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
|
||||||
|
eb.ID = ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Company represents a company in the system
|
||||||
|
type Company struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (Company) TableName() string {
|
||||||
|
return "companies"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompanyCreate contains the fields for creating a new company
|
||||||
|
type CompanyCreate struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompanyUpdate contains the updatable fields of a company
|
||||||
|
type CompanyUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Exclude from updates
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompanyByID finds a company by its ID
|
||||||
|
func GetCompanyByID(ctx context.Context, id ulid.ULID) (*Company, error) {
|
||||||
|
var company Company
|
||||||
|
result := GetEngine(ctx).Where("id = ?", id).First(&company)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &company, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCompanies returns all companies
|
||||||
|
func GetAllCompanies(ctx context.Context) ([]Company, error) {
|
||||||
|
var companies []Company
|
||||||
|
result := GetEngine(ctx).Find(&companies)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return companies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, error) {
|
||||||
|
var customers []Customer
|
||||||
|
result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&customers)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return customers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCompany creates a new company
|
||||||
|
func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) {
|
||||||
|
company := Company{
|
||||||
|
Name: create.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&company)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &company, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCompany updates an existing company
|
||||||
|
func UpdateCompany(ctx context.Context, update CompanyUpdate) (*Company, error) {
|
||||||
|
company, err := GetCompanyByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if company == nil {
|
||||||
|
return nil, errors.New("company not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generic update function
|
||||||
|
if err := UpdateModel(ctx, company, update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load updated data from the database
|
||||||
|
return GetCompanyByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCompany deletes a company by its ID
|
||||||
|
func DeleteCompany(ctx context.Context, id ulid.ULID) error {
|
||||||
|
result := GetEngine(ctx).Delete(&Company{}, id)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Customer represents a customer in the system
|
||||||
|
type Customer struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
CompanyID int `gorm:"column:company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (Customer) TableName() string {
|
||||||
|
return "customers"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomerCreate contains the fields for creating a new customer
|
||||||
|
type CustomerCreate struct {
|
||||||
|
Name string
|
||||||
|
CompanyID int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomerUpdate contains the updatable fields of a customer
|
||||||
|
type CustomerUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Exclude from updates
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
CompanyID *int `gorm:"column:company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCustomerByID finds a customer by its ID
|
||||||
|
func GetCustomerByID(ctx context.Context, id ulid.ULID) (*Customer, error) {
|
||||||
|
var customer Customer
|
||||||
|
result := GetEngine(ctx).Where("id = ?", id).First(&customer)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &customer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCustomers returns all customers
|
||||||
|
func GetAllCustomers(ctx context.Context) ([]Customer, error) {
|
||||||
|
var customers []Customer
|
||||||
|
result := GetEngine(ctx).Find(&customers)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return customers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCustomer creates a new customer
|
||||||
|
func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, error) {
|
||||||
|
customer := Customer{
|
||||||
|
Name: create.Name,
|
||||||
|
CompanyID: create.CompanyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&customer)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &customer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCustomer updates an existing customer
|
||||||
|
func UpdateCustomer(ctx context.Context, update CustomerUpdate) (*Customer, error) {
|
||||||
|
customer, err := GetCustomerByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if customer == nil {
|
||||||
|
return nil, errors.New("customer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generic update function
|
||||||
|
if err := UpdateModel(ctx, customer, update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load updated data from the database
|
||||||
|
return GetCustomerByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCustomer deletes a customer by its ID
|
||||||
|
func DeleteCustomer(ctx context.Context, id ulid.ULID) error {
|
||||||
|
result := GetEngine(ctx).Delete(&Customer{}, id)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres" // For PostgreSQL
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global variable for the DB connection
|
||||||
|
var defaultDB *gorm.DB
|
||||||
|
|
||||||
|
// DatabaseConfig contains the configuration data for the database connection
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
DBName string
|
||||||
|
SSLMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitDB initializes the database connection (once at startup)
|
||||||
|
// with the provided configuration
|
||||||
|
func InitDB(config DatabaseConfig) error {
|
||||||
|
// Create DSN (Data Source Name)
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
||||||
|
|
||||||
|
// Establish database connection
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error connecting to the database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDB = db
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEngine returns the DB instance, possibly with context
|
||||||
|
func GetEngine(ctx context.Context) *gorm.DB {
|
||||||
|
// If a special transaction is in ctx, you could check it here
|
||||||
|
return defaultDB.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateModel updates a model based on the set pointer fields
|
||||||
|
func UpdateModel(ctx context.Context, model any, updates any) error {
|
||||||
|
updateValue := reflect.ValueOf(updates)
|
||||||
|
|
||||||
|
// If updates is a pointer, use the value behind it
|
||||||
|
if updateValue.Kind() == reflect.Ptr {
|
||||||
|
updateValue = updateValue.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure updates is a struct
|
||||||
|
if updateValue.Kind() != reflect.Struct {
|
||||||
|
return errors.New("updates must be a struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateType := updateValue.Type()
|
||||||
|
updateMap := make(map[string]any)
|
||||||
|
|
||||||
|
// Iterate through all fields
|
||||||
|
for i := 0; i < updateValue.NumField(); i++ {
|
||||||
|
field := updateValue.Field(i)
|
||||||
|
fieldType := updateType.Field(i)
|
||||||
|
|
||||||
|
// Skip unexported fields
|
||||||
|
if !fieldType.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: Skip ID field (use only for updates)
|
||||||
|
if fieldType.Name == "ID" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For pointer types, check if they are not nil
|
||||||
|
if field.Kind() == reflect.Ptr && !field.IsNil() {
|
||||||
|
// Extract field name from GORM tag or use default field name
|
||||||
|
fieldName := fieldType.Name
|
||||||
|
|
||||||
|
if tag, ok := fieldType.Tag.Lookup("gorm"); ok {
|
||||||
|
// Separate tag options
|
||||||
|
options := strings.Split(tag, ";")
|
||||||
|
for _, option := range options {
|
||||||
|
if strings.HasPrefix(option, "column:") {
|
||||||
|
fieldName = strings.TrimPrefix(option, "column:")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the value behind the pointer
|
||||||
|
updateMap[fieldName] = field.Elem().Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateMap) == 0 {
|
||||||
|
return nil // Nothing to update
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetEngine(ctx).Model(model).Updates(updateMap).Error
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrUserAlreadyExists = errors.New("user already exists")
|
||||||
|
var ErrUserNotFound = errors.New("user not found")
|
||||||
|
var ErrActivityNotFound = errors.New("activity not found")
|
||||||
|
var ErrActivityAlreadyExists = errors.New("activity already exists")
|
||||||
|
var ErrInvalidPassword = errors.New("invalid password")
|
||||||
|
var ErrInvalidEmail = errors.New("invalid email")
|
||||||
|
var ErrInvalidUsername = errors.New("invalid username")
|
||||||
|
var ErrInvalidRole = errors.New("invalid role")
|
||||||
|
var ErrInvalidCompanyID = errors.New("invalid company id")
|
||||||
|
var ErrInvalidHourlyRate = errors.New("invalid hourly rate")
|
||||||
|
var ErrInvalidID = errors.New("invalid id")
|
||||||
|
var ErrTimeEntryNotFound = errors.New("time entry not found")
|
||||||
|
var ErrTimeEntryAlreadyExists = errors.New("time entry already exists")
|
||||||
|
var ErrInvalidDuration = errors.New("invalid duration")
|
||||||
|
var ErrInvalidDescription = errors.New("invalid description")
|
||||||
|
var ErrInvalidStartTime = errors.New("invalid start time")
|
||||||
|
var ErrInvalidEndTime = errors.New("invalid end time")
|
||||||
|
var ErrInvalidBillable = errors.New("invalid billable")
|
||||||
|
var ErrInvalidProjectID = errors.New("invalid project id")
|
||||||
|
var ErrProjectNotFound = errors.New("project not found")
|
||||||
|
var ErrProjectAlreadyExists = errors.New("project already exists")
|
||||||
|
var ErrInvalidName = errors.New("invalid name")
|
||||||
|
var ErrInvalidClientID = errors.New("invalid client id")
|
||||||
|
var ErrClientNotFound = errors.New("client not found")
|
||||||
|
var ErrClientAlreadyExists = errors.New("client already exists")
|
||||||
|
var ErrInvalidAddress = errors.New("invalid address")
|
||||||
|
var ErrInvalidPhone = errors.New("invalid phone")
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Project represents a project in the system
|
||||||
|
type Project struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name;not null"`
|
||||||
|
CustomerID ulid.ULID `gorm:"column:customer_id;type:uuid;not null"`
|
||||||
|
|
||||||
|
// Relationships (for Eager Loading)
|
||||||
|
Customer *Customer `gorm:"foreignKey:CustomerID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (Project) TableName() string {
|
||||||
|
return "projects"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectCreate contains the fields for creating a new project
|
||||||
|
type ProjectCreate struct {
|
||||||
|
Name string
|
||||||
|
CustomerID ulid.ULID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectUpdate contains the updatable fields of a project
|
||||||
|
type ProjectUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Exclude from updates
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
CustomerID *ulid.ULID `gorm:"column:customer_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the Create struct contains valid data
|
||||||
|
func (pc *ProjectCreate) Validate() error {
|
||||||
|
if pc.Name == "" {
|
||||||
|
return errors.New("project name cannot be empty")
|
||||||
|
}
|
||||||
|
// Check for valid CustomerID
|
||||||
|
if pc.CustomerID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("customerID cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the Update struct contains valid data
|
||||||
|
func (pu *ProjectUpdate) Validate() error {
|
||||||
|
if pu.Name != nil && *pu.Name == "" {
|
||||||
|
return errors.New("project name cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectByID finds a project by its ID
|
||||||
|
func GetProjectByID(ctx context.Context, id ulid.ULID) (*Project, error) {
|
||||||
|
var project Project
|
||||||
|
result := GetEngine(ctx).Where("id = ?", id).First(&project)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectWithCustomer loads a project with the associated customer information
|
||||||
|
func GetProjectWithCustomer(ctx context.Context, id ulid.ULID) (*Project, error) {
|
||||||
|
var project Project
|
||||||
|
result := GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&project)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllProjects returns all projects
|
||||||
|
func GetAllProjects(ctx context.Context) ([]Project, error) {
|
||||||
|
var projects []Project
|
||||||
|
result := GetEngine(ctx).Find(&projects)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllProjectsWithCustomers returns all projects with customer information
|
||||||
|
func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) {
|
||||||
|
var projects []Project
|
||||||
|
result := GetEngine(ctx).Preload("Customer").Find(&projects)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProjectsByCustomerID returns all projects of a specific customer
|
||||||
|
func GetProjectsByCustomerID(ctx context.Context, customerID ulid.ULID) ([]Project, error) {
|
||||||
|
var projects []Project
|
||||||
|
result := GetEngine(ctx).Where("customer_id = ?", customerID).Find(&projects)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProject creates a new project with validation
|
||||||
|
func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) {
|
||||||
|
// Validation
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the customer exists
|
||||||
|
customer, err := GetCustomerByID(ctx, create.CustomerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error checking the customer: %w", err)
|
||||||
|
}
|
||||||
|
if customer == nil {
|
||||||
|
return nil, errors.New("the specified customer does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
project := Project{
|
||||||
|
Name: create.Name,
|
||||||
|
CustomerID: create.CustomerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&project)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, fmt.Errorf("error creating the project: %w", result.Error)
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProject updates an existing project with validation
|
||||||
|
func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) {
|
||||||
|
// Validation
|
||||||
|
if err := update.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := GetProjectByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if project == nil {
|
||||||
|
return nil, errors.New("project not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If CustomerID is updated, check if the customer exists
|
||||||
|
if update.CustomerID != nil {
|
||||||
|
customer, err := GetCustomerByID(ctx, *update.CustomerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error checking the customer: %w", err)
|
||||||
|
}
|
||||||
|
if customer == nil {
|
||||||
|
return nil, errors.New("the specified customer does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generic update function
|
||||||
|
if err := UpdateModel(ctx, project, update); err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating the project: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load updated data from the database
|
||||||
|
return GetProjectByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProject deletes a project by its ID
|
||||||
|
func DeleteProject(ctx context.Context, id ulid.ULID) error {
|
||||||
|
// Here you could check if dependent entities exist
|
||||||
|
result := GetEngine(ctx).Delete(&Project{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("error deleting the project: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProjectWithTransaction creates a project within a transaction
|
||||||
|
func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*Project, error) {
|
||||||
|
// Validation
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var project *Project
|
||||||
|
|
||||||
|
// Start transaction
|
||||||
|
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Customer check within the transaction
|
||||||
|
var customer Customer
|
||||||
|
if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return errors.New("the specified customer does not exist")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create project
|
||||||
|
newProject := Project{
|
||||||
|
Name: create.Name,
|
||||||
|
CustomerID: create.CustomerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newProject).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save project for return
|
||||||
|
project = &newProject
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("transaction error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeEntry represents a time entry in the system
|
||||||
|
type TimeEntry struct {
|
||||||
|
EntityBase
|
||||||
|
UserID ulid.ULID `gorm:"column:user_id;type:uuid;not null;index"`
|
||||||
|
ProjectID ulid.ULID `gorm:"column:project_id;type:uuid;not null;index"`
|
||||||
|
ActivityID ulid.ULID `gorm:"column:activity_id;type:uuid;not null;index"`
|
||||||
|
Start time.Time `gorm:"column:start;not null"`
|
||||||
|
End time.Time `gorm:"column:end;not null"`
|
||||||
|
Description string `gorm:"column:description"`
|
||||||
|
Billable int `gorm:"column:billable"` // Percentage (0-100)
|
||||||
|
|
||||||
|
// Relationships for Eager Loading
|
||||||
|
User *User `gorm:"foreignKey:UserID"`
|
||||||
|
Project *Project `gorm:"foreignKey:ProjectID"`
|
||||||
|
Activity *Activity `gorm:"foreignKey:ActivityID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName specifies the table name for GORM
|
||||||
|
func (TimeEntry) TableName() string {
|
||||||
|
return "time_entries"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeEntryCreate contains the fields for creating a new time entry
|
||||||
|
type TimeEntryCreate struct {
|
||||||
|
UserID ulid.ULID
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
ActivityID ulid.ULID
|
||||||
|
Start time.Time
|
||||||
|
End time.Time
|
||||||
|
Description string
|
||||||
|
Billable int // Percentage (0-100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeEntryUpdate contains the updatable fields of a time entry
|
||||||
|
type TimeEntryUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Exclude from updates
|
||||||
|
UserID *ulid.ULID `gorm:"column:user_id"`
|
||||||
|
ProjectID *ulid.ULID `gorm:"column:project_id"`
|
||||||
|
ActivityID *ulid.ULID `gorm:"column:activity_id"`
|
||||||
|
Start *time.Time `gorm:"column:start"`
|
||||||
|
End *time.Time `gorm:"column:end"`
|
||||||
|
Description *string `gorm:"column:description"`
|
||||||
|
Billable *int `gorm:"column:billable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the Create struct contains valid data
|
||||||
|
func (tc *TimeEntryCreate) Validate() error {
|
||||||
|
// Check for empty IDs
|
||||||
|
if tc.UserID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("userID cannot be empty")
|
||||||
|
}
|
||||||
|
if tc.ProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("projectID cannot be empty")
|
||||||
|
}
|
||||||
|
if tc.ActivityID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("activityID cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time checks
|
||||||
|
if tc.Start.IsZero() {
|
||||||
|
return errors.New("start time cannot be empty")
|
||||||
|
}
|
||||||
|
if tc.End.IsZero() {
|
||||||
|
return errors.New("end time cannot be empty")
|
||||||
|
}
|
||||||
|
if tc.End.Before(tc.Start) {
|
||||||
|
return errors.New("end time cannot be before start time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Billable percentage check
|
||||||
|
if tc.Billable < 0 || tc.Billable > 100 {
|
||||||
|
return errors.New("billable must be between 0 and 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the Update struct contains valid data
|
||||||
|
func (tu *TimeEntryUpdate) Validate() error {
|
||||||
|
// Billable percentage check
|
||||||
|
if tu.Billable != nil && (*tu.Billable < 0 || *tu.Billable > 100) {
|
||||||
|
return errors.New("billable must be between 0 and 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time checks
|
||||||
|
if tu.Start != nil && tu.End != nil && tu.End.Before(*tu.Start) {
|
||||||
|
return errors.New("end time cannot be before start time")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeEntryByID finds a time entry by its ID
|
||||||
|
func GetTimeEntryByID(ctx context.Context, id ulid.ULID) (*TimeEntry, error) {
|
||||||
|
var timeEntry TimeEntry
|
||||||
|
result := GetEngine(ctx).Where("id = ?", id).First(&timeEntry)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &timeEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeEntryWithRelations loads a time entry with all associated data
|
||||||
|
func GetTimeEntryWithRelations(ctx context.Context, id ulid.ULID) (*TimeEntry, error) {
|
||||||
|
var timeEntry TimeEntry
|
||||||
|
result := GetEngine(ctx).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Project").
|
||||||
|
Preload("Project.Customer"). // Nested relationship
|
||||||
|
Preload("Activity").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&timeEntry)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &timeEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTimeEntries returns all time entries
|
||||||
|
func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
|
||||||
|
var timeEntries []TimeEntry
|
||||||
|
result := GetEngine(ctx).Find(&timeEntries)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeEntriesByUserID returns all time entries of a user
|
||||||
|
func GetTimeEntriesByUserID(ctx context.Context, userID ulid.ULID) ([]TimeEntry, error) {
|
||||||
|
var timeEntries []TimeEntry
|
||||||
|
result := GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeEntriesByProjectID returns all time entries of a project
|
||||||
|
func GetTimeEntriesByProjectID(ctx context.Context, projectID ulid.ULID) ([]TimeEntry, error) {
|
||||||
|
var timeEntries []TimeEntry
|
||||||
|
result := GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeEntriesByDateRange returns all time entries within a time range
|
||||||
|
func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) {
|
||||||
|
var timeEntries []TimeEntry
|
||||||
|
// Search for overlaps in the time range
|
||||||
|
result := GetEngine(ctx).
|
||||||
|
Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)",
|
||||||
|
start, end, start, end).
|
||||||
|
Find(&timeEntries)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumBillableHoursByProject calculates the billable hours per project
|
||||||
|
func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float64, error) {
|
||||||
|
type Result struct {
|
||||||
|
TotalHours float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Result
|
||||||
|
|
||||||
|
// SQL calculation of weighted hours
|
||||||
|
err := GetEngine(ctx).Raw(`
|
||||||
|
SELECT SUM(
|
||||||
|
EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0)
|
||||||
|
) as total_hours
|
||||||
|
FROM time_entries
|
||||||
|
WHERE project_id = ?
|
||||||
|
`, projectID).Scan(&result).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.TotalHours, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTimeEntry creates a new time entry with validation
|
||||||
|
func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, error) {
|
||||||
|
// Validation
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
var timeEntry *TimeEntry
|
||||||
|
|
||||||
|
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Check references
|
||||||
|
if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create time entry
|
||||||
|
newTimeEntry := TimeEntry{
|
||||||
|
UserID: create.UserID,
|
||||||
|
ProjectID: create.ProjectID,
|
||||||
|
ActivityID: create.ActivityID,
|
||||||
|
Start: create.Start,
|
||||||
|
End: create.End,
|
||||||
|
Description: create.Description,
|
||||||
|
Billable: create.Billable,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newTimeEntry).Error; err != nil {
|
||||||
|
return fmt.Errorf("error creating the time entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeEntry = &newTimeEntry
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateReferences checks if all referenced entities exist
|
||||||
|
func validateReferences(tx *gorm.DB, userID, projectID, activityID ulid.ULID) error {
|
||||||
|
// Check user
|
||||||
|
var userCount int64
|
||||||
|
if err := tx.Model(&User{}).Where("id = ?", userID).Count(&userCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking the user: %w", err)
|
||||||
|
}
|
||||||
|
if userCount == 0 {
|
||||||
|
return errors.New("the specified user does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check project
|
||||||
|
var projectCount int64
|
||||||
|
if err := tx.Model(&Project{}).Where("id = ?", projectID).Count(&projectCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking the project: %w", err)
|
||||||
|
}
|
||||||
|
if projectCount == 0 {
|
||||||
|
return errors.New("the specified project does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check activity
|
||||||
|
var activityCount int64
|
||||||
|
if err := tx.Model(&Activity{}).Where("id = ?", activityID).Count(&activityCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking the activity: %w", err)
|
||||||
|
}
|
||||||
|
if activityCount == 0 {
|
||||||
|
return errors.New("the specified activity does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTimeEntry updates an existing time entry with validation
|
||||||
|
func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, error) {
|
||||||
|
// Validation
|
||||||
|
if err := update.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find time entry
|
||||||
|
timeEntry, err := GetTimeEntryByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if timeEntry == nil {
|
||||||
|
return nil, errors.New("time entry not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction for the update
|
||||||
|
err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Check references if they are updated
|
||||||
|
if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil {
|
||||||
|
// Use current values if not updated
|
||||||
|
userID := timeEntry.UserID
|
||||||
|
if update.UserID != nil {
|
||||||
|
userID = *update.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := timeEntry.ProjectID
|
||||||
|
if update.ProjectID != nil {
|
||||||
|
projectID = *update.ProjectID
|
||||||
|
}
|
||||||
|
|
||||||
|
activityID := timeEntry.ActivityID
|
||||||
|
if update.ActivityID != nil {
|
||||||
|
activityID = *update.ActivityID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateReferences(tx, userID, projectID, activityID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check time consistency
|
||||||
|
start := timeEntry.Start
|
||||||
|
if update.Start != nil {
|
||||||
|
start = *update.Start
|
||||||
|
}
|
||||||
|
|
||||||
|
end := timeEntry.End
|
||||||
|
if update.End != nil {
|
||||||
|
end = *update.End
|
||||||
|
}
|
||||||
|
|
||||||
|
if end.Before(start) {
|
||||||
|
return errors.New("end time cannot be before start time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use generic update
|
||||||
|
if err := UpdateModel(ctx, timeEntry, update); err != nil {
|
||||||
|
return fmt.Errorf("error updating the time entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load updated data from the database
|
||||||
|
return GetTimeEntryByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTimeEntry deletes a time entry by its ID
|
||||||
|
func DeleteTimeEntry(ctx context.Context, id ulid.ULID) error {
|
||||||
|
result := GetEngine(ctx).Delete(&TimeEntry{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("error deleting the time entry: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,534 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Argon2 Parameters
|
||||||
|
const (
|
||||||
|
// Recommended values for Argon2id
|
||||||
|
ArgonTime = 1
|
||||||
|
ArgonMemory = 64 * 1024 // 64MB
|
||||||
|
ArgonThreads = 4
|
||||||
|
ArgonKeyLen = 32
|
||||||
|
SaltLength = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// Role Constants
|
||||||
|
const (
|
||||||
|
RoleAdmin = "admin"
|
||||||
|
RoleUser = "user"
|
||||||
|
RoleViewer = "viewer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User represents a user in the system
|
||||||
|
type User struct {
|
||||||
|
EntityBase
|
||||||
|
Email string `gorm:"column:email;unique;not null"`
|
||||||
|
Salt string `gorm:"column:salt;not null;type:varchar(64)"` // Base64-encoded Salt
|
||||||
|
Hash string `gorm:"column:hash;not null;type:varchar(128)"` // Base64-encoded Hash
|
||||||
|
Role string `gorm:"column:role;not null;default:'user'"`
|
||||||
|
CompanyID ulid.ULID `gorm:"column:company_id;type:uuid;not null;index"`
|
||||||
|
HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"`
|
||||||
|
|
||||||
|
// Relationship for Eager Loading
|
||||||
|
Company *Company `gorm:"foreignKey:CompanyID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName provides the table name for GORM
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCreate contains the fields for creating a new user
|
||||||
|
type UserCreate struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
Role string
|
||||||
|
CompanyID ulid.ULID
|
||||||
|
HourlyRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserUpdate contains the updatable fields of a user
|
||||||
|
type UserUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Exclude from updates
|
||||||
|
Email *string `gorm:"column:email"`
|
||||||
|
Password *string `gorm:"-"` // Not stored directly in DB
|
||||||
|
Role *string `gorm:"column:role"`
|
||||||
|
CompanyID *ulid.ULID `gorm:"column:company_id"`
|
||||||
|
HourlyRate *float64 `gorm:"column:hourly_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordData contains the data for password hash and salt
|
||||||
|
type PasswordData struct {
|
||||||
|
Salt string
|
||||||
|
Hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSalt generates a cryptographically secure salt
|
||||||
|
func GenerateSalt() (string, error) {
|
||||||
|
salt := make([]byte, SaltLength)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(salt), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword creates a secure password hash with Argon2id and a random salt
|
||||||
|
func HashPassword(password string) (PasswordData, error) {
|
||||||
|
// Generate a cryptographically secure salt
|
||||||
|
saltStr, err := GenerateSalt()
|
||||||
|
if err != nil {
|
||||||
|
return PasswordData{}, fmt.Errorf("error generating salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(saltStr)
|
||||||
|
if err != nil {
|
||||||
|
return PasswordData{}, fmt.Errorf("error decoding salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create hash with Argon2id (modern, secure hash function)
|
||||||
|
hash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen)
|
||||||
|
hashStr := base64.StdEncoding.EncodeToString(hash)
|
||||||
|
|
||||||
|
return PasswordData{
|
||||||
|
Salt: saltStr,
|
||||||
|
Hash: hashStr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword checks if a password matches the hash
|
||||||
|
func VerifyPassword(password, saltStr, hashStr string) (bool, error) {
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(saltStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := base64.StdEncoding.DecodeString(hashStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error decoding hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate hash with the same salt
|
||||||
|
computedHash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen)
|
||||||
|
|
||||||
|
// Constant time comparison to prevent timing attacks
|
||||||
|
return hmacEqual(hash, computedHash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hmacEqual performs a constant-time comparison (prevents timing attacks)
|
||||||
|
func hmacEqual(a, b []byte) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var result byte
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
result |= a[i] ^ b[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the Create structure contains valid data
|
||||||
|
func (uc *UserCreate) Validate() error {
|
||||||
|
if uc.Email == "" {
|
||||||
|
return errors.New("email cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check email format
|
||||||
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
||||||
|
if !emailRegex.MatchString(uc.Email) {
|
||||||
|
return errors.New("invalid email format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.Password == "" {
|
||||||
|
return errors.New("password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check password complexity
|
||||||
|
if len(uc.Password) < 10 {
|
||||||
|
return errors.New("password must be at least 10 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// More complex password validation
|
||||||
|
var (
|
||||||
|
hasUpper = false
|
||||||
|
hasLower = false
|
||||||
|
hasNumber = false
|
||||||
|
hasSpecial = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, char := range uc.Password {
|
||||||
|
switch {
|
||||||
|
case 'A' <= char && char <= 'Z':
|
||||||
|
hasUpper = true
|
||||||
|
case 'a' <= char && char <= 'z':
|
||||||
|
hasLower = true
|
||||||
|
case '0' <= char && char <= '9':
|
||||||
|
hasNumber = true
|
||||||
|
case char == '!' || char == '@' || char == '#' || char == '$' ||
|
||||||
|
char == '%' || char == '^' || char == '&' || char == '*':
|
||||||
|
hasSpecial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
|
||||||
|
return errors.New("password must contain uppercase letters, lowercase letters, numbers, and special characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role
|
||||||
|
if uc.Role == "" {
|
||||||
|
uc.Role = RoleUser // Set default role
|
||||||
|
} else {
|
||||||
|
validRoles := []string{RoleAdmin, RoleUser, RoleViewer}
|
||||||
|
isValid := slices.Contains(validRoles, uc.Role)
|
||||||
|
if !isValid {
|
||||||
|
return fmt.Errorf("invalid role: %s, allowed are: %s",
|
||||||
|
uc.Role, strings.Join(validRoles, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.CompanyID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("companyID cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.HourlyRate < 0 {
|
||||||
|
return errors.New("hourly rate cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the Update structure contains valid data
|
||||||
|
func (uu *UserUpdate) Validate() error {
|
||||||
|
if uu.Email != nil && *uu.Email == "" {
|
||||||
|
return errors.New("email cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check email format
|
||||||
|
if uu.Email != nil {
|
||||||
|
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
||||||
|
if !emailRegex.MatchString(*uu.Email) {
|
||||||
|
return errors.New("invalid email format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uu.Password != nil {
|
||||||
|
if *uu.Password == "" {
|
||||||
|
return errors.New("password cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check password complexity
|
||||||
|
if len(*uu.Password) < 10 {
|
||||||
|
return errors.New("password must be at least 10 characters long")
|
||||||
|
}
|
||||||
|
|
||||||
|
// More complex password validation
|
||||||
|
var (
|
||||||
|
hasUpper = false
|
||||||
|
hasLower = false
|
||||||
|
hasNumber = false
|
||||||
|
hasSpecial = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, char := range *uu.Password {
|
||||||
|
switch {
|
||||||
|
case 'A' <= char && char <= 'Z':
|
||||||
|
hasUpper = true
|
||||||
|
case 'a' <= char && char <= 'z':
|
||||||
|
hasLower = true
|
||||||
|
case '0' <= char && char <= '9':
|
||||||
|
hasNumber = true
|
||||||
|
case char == '!' || char == '@' || char == '#' || char == '$' ||
|
||||||
|
char == '%' || char == '^' || char == '&' || char == '*':
|
||||||
|
hasSpecial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
|
||||||
|
return errors.New("password must contain uppercase letters, lowercase letters, numbers, and special characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check role
|
||||||
|
if uu.Role != nil {
|
||||||
|
validRoles := []string{RoleAdmin, RoleUser, RoleViewer}
|
||||||
|
isValid := false
|
||||||
|
for _, role := range validRoles {
|
||||||
|
if *uu.Role == role {
|
||||||
|
isValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
return fmt.Errorf("invalid role: %s, allowed are: %s",
|
||||||
|
*uu.Role, strings.Join(validRoles, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uu.HourlyRate != nil && *uu.HourlyRate < 0 {
|
||||||
|
return errors.New("hourly rate cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID finds a user by their ID
|
||||||
|
func GetUserByID(ctx context.Context, id ulid.ULID) (*User, error) {
|
||||||
|
var user User
|
||||||
|
result := GetEngine(ctx).Where("id = ?", id).First(&user)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail finds a user by their email
|
||||||
|
func GetUserByEmail(ctx context.Context, email string) (*User, error) {
|
||||||
|
var user User
|
||||||
|
result := GetEngine(ctx).Where("email = ?", email).First(&user)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserWithCompany loads a user with their company
|
||||||
|
func GetUserWithCompany(ctx context.Context, id ulid.ULID) (*User, error) {
|
||||||
|
var user User
|
||||||
|
result := GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&user)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllUsers returns all users
|
||||||
|
func GetAllUsers(ctx context.Context) ([]User, error) {
|
||||||
|
var users []User
|
||||||
|
result := GetEngine(ctx).Find(&users)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsersByCompanyID returns all users of a company
|
||||||
|
func GetUsersByCompanyID(ctx context.Context, companyID ulid.ULID) ([]User, error) {
|
||||||
|
var users []User
|
||||||
|
result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&users)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser creates a new user with validation and secure password hashing
|
||||||
|
func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
|
||||||
|
// Validation
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction
|
||||||
|
var user *User
|
||||||
|
|
||||||
|
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Check if email already exists
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking email: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("email is already in use")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if company exists
|
||||||
|
var companyCount int64
|
||||||
|
if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking company: %w", err)
|
||||||
|
}
|
||||||
|
if companyCount == 0 {
|
||||||
|
return errors.New("the specified company does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password with unique salt
|
||||||
|
pwData, err := HashPassword(create.Password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error hashing password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user with salt and hash stored separately
|
||||||
|
newUser := User{
|
||||||
|
Email: create.Email,
|
||||||
|
Salt: pwData.Salt,
|
||||||
|
Hash: pwData.Hash,
|
||||||
|
Role: create.Role,
|
||||||
|
CompanyID: create.CompanyID,
|
||||||
|
HourlyRate: create.HourlyRate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newUser).Error; err != nil {
|
||||||
|
return fmt.Errorf("error creating user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user = &newUser
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates an existing user
|
||||||
|
func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) {
|
||||||
|
// Validation
|
||||||
|
if err := update.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validation error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user
|
||||||
|
user, err := GetUserByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a transaction for the update
|
||||||
|
err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// If email is updated, check if it's already in use
|
||||||
|
if update.Email != nil && *update.Email != user.Email {
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&User{}).Where("email = ? AND id != ?", *update.Email, update.ID).Count(&count).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking email: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("email is already in use")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If CompanyID is updated, check if it exists
|
||||||
|
if update.CompanyID != nil && update.CompanyID.Compare(user.CompanyID) != 0 {
|
||||||
|
var companyCount int64
|
||||||
|
if err := tx.Model(&Company{}).Where("id = ?", *update.CompanyID).Count(&companyCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("error checking company: %w", err)
|
||||||
|
}
|
||||||
|
if companyCount == 0 {
|
||||||
|
return errors.New("the specified company does not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If password is updated, rehash with new salt
|
||||||
|
if update.Password != nil {
|
||||||
|
pwData, err := HashPassword(*update.Password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error hashing password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update salt and hash directly in the model
|
||||||
|
if err := tx.Model(user).Updates(map[string]any{
|
||||||
|
"salt": pwData.Salt,
|
||||||
|
"hash": pwData.Hash,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fmt.Errorf("error updating password: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create map for generic update
|
||||||
|
updates := make(map[string]any)
|
||||||
|
|
||||||
|
// Add only non-password fields to the update
|
||||||
|
if update.Email != nil {
|
||||||
|
updates["email"] = *update.Email
|
||||||
|
}
|
||||||
|
if update.Role != nil {
|
||||||
|
updates["role"] = *update.Role
|
||||||
|
}
|
||||||
|
if update.CompanyID != nil {
|
||||||
|
updates["company_id"] = *update.CompanyID
|
||||||
|
}
|
||||||
|
if update.HourlyRate != nil {
|
||||||
|
updates["hourly_rate"] = *update.HourlyRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only execute generic update if there are changes
|
||||||
|
if len(updates) > 0 {
|
||||||
|
if err := tx.Model(user).Updates(updates).Error; err != nil {
|
||||||
|
return fmt.Errorf("error updating user: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load updated data from the database
|
||||||
|
return GetUserByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser deletes a user by their ID
|
||||||
|
func DeleteUser(ctx context.Context, id ulid.ULID) error {
|
||||||
|
// Here one could check if dependent entities exist
|
||||||
|
// e.g., don't delete if time entries still exist
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Delete(&User{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("error deleting user: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticateUser authenticates a user with email and password
|
||||||
|
func AuthenticateUser(ctx context.Context, email, password string) (*User, error) {
|
||||||
|
user, err := GetUserByEmail(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
// Same error message to avoid revealing information about existing accounts
|
||||||
|
return nil, errors.New("invalid login credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password with the stored salt
|
||||||
|
isValid, err := VerifyPassword(password, user.Salt, user.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error verifying password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
return nil, errors.New("invalid login credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
packages:
|
||||||
|
- path: github.com/timetracker/backend/dto
|
||||||
|
type_mappings:
|
||||||
|
"time.Time": "string"
|
||||||
|
"ulid.ULID": "string"
|
||||||
|
output_path: ../frontend/src/types/dto.ts
|
||||||
@@ -13,6 +13,8 @@ This document provides an overview of the Time Tracking and Management System. F
|
|||||||
- [Extensibility and Integrations](extensibility_integrations.md)
|
- [Extensibility and Integrations](extensibility_integrations.md)
|
||||||
- [LLM Guidance](llm_guidance.md)
|
- [LLM Guidance](llm_guidance.md)
|
||||||
- [Roadmap](Roadmap.md)
|
- [Roadmap](Roadmap.md)
|
||||||
|
- [Domain Types](domain_types.md)
|
||||||
|
- [DTOs](dtos.md)
|
||||||
|
|
||||||
## Code Examples
|
## Code Examples
|
||||||
- [GORM Entities](code_examples/gorm_entities.go)
|
- [GORM Entities](code_examples/gorm_entities.go)
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/email/timetracker/internal/application/timetracking"
|
||||||
|
"github.com/email/timetracker/internal/interfaces/http/dto"
|
||||||
|
"github.com/email/timetracker/internal/interfaces/http/middleware"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/username/timetracker/internal/application/timetracking"
|
|
||||||
"github.com/username/timetracker/internal/interfaces/http/dto"
|
|
||||||
"github.com/username/timetracker/internal/interfaces/http/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimeEntryHandler behandelt HTTP-Anfragen für Zeitbuchungen
|
// TimeEntryHandler behandelt HTTP-Anfragen für Zeitbuchungen
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/email/timetracker/internal/domain/entities"
|
||||||
|
"github.com/email/timetracker/pkg/functional"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/username/timetracker/internal/domain/entities"
|
|
||||||
"github.com/username/timetracker/pkg/functional"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TimeEntryFilter enthält Filter für die Suche nach Zeitbuchungen
|
// TimeEntryFilter enthält Filter für die Suche nach Zeitbuchungen
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/email/timetracker/internal/domain/entities"
|
||||||
|
"github.com/email/timetracker/internal/domain/repositories"
|
||||||
|
"github.com/email/timetracker/pkg/functional"
|
||||||
|
"github.com/email/timetracker/pkg/validator"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/username/timetracker/internal/domain/entities"
|
|
||||||
"github.com/username/timetracker/internal/domain/repositories"
|
|
||||||
"github.com/username/timetracker/pkg/functional"
|
|
||||||
"github.com/username/timetracker/pkg/validator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateTimeEntryCommand enthält die Daten zum Erstellen einer Zeitbuchung
|
// CreateTimeEntryCommand enthält die Daten zum Erstellen einer Zeitbuchung
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package entities
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|||||||
@@ -13,6 +13,40 @@ CREATE TABLE companies (
|
|||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Go structs for creating and updating customers
|
||||||
|
-- type CustomerCreate struct {
|
||||||
|
-- Name string
|
||||||
|
-- CompanyID int
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- type CustomerUpdate struct {
|
||||||
|
-- ID ulid.ULID
|
||||||
|
-- Name *string
|
||||||
|
-- CompanyID *int
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- Go structs for creating and updating companies
|
||||||
|
-- type CompanyCreate struct {
|
||||||
|
-- Name string
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- type CompanyUpdate struct {
|
||||||
|
-- ID ulid.ULID
|
||||||
|
-- Name *string
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- Go structs for creating and updating activities
|
||||||
|
-- type ActivityCreate struct {
|
||||||
|
-- Name string
|
||||||
|
-- BillingRate float64
|
||||||
|
-- }
|
||||||
|
|
||||||
|
-- type ActivityUpdate struct {
|
||||||
|
-- ID ulid.ULID
|
||||||
|
-- Name *string
|
||||||
|
-- BillingRate *float64
|
||||||
|
-- }
|
||||||
|
|
||||||
-- Users and Roles
|
-- Users and Roles
|
||||||
CREATE TABLE roles (
|
CREATE TABLE roles (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Domain Types
|
||||||
|
|
||||||
|
This document describes the domain types used in the Time Tracker application. Domain types represent the core business concepts and are used throughout the application.
|
||||||
|
|
||||||
|
## Activity
|
||||||
|
|
||||||
|
The `Activity` type represents a specific activity that can be tracked, such as "Development", "Meeting", or "Bug Fixing".
|
||||||
|
|
||||||
|
## Company
|
||||||
|
|
||||||
|
The `Company` type represents a tenant in the multi-tenant application. Each company has its own set of users, customers, projects, and activities.
|
||||||
|
|
||||||
|
## Customer
|
||||||
|
|
||||||
|
The `Customer` type represents a customer of a company.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
The `Project` type represents a project for a specific customer.
|
||||||
|
|
||||||
|
## TimeEntry
|
||||||
|
|
||||||
|
The `TimeEntry` type represents a time booking for a specific user, project, and activity.
|
||||||
|
|
||||||
|
## User
|
||||||
|
|
||||||
|
The `User` type represents a user of the application. Each user belongs to a company and has a specific role.
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Ergänzung zur Zeiterfassungstool Spezifikation
|
|
||||||
|
|
||||||
## 10. Datensicherheit und Datenschutz
|
|
||||||
|
|
||||||
### Sicherheitsmaßnahmen
|
|
||||||
- **Verschlüsselung**: TLS für alle Verbindungen, Verschlüsselung sensibler Daten in der Datenbank
|
|
||||||
- **Tenant-Isolation**: Strikte Trennung der Daten zwischen verschiedenen Companies
|
|
||||||
- **Audit-Trail**: Lückenlose Protokollierung aller relevanten Systemaktivitäten
|
|
||||||
- **DSGVO-Konformität**: Implementierung von Datenschutzfunktionen (Recht auf Vergessen, Datenexport)
|
|
||||||
|
|
||||||
## 11. Deployment und DevOps
|
|
||||||
|
|
||||||
### Containerisierung
|
|
||||||
- Docker-Container für Backend und Frontend
|
|
||||||
- Docker-Compose für Entwicklungsumgebung
|
|
||||||
- Kubernetes-Manifest für Produktionsumgebung
|
|
||||||
|
|
||||||
### CI/CD-Pipeline
|
|
||||||
- Automatisierte Tests (Unit, Integration, E2E)
|
|
||||||
- Automatisiertes Deployment
|
|
||||||
- Versionsmanagement
|
|
||||||
|
|
||||||
### Monitoring und Logging
|
|
||||||
- Prometheus für Metriken
|
|
||||||
- Grafana für Visualisierung
|
|
||||||
- ELK-Stack oder ähnliches für Logging
|
|
||||||
- Alerting bei kritischen Ereignissen
|
|
||||||
|
|
||||||
## 12. Backup und Recovery
|
|
||||||
|
|
||||||
- Regelmäßige Datenbank-Backups
|
|
||||||
- Point-in-Time-Recovery
|
|
||||||
- Disaster-Recovery-Plan
|
|
||||||
|
|
||||||
## 13. Offline-Funktionalität
|
|
||||||
|
|
||||||
- Progressive Web App (PWA) Funktionalitäten
|
|
||||||
- Lokale Speicherung von Zeitbuchungen bei fehlender Internetverbindung
|
|
||||||
- Synchronisation bei Wiederherstellung der Verbindung
|
|
||||||
|
|
||||||
## 14. Integrationen
|
|
||||||
|
|
||||||
### API-Integrationen
|
|
||||||
- **Abrechnungssysteme**: Export von Abrechnungsdaten
|
|
||||||
- **Kalendersysteme**: Synchronisation mit Google Calendar, Outlook, etc.
|
|
||||||
- **Projektmanagementtools**: Jira, Trello, etc. (insbesondere für Version 2)
|
|
||||||
- **Webhooks**: Für benutzerdefinierte Integrationen
|
|
||||||
|
|
||||||
### Single Sign-On (SSO)
|
|
||||||
- Integration mit gängigen Identity Providern (Google, Microsoft, SAML)
|
|
||||||
- OAuth 2.0 / OpenID Connect
|
|
||||||
|
|
||||||
## 15. Erweiterbarkeit
|
|
||||||
|
|
||||||
### Plugin-System
|
|
||||||
- Möglichkeit zur Erweiterung durch Plugins
|
|
||||||
- Hooks für benutzerdefinierte Logik
|
|
||||||
|
|
||||||
### Custom Fields
|
|
||||||
- Erweiterbare Datenmodelle mit benutzerdefinierten Feldern
|
|
||||||
- Anpassbare Formulare
|
|
||||||
|
|
||||||
## 16. Mobile App-Unterstützung
|
|
||||||
|
|
||||||
- Responsive Webdesign für mobile Browser
|
|
||||||
- Native Mobile Apps (zukünftige Erweiterung)
|
|
||||||
- API-Design mit Mobilnutzung im Blick
|
|
||||||
|
|
||||||
## 17. Internationalisierung und Lokalisierung
|
|
||||||
|
|
||||||
- Mehrsprachige Benutzeroberfläche
|
|
||||||
- Lokalisierte Datums- und Zeitformate
|
|
||||||
- Währungsumrechnung
|
|
||||||
|
|
||||||
## 18. Abrechnung und Fakturierung
|
|
||||||
|
|
||||||
- Generierung von Rechnungsentwürfen basierend auf Zeitbuchungen
|
|
||||||
- Export von Abrechnungsdaten in verschiedenen Formaten (CSV, Excel, etc.)
|
|
||||||
- Automatisierte Erinnerungen für nicht erfasste Zeiten
|
|
||||||
|
|
||||||
## 19. Benachrichtigungssystem
|
|
||||||
|
|
||||||
- E-Mail-Benachrichtigungen
|
|
||||||
- In-App-Benachrichtigungen
|
|
||||||
- Erinnerungen für ausstehende Zeiterfassungen
|
|
||||||
- Genehmigungsanfragen
|
|
||||||
|
|
||||||
## 20. Analytics und Business Intelligence
|
|
||||||
|
|
||||||
- Erweiterte Analytik für Management-Insights
|
|
||||||
- Trendanalysen und Prognosen
|
|
||||||
- Ressourcenauslastung und -planung
|
|
||||||
- Exportmöglichkeiten für BI-Tools
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
Folgende Punkte könnten sinnvoll ergänzt werden:
|
|
||||||
|
|
||||||
- **Audit-Logs**:
|
|
||||||
- Protokollierung wichtiger Aktionen (z.B. Änderungen an Stammdaten, Rollenänderungen).
|
|
||||||
|
|
||||||
- **Benachrichtigungen**:
|
|
||||||
- E-Mail/Push bei wichtigen Ereignissen (z.B. Abweichungen, Freigaben).
|
|
||||||
|
|
||||||
- **Fehlerbehandlung & Validierung**:
|
|
||||||
- Einheitliche Strategie für API-Fehler und Client-seitige Validierungen.
|
|
||||||
|
|
||||||
- **Internationale Lokalisierung (i18n)**:
|
|
||||||
- Unterstützung mehrerer Sprachen für UI-Texte und Berichte.
|
|
||||||
|
|
||||||
- **Performance & Skalierung**:
|
|
||||||
- Caching-Strategien (z.B. Redis).
|
|
||||||
- Indexing-Strategien für effiziente Queries.
|
|
||||||
|
|
||||||
- **Backup & Restore-Konzept**:
|
|
||||||
- Regelmäßige Backups und Wiederherstellungsszenarien.
|
|
||||||
|
|
||||||
Möchtest du davon etwas in die Spezifikation übernehmen?
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
# Ergänzung zur Zeiterfassungstool Spezifikation
|
|
||||||
|
|
||||||
## 10. Datensicherheit und Datenschutz
|
|
||||||
|
|
||||||
### Sicherheitsmaßnahmen
|
|
||||||
- **Verschlüsselung**: TLS für alle Verbindungen, Verschlüsselung sensibler Daten in der Datenbank
|
|
||||||
- **Tenant-Isolation**: Strikte Trennung der Daten zwischen verschiedenen Companies
|
|
||||||
- **Audit-Trail**: Lückenlose Protokollierung aller relevanten Systemaktivitäten
|
|
||||||
- **DSGVO-Konformität**: Implementierung von Datenschutzfunktionen (Recht auf Vergessen, Datenexport)
|
|
||||||
|
|
||||||
## 11. Deployment und DevOps
|
|
||||||
|
|
||||||
### Containerisierung
|
|
||||||
- Docker-Container für Backend und Frontend
|
|
||||||
- Docker-Compose für Entwicklungsumgebung
|
|
||||||
- Kubernetes-Manifest für Produktionsumgebung
|
|
||||||
|
|
||||||
### CI/CD-Pipeline
|
|
||||||
- Automatisierte Tests (Unit, Integration, E2E)
|
|
||||||
- Automatisiertes Deployment
|
|
||||||
- Versionsmanagement
|
|
||||||
|
|
||||||
### Monitoring und Logging
|
|
||||||
- Prometheus für Metriken
|
|
||||||
- Grafana für Visualisierung
|
|
||||||
- ELK-Stack oder ähnliches für Logging
|
|
||||||
- Alerting bei kritischen Ereignissen
|
|
||||||
|
|
||||||
## 12. Backup und Recovery
|
|
||||||
|
|
||||||
- Regelmäßige Datenbank-Backups
|
|
||||||
- Point-in-Time-Recovery
|
|
||||||
- Disaster-Recovery-Plan
|
|
||||||
|
|
||||||
## 13. Offline-Funktionalität
|
|
||||||
|
|
||||||
- Progressive Web App (PWA) Funktionalitäten
|
|
||||||
- Lokale Speicherung von Zeitbuchungen bei fehlender Internetverbindung
|
|
||||||
- Synchronisation bei Wiederherstellung der Verbindung
|
|
||||||
|
|
||||||
## 14. Integrationen
|
|
||||||
|
|
||||||
### API-Integrationen
|
|
||||||
- **Abrechnungssysteme**: Export von Abrechnungsdaten
|
|
||||||
- **Kalendersysteme**: Synchronisation mit Google Calendar, Outlook, etc.
|
|
||||||
- **Projektmanagementtools**: Jira, Trello, etc. (insbesondere für Version 2)
|
|
||||||
- **Webhooks**: Für benutzerdefinierte Integrationen
|
|
||||||
|
|
||||||
### Single Sign-On (SSO)
|
|
||||||
- Integration mit gängigen Identity Providern (Google, Microsoft, SAML)
|
|
||||||
- OAuth 2.0 / OpenID Connect
|
|
||||||
|
|
||||||
## 15. Erweiterbarkeit
|
|
||||||
|
|
||||||
### Plugin-System
|
|
||||||
- Möglichkeit zur Erweiterung durch Plugins
|
|
||||||
- Hooks für benutzerdefinierte Logik
|
|
||||||
|
|
||||||
### Custom Fields
|
|
||||||
- Erweiterbare Datenmodelle mit benutzerdefinierten Feldern
|
|
||||||
- Anpassbare Formulare
|
|
||||||
|
|
||||||
## 16. Mobile App-Unterstützung
|
|
||||||
|
|
||||||
- Responsive Webdesign für mobile Browser
|
|
||||||
- Native Mobile Apps (zukünftige Erweiterung)
|
|
||||||
- API-Design mit Mobilnutzung im Blick
|
|
||||||
|
|
||||||
## 17. Internationalisierung und Lokalisierung
|
|
||||||
|
|
||||||
- Mehrsprachige Benutzeroberfläche
|
|
||||||
- Lokalisierte Datums- und Zeitformate
|
|
||||||
- Währungsumrechnung
|
|
||||||
|
|
||||||
## 18. Abrechnung und Fakturierung
|
|
||||||
|
|
||||||
- Generierung von Rechnungsentwürfen basierend auf Zeitbuchungen
|
|
||||||
- Export von Abrechnungsdaten in verschiedenen Formaten (CSV, Excel, etc.)
|
|
||||||
- Automatisierte Erinnerungen für nicht erfasste Zeiten
|
|
||||||
|
|
||||||
## 19. Benachrichtigungssystem
|
|
||||||
|
|
||||||
- E-Mail-Benachrichtigungen
|
|
||||||
- In-App-Benachrichtigungen
|
|
||||||
- Erinnerungen für ausstehende Zeiterfassungen
|
|
||||||
- Genehmigungsanfragen
|
|
||||||
|
|
||||||
## 20. Analytics und Business Intelligence
|
|
||||||
|
|
||||||
- Erweiterte Analytik für Management-Insights
|
|
||||||
- Trendanalysen und Prognosen
|
|
||||||
- Ressourcenauslastung und -planung
|
|
||||||
- Exportmöglichkeiten für BI-Tools
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
# Zeiterfassungstool Spezifikation
|
|
||||||
|
|
||||||
## 1. Überblick
|
|
||||||
|
|
||||||
Dieses Dokument spezifiziert ein modernes Zeiterfassungstool mit Multi-Tenant-Architektur, implementiert mit Go (Backend) und NextJS (Frontend). Das System unterstützt die Erfassung von Arbeitszeiten, Projektmanagement und Reporting-Funktionen.
|
|
||||||
|
|
||||||
## 2. Technologie-Stack
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- **Programmiersprache**: Go
|
|
||||||
- **Architektur**: Funktional-Programmierung (FPGO)
|
|
||||||
- **Datenbank**: PostgreSQL mit ORM
|
|
||||||
- **API-Dokumentation**: Swagger
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **Framework**: NextJS
|
|
||||||
- **Architektur**: Funktional-Programmierung (FPTS - Functional Programming TypeScript)
|
|
||||||
- **UI-Komponenten**: React mit Tailwind CSS
|
|
||||||
|
|
||||||
## 3. Multi-Tenant-Architektur
|
|
||||||
|
|
||||||
Das System unterstützt mehrere Unternehmen (Companies) mit eigenen Benutzern und Daten.
|
|
||||||
|
|
||||||
### Rollen und Berechtigungen
|
|
||||||
- **Admin**: Systemweiter Administrator mit vollständigem Zugriff
|
|
||||||
- **Company**: Unternehmensadministrator
|
|
||||||
- **Manager**: Projektmanager mit erweiterten Rechten
|
|
||||||
- **User**: Standardbenutzer (Mitarbeiter)
|
|
||||||
- **Auditor**: Nur-Lese-Zugriff für Berichte und Audits
|
|
||||||
|
|
||||||
## 4. Datenbankmodell
|
|
||||||
|
|
||||||
### Hauptentitäten
|
|
||||||
|
|
||||||
#### Tenant-Struktur
|
|
||||||
- **Company**: Unternehmen
|
|
||||||
- **User**: Benutzer mit Rollen
|
|
||||||
|
|
||||||
#### Zeiterfassung
|
|
||||||
- **Kunde**: Kunde einer Company
|
|
||||||
- **Projekt**: Projekte für Kunden
|
|
||||||
- **Tätigkeit**: Arbeitstätigkeiten
|
|
||||||
- **Buchung**: Zeiterfassung
|
|
||||||
- **Stundensatz**: Pro Mitarbeiter
|
|
||||||
- **Verrechnungspreis**: Pro Tätigkeit
|
|
||||||
|
|
||||||
#### Projektmanagement (Version 2)
|
|
||||||
- **Sprint**: Iterationen innerhalb eines Projekts
|
|
||||||
- **Task**: Aufgaben innerhalb eines Sprints
|
|
||||||
- **KanbanBoard**: Visualisierung von Tasks
|
|
||||||
|
|
||||||
## 5. Backend-Architektur
|
|
||||||
|
|
||||||
### Modularisierung
|
|
||||||
Das Backend wird in kleine, funktionale Module aufgeteilt, um die Kontextgröße für LLM-basierte Implementierung zu optimieren.
|
|
||||||
|
|
||||||
#### Core-Module
|
|
||||||
- **auth**: Authentifizierung und Autorisierung
|
|
||||||
- **tenant**: Multi-Tenant-Verwaltung
|
|
||||||
- **user**: Benutzerverwaltung
|
|
||||||
- **timetracking**: Zeiterfassung
|
|
||||||
- **customer**: Kundenverwaltung
|
|
||||||
- **project**: Projektverwaltung
|
|
||||||
- **billing**: Abrechnung und Verrechnungspreise
|
|
||||||
- **reporting**: Report-Generierung
|
|
||||||
|
|
||||||
### API-Design
|
|
||||||
- RESTful API mit Swagger-Dokumentation
|
|
||||||
- JWT-basierte Authentifizierung
|
|
||||||
- Tenant-Isolation durch Middleware
|
|
||||||
|
|
||||||
## 6. Frontend-Architektur
|
|
||||||
|
|
||||||
### Komponentenstruktur
|
|
||||||
- Modulare React-Komponenten
|
|
||||||
- State Management mit Context API oder Redux
|
|
||||||
- Responsive Design
|
|
||||||
|
|
||||||
### Hauptseiten
|
|
||||||
- Dashboard (Übersicht)
|
|
||||||
- Zeiterfassung mit Tracker
|
|
||||||
- Kunden- und Projektverwaltung
|
|
||||||
- Reporting und Auswertungen
|
|
||||||
- Administrationsoberfläche
|
|
||||||
|
|
||||||
## 7. Funktionsumfang (Version 1)
|
|
||||||
|
|
||||||
### Unternehmensverwaltung
|
|
||||||
- Anlegen und Verwalten von Companies (Multi-Tenant)
|
|
||||||
- Benutzerverwaltung mit Rollenzuweisung
|
|
||||||
|
|
||||||
### Stammdatenverwaltung
|
|
||||||
- Kunden anlegen und verwalten
|
|
||||||
- Projekte anlegen und verwalten
|
|
||||||
- Tätigkeiten definieren mit Verrechnungspreisen
|
|
||||||
- Mitarbeiter-Stundensätze hinterlegen
|
|
||||||
|
|
||||||
### Zeiterfassung
|
|
||||||
- Zeiten buchen mit Angaben zu:
|
|
||||||
- Zeitraum (Start/Ende oder Dauer)
|
|
||||||
- Projekt (Wohin)
|
|
||||||
- Tätigkeit (Wofür)
|
|
||||||
- Benutzer (Wer)
|
|
||||||
- Beschreibung (optional)
|
|
||||||
- Verrechnungspreis
|
|
||||||
- Abrechenbarkeit (0-100% Slider)
|
|
||||||
- Zeittracker mit Start/Stop-Funktion
|
|
||||||
- Übernahme der letzten Buchungsparameter
|
|
||||||
|
|
||||||
### Reporting
|
|
||||||
- Kumulation von Buchungen nach:
|
|
||||||
- Projekten
|
|
||||||
- Mitarbeitern
|
|
||||||
- Kunden
|
|
||||||
- Zeiträumen
|
|
||||||
- PDF-Export von Reports
|
|
||||||
- Dashboards mit Visualisierungen
|
|
||||||
|
|
||||||
## 8. Funktionsumfang (Version 2)
|
|
||||||
|
|
||||||
### Projektmanagement
|
|
||||||
- Sprints innerhalb von Projekten
|
|
||||||
- Task-Items in Sprints
|
|
||||||
- Kanban-Boards für visuelle Darstellung
|
|
||||||
- Zuweisung von Tasks zu Benutzern
|
|
||||||
- Direkte Verknüpfung von Tasks mit Zeitbuchungen
|
|
||||||
|
|
||||||
## 9. Implementierungsplan
|
|
||||||
|
|
||||||
### Phase 1: Grundlegende Infrastruktur
|
|
||||||
- Aufsetzen der Go-Backend-Struktur
|
|
||||||
- Implementierung der Datenbank mit ORM
|
|
||||||
- NextJS-Frontend-Grundgerüst
|
|
||||||
- Authentifizierung und Autorisierung
|
|
||||||
|
|
||||||
### Phase 2: Version 1 Features
|
|
||||||
- Stammdatenverwaltung
|
|
||||||
- Zeiterfassung
|
|
||||||
- Reporting
|
|
||||||
- Dashboard
|
|
||||||
|
|
||||||
### Phase 3: Version 2 Features
|
|
||||||
- Projektmanagement
|
|
||||||
- Kanban-Boards
|
|
||||||
- Task-Tracking
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# Spezifikation Zeiterfassungs-Tool
|
|
||||||
|
|
||||||
## Technologie-Stack
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- Go mit fp-go
|
|
||||||
- PostgreSQL mit ORM (GORM oder Bun)
|
|
||||||
- Swagger/OpenAPI
|
|
||||||
- JWT-basierte Authentifizierung
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- Next.js mit fp-ts
|
|
||||||
- React und Tailwind CSS
|
|
||||||
- Responsive Webdesign
|
|
||||||
- Progressive Web App (PWA)
|
|
||||||
|
|
||||||
## Multi-Tenancy & Rollen
|
|
||||||
- **Mandantenfähigkeit**: Isolation von Daten je Unternehmen (Tenant)
|
|
||||||
- **Rollen:** Admin, Company-Admin, Manager, Auditor, User
|
|
||||||
|
|
||||||
## Datenmodelle
|
|
||||||
- **Company** (Tenant)
|
|
||||||
- **User** (mit Rollen, Stundenrate)
|
|
||||||
- **Kunde**
|
|
||||||
- **Projekt** (mit Kundenreferenz)
|
|
||||||
- **Tätigkeit** (Verrechnungspreis)
|
|
||||||
- **Buchung** (Zeit, User, Projekt, Tätigkeit, Beschreibung optional, Abrechenbarkeit 0-100%)
|
|
||||||
|
|
||||||
## Funktionale Anforderungen
|
|
||||||
### Version 1
|
|
||||||
- Stammdatenverwaltung (Kunden, Projekte, Tätigkeiten, User)
|
|
||||||
- Zeiterfassung mit Start/Stopp-Funktion und Parameterübernahme aus letzter Buchung
|
|
||||||
- Übersichtliches Dashboard mit letzten Buchungen und Tracker
|
|
||||||
- Aggregierte Reports nach Zeitraum, Projekt, Kunde, Mitarbeiter mit PDF-Export
|
|
||||||
- Grafische Dashboards (Diagramme)
|
|
||||||
|
|
||||||
### Version 2
|
|
||||||
- Projektverwaltung mit Sprints, Tasks
|
|
||||||
- Kanban-Boards zur Aufgabenverwaltung
|
|
||||||
- Direkte Verknüpfung von Tasks mit Zeitbuchungen
|
|
||||||
|
|
||||||
## Sicherheit und Datenschutz
|
|
||||||
- Tenant-Isolation
|
|
||||||
- TLS-Verschlüsselung und verschlüsselte Speicherung sensibler Daten
|
|
||||||
- Audit-Logs (lückenlose Protokollierung)
|
|
||||||
- DSGVO-konform (Datenexport, Recht auf Vergessen)
|
|
||||||
|
|
||||||
## Technische Umsetzung
|
|
||||||
### Backend-Architektur
|
|
||||||
- Modulare Pakete:
|
|
||||||
- Auth
|
|
||||||
- Tenant
|
|
||||||
- User
|
|
||||||
- TimeTracking
|
|
||||||
- Customer
|
|
||||||
- Project
|
|
||||||
- Billing
|
|
||||||
- Reporting
|
|
||||||
- RESTful API mit Middleware zur Tenant-Isolation
|
|
||||||
|
|
||||||
### Frontend-Komponenten
|
|
||||||
- Dashboard (Tracker, letzte Buchungen)
|
|
||||||
- Buchungsformular und Historie
|
|
||||||
- Stammdatenverwaltung (Kunden, Projekte, Tätigkeiten)
|
|
||||||
- Reporting-Seiten mit PDF-Export
|
|
||||||
- Admin-Oberfläche
|
|
||||||
|
|
||||||
## Deployment & DevOps
|
|
||||||
- Docker, Docker-Compose (Entwicklung)
|
|
||||||
- Kubernetes-Manifeste für Produktion
|
|
||||||
- CI/CD mit automatischen Tests und Deployment
|
|
||||||
- Monitoring und Logging (Prometheus, Grafana, ELK)
|
|
||||||
- Regelmäßige Backups, Disaster-Recovery
|
|
||||||
|
|
||||||
## Erweiterbarkeit und Integrationen
|
|
||||||
- API für externe Abrechnungssysteme und Kalenderintegration
|
|
||||||
- Single Sign-On (Google, Microsoft, SAML)
|
|
||||||
- Plugin-System und Custom Fields für Erweiterbarkeit
|
|
||||||
- Analytics und BI-Export
|
|
||||||
|
|
||||||
## Zusätzliche Funktionen
|
|
||||||
- Audit-Logs wichtiger Aktionen
|
|
||||||
- Benachrichtigungssystem (E-Mail, In-App)
|
|
||||||
- Lokalisierung (Mehrsprachigkeit, Zeit- und Datumsformate)
|
|
||||||
|
|
||||||
Diese aktualisierte Spezifikation berücksichtigt alle relevanten Punkte für eine modulare, erweiterbare und sichere Implementierung.
|
|
||||||
|
|
||||||
@@ -1,389 +0,0 @@
|
|||||||
# Zeiterfassungstool - Projektstruktur
|
|
||||||
|
|
||||||
## Backend (Go)
|
|
||||||
|
|
||||||
```
|
|
||||||
timetracker-backend/
|
|
||||||
├── cmd/ # Einstiegspunkte für die Anwendung
|
|
||||||
│ ├── api/ # API-Server
|
|
||||||
│ │ └── main.go
|
|
||||||
│ └── worker/ # Hintergrundprozesse (Reports, Benachrichtigungen usw.)
|
|
||||||
│ └── main.go
|
|
||||||
├── internal/ # Interner Code, nicht exportierbar
|
|
||||||
│ ├── auth/ # Authentifizierung und Autorisierung
|
|
||||||
│ │ ├── jwt.go
|
|
||||||
│ │ ├── middleware.go
|
|
||||||
│ │ ├── permissions.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── tenant/ # Multi-Tenant-Funktionalität
|
|
||||||
│ │ ├── middleware.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── user/ # Benutzerverwaltung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── company/ # Unternehmensverwaltung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── customer/ # Kundenverwaltung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── project/ # Projektverwaltung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── activity/ # Tätigkeitsverwaltung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── timetracking/ # Zeiterfassung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── billing/ # Abrechnung
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── reporting/ # Berichtswesen
|
|
||||||
│ │ ├── generator.go
|
|
||||||
│ │ ├── pdf.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── notification/ # Benachrichtigungen
|
|
||||||
│ │ ├── email.go
|
|
||||||
│ │ ├── inapp.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── kanban/ # Kanban-Board (Version 2)
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── task/ # Task-Management (Version 2)
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── sprint/ # Sprint-Management (Version 2)
|
|
||||||
│ │ ├── model.go
|
|
||||||
│ │ ├── repository.go
|
|
||||||
│ │ └── service.go
|
|
||||||
│ ├── config/ # Konfiguration
|
|
||||||
│ │ └── config.go
|
|
||||||
│ └── database/ # Datenbankzugriff
|
|
||||||
│ ├── migrations/
|
|
||||||
│ ├── seeds/
|
|
||||||
│ └── connection.go
|
|
||||||
├── pkg/ # Öffentliche Pakete, die exportiert werden können
|
|
||||||
│ ├── apierror/ # API-Fehlerobjekte
|
|
||||||
│ │ └── error.go
|
|
||||||
│ ├── logger/ # Logging-Funktionalität
|
|
||||||
│ │ └── logger.go
|
|
||||||
│ ├── validator/ # Validierungsfunktionen
|
|
||||||
│ │ └── validator.go
|
|
||||||
│ └── utils/ # Allgemeine Hilfsfunktionen
|
|
||||||
│ ├── dates.go
|
|
||||||
│ ├── encryption.go
|
|
||||||
│ └── helpers.go
|
|
||||||
├── api/ # API-Definitionen
|
|
||||||
│ ├── handlers/ # API-Handler
|
|
||||||
│ │ ├── auth.go
|
|
||||||
│ │ ├── user.go
|
|
||||||
│ │ ├── company.go
|
|
||||||
│ │ ├── customer.go
|
|
||||||
│ │ ├── project.go
|
|
||||||
│ │ ├── timetracking.go
|
|
||||||
│ │ ├── reporting.go
|
|
||||||
│ │ └── task.go
|
|
||||||
│ ├── middleware/ # API-Middleware
|
|
||||||
│ │ ├── auth.go
|
|
||||||
│ │ ├── tenant.go
|
|
||||||
│ │ └── logging.go
|
|
||||||
│ ├── routes/ # API-Routen
|
|
||||||
│ │ └── routes.go
|
|
||||||
│ └── swagger/ # Swagger-Dokumentation
|
|
||||||
│ └── swagger.yaml
|
|
||||||
├── test/ # Tests
|
|
||||||
│ ├── integration/
|
|
||||||
│ ├── unit/
|
|
||||||
│ └── mocks/
|
|
||||||
├── scripts/ # Skripte für Build, Deployment usw.
|
|
||||||
│ ├── build.sh
|
|
||||||
│ ├── migrate.sh
|
|
||||||
│ └── seed.sh
|
|
||||||
├── docker/ # Docker-Konfiguration
|
|
||||||
│ ├── Dockerfile
|
|
||||||
│ └── docker-compose.yml
|
|
||||||
├── go.mod
|
|
||||||
├── go.sum
|
|
||||||
├── .env.example
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Frontend (NextJS)
|
|
||||||
|
|
||||||
```
|
|
||||||
timetracker-frontend/
|
|
||||||
├── public/ # Statische Dateien
|
|
||||||
│ ├── assets/
|
|
||||||
│ │ ├── images/
|
|
||||||
│ │ ├── icons/
|
|
||||||
│ │ └── fonts/
|
|
||||||
│ └── locales/ # Mehrsprachige Inhalte
|
|
||||||
│ ├── de/
|
|
||||||
│ └── en/
|
|
||||||
├── src/
|
|
||||||
│ ├── app/ # Next.js 13+ App Router
|
|
||||||
│ │ ├── api/ # API-Routen (falls nötig)
|
|
||||||
│ │ ├── (auth)/ # Authentifizierungsseiten
|
|
||||||
│ │ │ ├── login/
|
|
||||||
│ │ │ └── register/
|
|
||||||
│ │ ├── dashboard/ # Dashboard-Seiten
|
|
||||||
│ │ │ ├── page.tsx
|
|
||||||
│ │ │ └── layout.tsx
|
|
||||||
│ │ ├── time-tracking/ # Zeiterfassungsseiten
|
|
||||||
│ │ │ ├── page.tsx
|
|
||||||
│ │ │ ├── [id]/
|
|
||||||
│ │ │ └── components/
|
|
||||||
│ │ ├── projects/ # Projektseiten
|
|
||||||
│ │ │ ├── page.tsx
|
|
||||||
│ │ │ └── [id]/
|
|
||||||
│ │ ├── customers/ # Kundenseiten
|
|
||||||
│ │ │ ├── page.tsx
|
|
||||||
│ │ │ └── [id]/
|
|
||||||
│ │ ├── reports/ # Berichtsseiten
|
|
||||||
│ │ │ ├── page.tsx
|
|
||||||
│ │ │ └── [type]/
|
|
||||||
│ │ ├── admin/ # Administrationsseiten
|
|
||||||
│ │ │ ├── users/
|
|
||||||
│ │ │ ├── companies/
|
|
||||||
│ │ │ └── settings/
|
|
||||||
│ │ ├── kanban/ # Kanban-Boards (Version 2)
|
|
||||||
│ │ │ ├── page.tsx
|
|
||||||
│ │ │ └── [id]/
|
|
||||||
│ │ ├── layout.tsx
|
|
||||||
│ │ └── page.tsx
|
|
||||||
│ ├── components/ # Wiederverwendbare Komponenten
|
|
||||||
│ │ ├── common/ # Allgemeine Komponenten
|
|
||||||
│ │ │ ├── Button.tsx
|
|
||||||
│ │ │ ├── Card.tsx
|
|
||||||
│ │ │ ├── Input.tsx
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ ├── layout/ # Layout-Komponenten
|
|
||||||
│ │ │ ├── Navbar.tsx
|
|
||||||
│ │ │ ├── Sidebar.tsx
|
|
||||||
│ │ │ ├── Footer.tsx
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ ├── dashboard/ # Dashboard-Komponenten
|
|
||||||
│ │ │ ├── ActivityChart.tsx
|
|
||||||
│ │ │ ├── RecentEntries.tsx
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ ├── timetracker/ # Zeiterfassungskomponenten
|
|
||||||
│ │ │ ├── Timer.tsx
|
|
||||||
│ │ │ ├── EntryForm.tsx
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ ├── reports/ # Berichtskomponenten
|
|
||||||
│ │ │ ├── ReportFilter.tsx
|
|
||||||
│ │ │ ├── Chart.tsx
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ └── kanban/ # Kanban-Komponenten (Version 2)
|
|
||||||
│ │ ├── Board.tsx
|
|
||||||
│ │ ├── Column.tsx
|
|
||||||
│ │ ├── Card.tsx
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── hooks/ # Custom React Hooks
|
|
||||||
│ │ ├── useAuth.ts
|
|
||||||
│ │ ├── useTimeTracking.ts
|
|
||||||
│ │ ├── useProjects.ts
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── lib/ # Hilfsfunktionen und Bibliotheken
|
|
||||||
│ │ ├── api.ts # API Client
|
|
||||||
│ │ ├── auth.ts # Auth-Utilities
|
|
||||||
│ │ ├── date-utils.ts # Date-Helpers
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── types/ # TypeScript-Typdefinitionen
|
|
||||||
│ │ ├── auth.ts
|
|
||||||
│ │ ├── user.ts
|
|
||||||
│ │ ├── timeTracking.ts
|
|
||||||
│ │ ├── project.ts
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── store/ # State Management (falls benötigt)
|
|
||||||
│ │ ├── slices/
|
|
||||||
│ │ └── index.ts
|
|
||||||
│ ├── styles/ # CSS/SCSS Styles
|
|
||||||
│ │ ├── globals.css
|
|
||||||
│ │ └── theme.ts
|
|
||||||
│ └── utils/ # Allgemeine Hilfsfunktionen
|
|
||||||
│ ├── format.ts
|
|
||||||
│ ├── validation.ts
|
|
||||||
│ └── ...
|
|
||||||
├── .env.local.example
|
|
||||||
├── .eslintrc.json
|
|
||||||
├── next.config.js
|
|
||||||
├── package.json
|
|
||||||
├── tailwind.config.js
|
|
||||||
├── tsconfig.json
|
|
||||||
├── jest.config.js # Test-Konfiguration
|
|
||||||
├── postcss.config.js # PostCSS-Konfiguration
|
|
||||||
└── README.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Datenbankschema (PostgreSQL)
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Multi-Tenant
|
|
||||||
CREATE TABLE companies (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
address TEXT,
|
|
||||||
contact_email VARCHAR(255),
|
|
||||||
contact_phone VARCHAR(50),
|
|
||||||
logo_url TEXT,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Benutzer und Rollen
|
|
||||||
CREATE TABLE roles (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(50) UNIQUE NOT NULL,
|
|
||||||
permissions JSONB
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE users (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID REFERENCES companies(id),
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
password_hash VARCHAR(255) NOT NULL,
|
|
||||||
first_name VARCHAR(100),
|
|
||||||
last_name VARCHAR(100),
|
|
||||||
role_id INTEGER REFERENCES roles(id),
|
|
||||||
hourly_rate DECIMAL(10, 2),
|
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Kunden
|
|
||||||
CREATE TABLE customers (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
contact_person VARCHAR(255),
|
|
||||||
email VARCHAR(255),
|
|
||||||
phone VARCHAR(50),
|
|
||||||
address TEXT,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Projekte
|
|
||||||
CREATE TABLE projects (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
customer_id UUID REFERENCES customers(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
start_date DATE,
|
|
||||||
end_date DATE,
|
|
||||||
status VARCHAR(50),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tätigkeiten
|
|
||||||
CREATE TABLE activities (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
billing_rate DECIMAL(10, 2),
|
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Zeitbuchungen
|
|
||||||
CREATE TABLE time_entries (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
|
||||||
project_id UUID NOT NULL REFERENCES projects(id),
|
|
||||||
activity_id UUID NOT NULL REFERENCES activities(id),
|
|
||||||
start_time TIMESTAMP NOT NULL,
|
|
||||||
end_time TIMESTAMP NOT NULL,
|
|
||||||
duration INTEGER NOT NULL, -- in minutes
|
|
||||||
description TEXT,
|
|
||||||
billable_percentage INTEGER NOT NULL DEFAULT 100,
|
|
||||||
billing_rate DECIMAL(10, 2),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Version 2: Sprint/Task Management
|
|
||||||
CREATE TABLE sprints (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
project_id UUID NOT NULL REFERENCES projects(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
start_date DATE NOT NULL,
|
|
||||||
end_date DATE NOT NULL,
|
|
||||||
status VARCHAR(50),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE task_statuses (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
color VARCHAR(7),
|
|
||||||
position INTEGER NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE tasks (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
project_id UUID NOT NULL REFERENCES projects(id),
|
|
||||||
sprint_id UUID REFERENCES sprints(id),
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
assignee_id UUID REFERENCES users(id),
|
|
||||||
status_id INTEGER REFERENCES task_statuses(id),
|
|
||||||
priority VARCHAR(50),
|
|
||||||
estimate INTEGER, -- in minutes
|
|
||||||
due_date TIMESTAMP,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE kanban_boards (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
company_id UUID NOT NULL REFERENCES companies(id),
|
|
||||||
project_id UUID NOT NULL REFERENCES projects(id),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE kanban_columns (
|
|
||||||
id UUID PRIMARY KEY,
|
|
||||||
board_id UUID NOT NULL REFERENCES kanban_boards(id),
|
|
||||||
name VARCHAR(100) NOT NULL,
|
|
||||||
position INTEGER NOT NULL,
|
|
||||||
task_status_id INTEGER REFERENCES task_statuses(id),
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Verknüpfung zwischen Zeitbuchungen und Tasks
|
|
||||||
ALTER TABLE time_entries ADD COLUMN task_id UUID REFERENCES tasks(id);
|
|
||||||
|
|
||||||
-- Indizes für Performance
|
|
||||||
CREATE INDEX idx_time_entries_user ON time_entries(user_id);
|
|
||||||
CREATE INDEX idx_time_entries_project ON time_entries(project_id);
|
|
||||||
CREATE INDEX idx_time_entries_date ON time_entries(start_time);
|
|
||||||
CREATE INDEX idx_projects_company ON projects(company_id);
|
|
||||||
CREATE INDEX idx_users_company ON users(company_id);
|
|
||||||
CREATE INDEX idx_tasks_project ON tasks(project_id);
|
|
||||||
CREATE INDEX idx_tasks_sprint ON tasks(sprint_id);
|
|
||||||
```
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
Okay, ich schlage dir eine detaillierte Projektstruktur für ein Time Tracking and Management System vor, das in Go (Backend) und Next.js (Frontend) mit FPGO und FPTS umgesetzt wird. Das System soll Multi-Tenant-fähig sein, PostgreSQL als Datenbank verwenden und modular, übersichtlich sowie für die Entwicklung mit LLMs geeignet sein. Die Struktur ist in zwei Hauptteile gegliedert: Backend und Frontend. Hier ist der Vorschlag:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Projektstruktur
|
|
||||||
|
|
||||||
### 1. Backend (Go)
|
|
||||||
|
|
||||||
Das Backend ist in Go geschrieben und folgt den Prinzipien von **Clean Architecture** und **Domain-Driven Design (DDD)**, um die Geschäftslogik klar von technischen Details zu trennen. Dies sorgt für Testbarkeit und Erweiterbarkeit.
|
|
||||||
|
|
||||||
```
|
|
||||||
backend-go/
|
|
||||||
├── cmd/
|
|
||||||
│ └── server/
|
|
||||||
│ └── main.go # Einstiegspunkt der Anwendung
|
|
||||||
├── internal/ # Nicht-öffentlicher Code
|
|
||||||
│ ├── domain/ # Domänen-Schicht: Geschäftlogik und Entitäten
|
|
||||||
│ │ ├── entities/ # Domänenmodelle
|
|
||||||
│ │ │ ├── user.go
|
|
||||||
│ │ │ ├── company.go
|
|
||||||
│ │ │ ├── project.go
|
|
||||||
│ │ │ ├── client.go
|
|
||||||
│ │ │ ├── activity.go
|
|
||||||
│ │ │ ├── employee.go
|
|
||||||
│ │ │ └── booking.go
|
|
||||||
│ │ ├── repositories/ # Interfaces für Datenpersistenz
|
|
||||||
│ │ │ ├── user_repository.go
|
|
||||||
│ │ │ ├── company_repository.go
|
|
||||||
│ │ │ ├── project_repository.go
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ └── services/ # Domänenservices für Anwendungsfälle
|
|
||||||
│ │ ├── user_service.go
|
|
||||||
│ │ ├── booking_service.go
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── application/ # Anwendungsschicht: API-spezifische Logik
|
|
||||||
│ │ └── services/
|
|
||||||
│ │ └── api_service.go
|
|
||||||
│ ├── infrastructure/ # Infrastrukturschicht: Externe Abhängigkeiten
|
|
||||||
│ │ ├── persistence/ # Implementierung der Repositories mit GORM
|
|
||||||
│ │ │ ├── gorm_user_repository.go
|
|
||||||
│ │ │ ├── gorm_company_repository.go
|
|
||||||
│ │ │ └── ...
|
|
||||||
│ │ └── config/ # Konfigurationsmanagement
|
|
||||||
│ │ └── config.go
|
|
||||||
│ └── interfaces/ # Schnittstellenschicht: API-Handler
|
|
||||||
│ └── http/
|
|
||||||
│ ├── handlers/ # HTTP-Handler für API-Endpunkte
|
|
||||||
│ │ ├── auth_handler.go
|
|
||||||
│ │ ├── booking_handler.go
|
|
||||||
│ │ ├── project_handler.go
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── middleware/ # Middleware für Auth und Tenant-Isolierung
|
|
||||||
│ ├── auth_middleware.go
|
|
||||||
│ └── tenant_middleware.go
|
|
||||||
├── pkg/ # Öffentliche, wiederverwendbare Packages
|
|
||||||
│ ├── logger.go
|
|
||||||
│ └── utils.go
|
|
||||||
├── tests/ # Tests
|
|
||||||
│ ├── unit/
|
|
||||||
│ └── integration/
|
|
||||||
├── docs/ # API-Dokumentation
|
|
||||||
│ └── swagger.yaml
|
|
||||||
├── go.mod
|
|
||||||
└── go.sum
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Erläuterung:
|
|
||||||
- **cmd/**: Enthält den Einstiegspunkt (`main.go`), der den Server startet.
|
|
||||||
- **internal/**: Nicht-öffentlicher Code, unterteilt in:
|
|
||||||
- **domain/**: Kernentitäten (z.B. `User`, `Company`), Repository-Interfaces und Domänenservices.
|
|
||||||
- **application/**: API-spezifische Logik, die Domänenservices nutzt.
|
|
||||||
- **infrastructure/**: Technische Implementierungen wie Datenbankzugriff (GORM für PostgreSQL) und Konfiguration.
|
|
||||||
- **interfaces/**: HTTP-Handler (mit Gin) und Middleware für Authentifizierung und Multi-Tenant-Isolierung.
|
|
||||||
- **pkg/**: Öffentliche Hilfsfunktionen (z.B. Logger).
|
|
||||||
- **tests/**: Unit- und Integrationstests.
|
|
||||||
- **docs/**: Swagger-Dokumentation für die API.
|
|
||||||
|
|
||||||
### 2. Frontend (Next.js)
|
|
||||||
|
|
||||||
Das Frontend nutzt **Next.js** mit **TypeScript** und **FPTS** für funktionale Programmierung. Die Struktur ist typisch für Next.js und fördert Wiederverwendbarkeit.
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend-nextjs/
|
|
||||||
├── pages/ # Seitenkomponenten, die Routen entsprechen
|
|
||||||
│ ├── index.tsx # Startseite
|
|
||||||
│ ├── login.tsx
|
|
||||||
│ ├── dashboard.tsx
|
|
||||||
│ ├── bookings.tsx
|
|
||||||
│ └── ...
|
|
||||||
├── components/ # Wiederverwendbare UI-Komponenten
|
|
||||||
│ ├── Header.tsx
|
|
||||||
│ ├── BookingForm.tsx
|
|
||||||
│ ├── ProjectList.tsx
|
|
||||||
│ └── ...
|
|
||||||
├── lib/ # Hilfsfunktionen und API-Calls
|
|
||||||
│ ├── api.ts # Axios-Instanz für API-Anfragen
|
|
||||||
│ ├── auth.ts # Authentifizierungslogik
|
|
||||||
│ └── ...
|
|
||||||
├── styles/ # Stylesheets
|
|
||||||
│ ├── globals.css
|
|
||||||
│ └── module.css
|
|
||||||
├── public/ # Statische Dateien
|
|
||||||
│ └── images/
|
|
||||||
├── types/ # TypeScript-Typdefinitionen
|
|
||||||
│ ├── user.ts
|
|
||||||
│ ├── booking.ts
|
|
||||||
│ └── ...
|
|
||||||
├── tests/ # Tests für Komponenten und Seiten
|
|
||||||
│ ├── components/
|
|
||||||
│ └── pages/
|
|
||||||
├── next.config.js
|
|
||||||
├── package.json
|
|
||||||
└── tsconfig.json
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Erläuterung:
|
|
||||||
- **pages/**: Seiten der Anwendung (z.B. `dashboard.tsx` für `/dashboard`).
|
|
||||||
- **components/**: Wiederverwendbare UI-Komponenten (z.B. `BookingForm.tsx`).
|
|
||||||
- **lib/**: Hilfsfunktionen, z.B. für API-Calls mit Axios.
|
|
||||||
- **styles/**: Globale und modulare CSS-Dateien.
|
|
||||||
- **public/**: Statische Dateien wie Bilder.
|
|
||||||
- **types/**: Typdefinitionen für Typsicherheit.
|
|
||||||
- **tests/**: Tests mit Tools wie Jest oder Cypress.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Multi-Tenant-Unterstützung
|
|
||||||
|
|
||||||
Die Multi-Tenant-Fähigkeit wird durch eine **Tenant-ID** (z.B. `company_id`) in den Datenbanktabellen umgesetzt. Das Backend verwendet ein **tenant_middleware.go**, das sicherstellt, dass Benutzer nur Daten ihres eigenen Tenants sehen, indem die `company_id` in jeder Anfrage geprüft wird.
|
|
||||||
|
|
||||||
## API-Dokumentation
|
|
||||||
|
|
||||||
Die API wird mit **Swagger** dokumentiert, gespeichert in `backend-go/docs/swagger.yaml`. Dies bietet eine klare, interaktive Übersicht der Endpunkte.
|
|
||||||
|
|
||||||
## Entwicklung mit LLMs
|
|
||||||
|
|
||||||
Für die Entwicklung mit LLMs ist der Code in kleine, klar benannte Dateien aufgeteilt (z.B. `user.go`, `booking_service.go`), die jeweils eine spezifische Aufgabe haben. Dies erleichtert LLMs das Verständnis und die Generierung von Code.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fazit
|
|
||||||
|
|
||||||
Diese Struktur ist modular, übersichtlich und erweiterbar. Sie nutzt bewährte Praktiken für Go und Next.js, unterstützt Multi-Tenant-Fähigkeit und ist durch die Aufteilung in kleine Module ideal für die Entwicklung mit LLMs. Wenn du Anpassungen oder mehr Details möchtest, lass es mich wissen!
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Spezifikation Zeiterfassungs-Tool
|
|
||||||
|
|
||||||
## Technische Rahmenbedingungen
|
|
||||||
- **Backend:** Go mit fp-go
|
|
||||||
- **Frontend:** Next.js mit fp-ts
|
|
||||||
- **Datenbank:** PostgreSQL mit ORM (z.B. GORM oder Bun)
|
|
||||||
- **API-Dokumentation:** Swagger/OpenAPI
|
|
||||||
- **PDF-Generierung:** serverseitig
|
|
||||||
|
|
||||||
## Multi-Tenancy & Rollen
|
|
||||||
- **Mandantenfähig**: Separierung aller Daten nach Unternehmen (Company)
|
|
||||||
- **Rollen:**
|
|
||||||
- Admin (systemweit)
|
|
||||||
- Company (Unternehmensebene)
|
|
||||||
- Manager
|
|
||||||
- Auditor (Read-only Zugriff auf Reports)
|
|
||||||
- User (regulärer Mitarbeiter)
|
|
||||||
|
|
||||||
## Datenmodelle (DB Entities)
|
|
||||||
- **Company**
|
|
||||||
- Kunden (Clients)
|
|
||||||
- Projekte (Projects)
|
|
||||||
- Tätigkeiten (Activities)
|
|
||||||
- **User**
|
|
||||||
- Stundenrate
|
|
||||||
- Rolle
|
|
||||||
- **Projekt**
|
|
||||||
- Kundenreferenz
|
|
||||||
- Beschreibung
|
|
||||||
- **Tätigkeit**
|
|
||||||
- Name
|
|
||||||
- Verrechnungspreis
|
|
||||||
- **Buchung**
|
|
||||||
- Zeit (Start, Ende, Dauer)
|
|
||||||
- User
|
|
||||||
- Projekt
|
|
||||||
- Tätigkeit
|
|
||||||
- Beschreibung (optional)
|
|
||||||
- Abrechenbarkeit (0-100%)
|
|
||||||
- Verrechnungspreis (berechnet)
|
|
||||||
|
|
||||||
## Funktionale Anforderungen
|
|
||||||
### Version 1
|
|
||||||
- Verwaltung von Kunden, Projekten, Tätigkeiten
|
|
||||||
- Mitarbeiterverwaltung mit Stundensatz
|
|
||||||
- Erstellung von Zeitbuchungen mit optionaler Beschreibung und wählbarer Abrechenbarkeit
|
|
||||||
- Automatische Übernahme der Parameter (Projekt, Tätigkeit, Beschreibung) der letzten Buchung
|
|
||||||
- Live-Tracker mit Start-/Stopp-Funktion auf Dashboard
|
|
||||||
- Reporting-Funktion
|
|
||||||
- Aggregation der Buchungen nach Zeitraum, Projekt, Kunde, Mitarbeiter
|
|
||||||
- Export als PDF
|
|
||||||
- Grafische Dashboards (Kreis-/Balkendiagramme, Zeitverläufe)
|
|
||||||
- Persönliches Dashboard
|
|
||||||
- Liste vergangener Buchungen
|
|
||||||
- Schnellstart-Tracker
|
|
||||||
|
|
||||||
### Version 2 (Ausblick)
|
|
||||||
- Projektverwaltung mit Sprints und Task-Items
|
|
||||||
- Kanban-Board zur Verwaltung von Tasks
|
|
||||||
- Zuordnung von Tasks direkt bei Buchungsauswahl
|
|
||||||
|
|
||||||
## API-Endpunkte
|
|
||||||
- Authentifizierung & Autorisierung (JWT o.ä.)
|
|
||||||
- CRUD-Operationen für alle Entities
|
|
||||||
- Endpunkte für Zeiterfassung, Buchungen starten/stoppen
|
|
||||||
- Reporting-Endpoints mit Filterparametern (Zeitraum, Kunde, Projekt, Mitarbeiter)
|
|
||||||
|
|
||||||
## Frontend-Komponenten
|
|
||||||
- **Dashboard**
|
|
||||||
- Tracker-Komponente
|
|
||||||
- Liste letzter Buchungen
|
|
||||||
- Schnelleinstieg für häufige Aktionen
|
|
||||||
- **Buchungs-Management**
|
|
||||||
- Buchungsformular
|
|
||||||
- Historie
|
|
||||||
- **Verwaltungsseiten**
|
|
||||||
- Kunden, Projekte, Tätigkeiten, User
|
|
||||||
- **Reporting-Ansicht**
|
|
||||||
- Interaktive Filtermöglichkeiten
|
|
||||||
- Grafische Darstellung (Charts)
|
|
||||||
- PDF-Export
|
|
||||||
|
|
||||||
## Technische Umsetzung
|
|
||||||
- Modularer Aufbau (separate Pakete für Entities, Services, Controllers)
|
|
||||||
- DDD-Prinzipien für saubere Trennung von Geschäftslogik
|
|
||||||
- APIs sauber nach Verantwortlichkeiten getrennt (RESTful Design)
|
|
||||||
- Swagger zur API-Dokumentation (generiert aus Annotationen)
|
|
||||||
- Strukturierter und übersichtlicher Code (kleine Dateien, Single Responsibility)
|
|
||||||
|
|
||||||
Diese Struktur ermöglicht eine überschaubare Implementierung mit optimaler Unterstützung durch LLMs.
|
|
||||||
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
Ich habe Ihre Anfrage analysiert und die bestehende Dokumentation aktualisiert, um Ihre Anforderungen für ein Zeiterfassungstool zu berücksichtigen. Das Tool soll als Multi-Tenant-Lösung fungieren, in Go (Backend) und Next.js (Frontend) mit FPGO und FPTS für funktionale Programmierung umgesetzt werden. PostgreSQL wird als Datenbank mit einem ORM verwendet, und eine Swagger-Dokumentation wird für die API erstellt. Der Code wird in kleinere, übersichtliche Module und Dateien aufgeteilt, um die Implementierung mit LLMs zu erleichtern. Im Folgenden finden Sie die überarbeitete Spezifikation, die alle Anforderungen für Version 1 sowie geplante Erweiterungen für Version 2 abdeckt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Software Requirements Specification
|
|
||||||
## für das Time Tracking and Management System
|
|
||||||
**Version 1.3**
|
|
||||||
**Erstellt von:** Jean Jacques Avril, Masterstudent, Informatik, Technische Hochschule Rosenheim
|
|
||||||
**Modul:** Konzepte der Programmiersprachen
|
|
||||||
**Datum:** 5. Januar 2025
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Einführung
|
|
||||||
|
|
||||||
### 1.1 Zweck
|
|
||||||
Dieses Software Requirements Specification (SRS) Dokument beschreibt detailliert die Anforderungen für das Time Tracking and Management System, eine Multi-Tenant-Lösung zur präzisen Erfassung und Verwaltung von Arbeitszeiten für mehrere Unternehmen. Jedes Unternehmen kann eigene Kunden, Projekte und Mitarbeiter verwalten. Das System unterstützt verschiedene Benutzerrollen (User, Manager, Auditor, Company, Admin) mit spezifischen Berechtigungen und Funktionen. Ziel ist es, eine klare Grundlage für die Entwicklung zu schaffen, die eine einfache Implementierung und Erweiterbarkeit ermöglicht.
|
|
||||||
|
|
||||||
### 1.2 Zielgruppe und Leseempfehlungen
|
|
||||||
Dieses Dokument richtet sich an:
|
|
||||||
- **Projektbeteiligte:** Für ein umfassendes Verständnis der Funktionen und Einschränkungen.
|
|
||||||
- **Entwickler:** Zur Einsicht in funktionale und nicht-funktionale Anforderungen für eine reibungslose Umsetzung.
|
|
||||||
- **Qualitätssicherungsteams:** Um Tests an den spezifizierten Anforderungen auszurichten.
|
|
||||||
- **Zukünftige Mitwirkende:** Um die Systemarchitektur zu verstehen und Erweiterungen beizutragen.
|
|
||||||
|
|
||||||
Leser sollten zunächst die funktionalen Anforderungen durchsehen, um die Kernfunktionen zu verstehen, gefolgt von den nicht-funktionalen Anforderungen für Einblicke in Sicherheit und Performance. Entwickler mit Fokus auf Backend und Architektur sollten die Abschnitte zur Systemarchitektur und Technologie besonders beachten.
|
|
||||||
|
|
||||||
### 1.3 Projektumfang
|
|
||||||
Das Time Tracking and Management System bietet eine benutzerfreundliche Plattform zur Zeiterfassung, Verwaltung von Projekten und Erstellung von Berichten in einer Multi-Tenant-Umgebung.
|
|
||||||
|
|
||||||
**Version 1 (Initiale Version):**
|
|
||||||
- Multi-Tenant-Unterstützung mit Datenisolierung.
|
|
||||||
- Benutzerverwaltung mit Rollen: User, Manager, Auditor, Company, Admin.
|
|
||||||
- Verwaltung von Kunden, Projekten und Tätigkeiten je Unternehmen.
|
|
||||||
- Zeiterfassung mit Buchungen (Zeit, Ort, Zweck, Mitarbeiter, Tätigkeit, Beschreibung, Verrechnungspreis, abrechenbarer Prozentsatz).
|
|
||||||
- Berichte und Dashboards mit PDF-Export und grafischen Darstellungen.
|
|
||||||
- Tracker auf der Startseite mit Standardwerten der letzten Buchung.
|
|
||||||
|
|
||||||
**Version 2 (Zukünftige Erweiterungen):**
|
|
||||||
- Projekte mit Sprints und Task-Items.
|
|
||||||
- Kanban-Boards zur Aufgabenzuweisung.
|
|
||||||
- Direkte Task-Auswahl bei Buchungen.
|
|
||||||
|
|
||||||
Die modulare Architektur ermöglicht einfache Erweiterungen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Anforderungen
|
|
||||||
|
|
||||||
### 2.1 Funktionale Anforderungen
|
|
||||||
|
|
||||||
#### FA-1: Multi-Tenant-Unterstützung
|
|
||||||
- **Beschreibung:** Das System muss mehrere Unternehmen (Tenants) unterstützen, jedes mit isolierten Daten.
|
|
||||||
- **Details:** Jedes Unternehmen hat eigene Kunden, Projekte, Tätigkeiten, Mitarbeiter und Buchungen.
|
|
||||||
- **Validierung:** Kein Zugriff auf Daten anderer Tenants möglich.
|
|
||||||
|
|
||||||
#### FA-2: Benutzerverwaltung
|
|
||||||
- **Beschreibung:** Benutzer können sich registrieren, anmelden und Rollen zugewiesen bekommen.
|
|
||||||
- **Details:**
|
|
||||||
- Rollen: User (Zeiterfassung), Manager (Projektübersicht), Auditor (Berichte einsehen), Company (Verwaltung), Admin (Systemweit).
|
|
||||||
- Admin verwaltet Unternehmen, Company verwaltet eigene Daten.
|
|
||||||
- **Validierung:** Eindeutige Benutzernamen und E-Mails pro Tenant.
|
|
||||||
|
|
||||||
#### FA-3: Kundenverwaltung
|
|
||||||
- **Beschreibung:** Unternehmen können Kunden anlegen, bearbeiten und löschen.
|
|
||||||
- **Details:** Kunden sind unternehmensspezifisch.
|
|
||||||
- **Validierung:** Nur Company- und Admin-Rollen dürfen Kunden verwalten.
|
|
||||||
|
|
||||||
#### FA-4: Projektverwaltung
|
|
||||||
- **Beschreibung:** Unternehmen können Projekte für Kunden anlegen, bearbeiten und löschen.
|
|
||||||
- **Details:** Projekte sind mit Kunden und Unternehmen verknüpft.
|
|
||||||
- **Validierung:** Nur Company- und Manager-Rollen dürfen Projekte verwalten.
|
|
||||||
|
|
||||||
#### FA-5: Tätigkeitsverwaltung
|
|
||||||
- **Beschreibung:** Unternehmen können Tätigkeiten mit Verrechnungspreisen definieren.
|
|
||||||
- **Details:** Tätigkeiten sind unternehmensspezifisch.
|
|
||||||
- **Validierung:** Verrechnungspreise müssen positiv sein.
|
|
||||||
|
|
||||||
#### FA-6: Mitarbeiterverwaltung
|
|
||||||
- **Beschreibung:** Unternehmen können Mitarbeiter mit Stundensätzen verwalten.
|
|
||||||
- **Details:** Mitarbeiter sind unternehmensspezifisch.
|
|
||||||
- **Validierung:** Stundensätze müssen positiv sein.
|
|
||||||
|
|
||||||
#### FA-7: Buchungsverwaltung
|
|
||||||
- **Beschreibung:** Benutzer können Zeitbuchungen erfassen.
|
|
||||||
- **Details:** Buchungen enthalten Zeit, Ort, Zweck, Mitarbeiter, Tätigkeit, optionale Beschreibung, Verrechnungspreis und abrechenbaren Prozentsatz (0-100%).
|
|
||||||
- **Validierung:** Buchungen müssen gültige Projekte und Tätigkeiten referenzieren.
|
|
||||||
|
|
||||||
#### FA-8: Zeiterfassung
|
|
||||||
- **Beschreibung:** Benutzer können einen Tracker starten und stoppen.
|
|
||||||
- **Details:** Ohne Auswahl werden die Parameter der letzten Buchung übernommen.
|
|
||||||
- **Validierung:** Kein Start möglich, wenn bereits aktiv.
|
|
||||||
|
|
||||||
#### FA-9: Berichte
|
|
||||||
- **Beschreibung:** Berichte können über Projekte, Mitarbeiter, Kunden und Zeiträume erstellt werden.
|
|
||||||
- **Details:** Berichte enthalten Arbeitszeit, abrechenbare Beträge und können als PDF exportiert werden.
|
|
||||||
- **Validierung:** Nur autorisierte Rollen dürfen Berichte generieren.
|
|
||||||
|
|
||||||
#### FA-10: Dashboard
|
|
||||||
- **Beschreibung:** Benutzer sehen auf der Startseite letzte Buchungen und einen Tracker.
|
|
||||||
- **Details:** Dashboards zeigen grafische Zusammenfassungen.
|
|
||||||
|
|
||||||
### 2.2 Nicht-funktionale Anforderungen
|
|
||||||
|
|
||||||
#### NFA-1: Sicherheit
|
|
||||||
- **Beschreibung:** Datenisolierung zwischen Tenants und sicherer Zugriff.
|
|
||||||
- **Details:** RBAC, JWT-Authentifizierung, Verschlüsselung sensibler Daten.
|
|
||||||
|
|
||||||
#### NFA-2: Performance
|
|
||||||
- **Beschreibung:** API-Antwortzeit unter 200 ms.
|
|
||||||
- **Details:** Optimierte Datenbankabfragen und Caching.
|
|
||||||
|
|
||||||
#### NFA-3: Skalierbarkeit
|
|
||||||
- **Beschreibung:** Unterstützung vieler gleichzeitiger Benutzer und Tenants.
|
|
||||||
- **Details:** Horizontale Skalierung möglich.
|
|
||||||
|
|
||||||
#### NFA-4: Benutzbarkeit
|
|
||||||
- **Beschreibung:** Intuitive und responsive Benutzeroberfläche.
|
|
||||||
- **Details:** Kompatibel mit Desktop, Tablet und Mobilgeräten.
|
|
||||||
|
|
||||||
#### NFA-5: Modularität
|
|
||||||
- **Beschreibung:** Code in kleine, überschaubare Module aufteilen.
|
|
||||||
- **Details:** Erleichtert die Entwicklung mit LLMs.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Technische Spezifikation
|
|
||||||
|
|
||||||
### 3.1 Gesamtarchitektur
|
|
||||||
Das System ist eine Full-Stack-Webanwendung:
|
|
||||||
- **Frontend:** Next.js mit React und FPTS.
|
|
||||||
- **Backend:** Go mit FPGO.
|
|
||||||
- **Datenbank:** PostgreSQL mit GORM als ORM.
|
|
||||||
- **API-Dokumentation:** Swagger für RESTful APIs.
|
|
||||||
- **Kommunikation:** RESTful APIs, optional WebSockets für Echtzeitfunktionen.
|
|
||||||
|
|
||||||
### 3.2 Technologie-Stack
|
|
||||||
#### 3.2.1 Frontend
|
|
||||||
- **Next.js/React:** Für interaktive Benutzeroberflächen.
|
|
||||||
- **FPTS:** Funktionale Programmierung in TypeScript.
|
|
||||||
- **Axios:** Für HTTP-Anfragen.
|
|
||||||
|
|
||||||
#### 3.2.2 Backend
|
|
||||||
- **Go:** Hauptsprache für Backend.
|
|
||||||
- **FPGO:** Funktionale Programmierung in Go.
|
|
||||||
- **GORM:** ORM für PostgreSQL.
|
|
||||||
- **Gin:** Web-Framework für HTTP-Anfragen.
|
|
||||||
- **JWT:** Für sichere Authentifizierung.
|
|
||||||
|
|
||||||
#### 3.2.3 Datenbank
|
|
||||||
- **PostgreSQL:** Relationale Datenbank.
|
|
||||||
|
|
||||||
### 3.3 Entitäten
|
|
||||||
- **Company:** Tenant-Daten.
|
|
||||||
- **User:** Mit Rollen und Unternehmenszuordnung.
|
|
||||||
- **Client:** Unternehmensspezifisch.
|
|
||||||
- **Project:** Mit Kunden und Unternehmen verknüpft.
|
|
||||||
- **Activity:** Mit Verrechnungspreis.
|
|
||||||
- **Employee:** Mit Stundensatz.
|
|
||||||
- **Booking:** Zeitbuchungen mit Details.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Systemdesign
|
|
||||||
|
|
||||||
### 4.1 Übersicht
|
|
||||||
Das System folgt Domain-Driven Design (DDD) und Clean Architecture mit einer Multi-Tenant-Architektur (gemeinsame Datenbank mit Tenant-IDs).
|
|
||||||
|
|
||||||
### 4.2 Architekturdesign
|
|
||||||
- **Domain-Schicht:** Geschäftlogik und Entitäten.
|
|
||||||
- **Application-Schicht:** Services und Anwendungsfälle.
|
|
||||||
- **Infrastructure-Schicht:** Datenbankzugriff via GORM.
|
|
||||||
- **Interface-Schicht:** API-Handler.
|
|
||||||
- **Frontend:** Next.js mit FPTS.
|
|
||||||
|
|
||||||
### 4.3 Moduldesign
|
|
||||||
- **Benutzerverwaltung:** Registrierung, Authentifizierung.
|
|
||||||
- **Unternehmensverwaltung:** Tenant-Daten.
|
|
||||||
- **Projektverwaltung:** Projektzuweisungen.
|
|
||||||
- **Buchungsverwaltung:** Zeiterfassung.
|
|
||||||
- **Berichte:** Generierung und Export.
|
|
||||||
|
|
||||||
### 4.4 Datenbankdesign
|
|
||||||
- **Companies:** id, name, created_at, updated_at.
|
|
||||||
- **Users:** id, company_id, role, name, email, password_hash.
|
|
||||||
- **Clients:** id, company_id, name.
|
|
||||||
- **Projects:** id, client_id, company_id, name.
|
|
||||||
- **Activities:** id, company_id, name, billing_rate.
|
|
||||||
- **Employees:** id, company_id, name, hourly_rate.
|
|
||||||
- **Bookings:** id, user_id, project_id, activity_id, start_time, end_time, description, billable_percentage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. API-Endpunkte
|
|
||||||
|
|
||||||
### 5.1 Übersicht
|
|
||||||
RESTful API mit Swagger-Dokumentation, gesichert durch JWT und RBAC.
|
|
||||||
|
|
||||||
### 5.2 Endpunkte
|
|
||||||
#### 5.2.1 Authentifizierung
|
|
||||||
- **POST /api/auth/register:** Neuer Benutzer.
|
|
||||||
- **POST /api/auth/login:** Login mit JWT.
|
|
||||||
- **POST /api/auth/logout:** Logout.
|
|
||||||
- **GET /api/auth/me:** Benutzerdetails.
|
|
||||||
|
|
||||||
#### 5.2.2 Unternehmensverwaltung (Admin)
|
|
||||||
- **POST /api/companies:** Neues Unternehmen.
|
|
||||||
- **GET /api/companies:** Liste aller Unternehmen.
|
|
||||||
- **PUT /api/companies/{id}:** Unternehmen bearbeiten.
|
|
||||||
- **DELETE /api/companies/{id}:** Unternehmen löschen.
|
|
||||||
|
|
||||||
#### 5.2.3 Buchungsverwaltung
|
|
||||||
- **POST /api/bookings:** Neue Buchung.
|
|
||||||
- **GET /api/bookings:** Buchungen auflisten.
|
|
||||||
- **PUT /api/bookings/{id}:** Buchung bearbeiten.
|
|
||||||
- **DELETE /api/bookings/{id}:** Buchung löschen.
|
|
||||||
|
|
||||||
#### 5.2.4 Berichte
|
|
||||||
- **GET /api/reports:** Bericht generieren.
|
|
||||||
- **GET /api/reports/export:** PDF-Export.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Sicherheitsaspekte
|
|
||||||
- **Authentifizierung:** JWT-Token.
|
|
||||||
- **RBAC:** Rollenbasierte Zugriffskontrolle.
|
|
||||||
- **Datenisolierung:** Tenant-spezifischer Zugriff via Company-ID.
|
|
||||||
- **Verschlüsselung:** Sensible Daten verschlüsselt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Diese Spezifikation bietet eine klare Anleitung für die Entwicklung eines Multi-Tenant-Zeiterfassungstools mit modularer Struktur, das leicht mit LLMs implementiert werden kann. Die Version 2-Anforderungen sind als zukünftige Erweiterungen berücksichtigt.
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Data Transfer Objects (DTOs)
|
||||||
|
|
||||||
|
This document describes the Data Transfer Objects (DTOs) used in the Time Tracker application. DTOs are used to transfer data between the backend and frontend, and between different layers of the backend.
|
||||||
|
|
||||||
|
## ActivityDto
|
||||||
|
|
||||||
|
The `ActivityDto` type represents a specific activity that can be tracked, such as "Development", "Meeting", or "Bug Fixing". It is used to transfer activity data between the backend and frontend.
|
||||||
|
|
||||||
|
## CompanyDto
|
||||||
|
|
||||||
|
The `CompanyDto` type represents a tenant in the multi-tenant application. Each company has its own set of users, customers, projects, and activities. It is used to transfer company data between the backend and frontend.
|
||||||
|
|
||||||
|
## CustomerDto
|
||||||
|
|
||||||
|
The `CustomerDto` type represents a customer of a company. It is used to transfer customer data between the backend and frontend.
|
||||||
|
|
||||||
|
## ProjectDto
|
||||||
|
|
||||||
|
The `ProjectDto` type represents a project for a specific customer. It is used to transfer project data between the backend and frontend.
|
||||||
|
|
||||||
|
## TimeEntryDto
|
||||||
|
|
||||||
|
The `TimeEntryDto` type represents a time booking for a specific user, project, and activity. It is used to transfer time entry data between the backend and frontend.
|
||||||
|
|
||||||
|
## UserDto
|
||||||
|
|
||||||
|
The `UserDto` type represents a user of the application. Each user belongs to a company and has a specific role. It is used to transfer user data between the backend and frontend.
|
||||||
@@ -43,7 +43,7 @@ Here's a guide to finding information within the project:
|
|||||||
- **Code Examples:**
|
- **Code Examples:**
|
||||||
- `docu/code_examples/react_component.tsx`: Example React component.
|
- `docu/code_examples/react_component.tsx`: Example React component.
|
||||||
|
|
||||||
**Important Note about Code Examples:** The files in `docu/code_examples/` are for illustrative purposes *only*. They do *not* represent a runnable project structure. Treat each file as an isolated example. The package declarations within these files (e.g., `package entities`, `package repositories`, `package main`) are conceptual and should be interpreted in the context of the described architecture, *not* as a literal directory structure. Do not attempt to run `go get` or similar commands based on these examples, as the necessary project structure and dependencies are not present.
|
**Important Note about Code Examples:** The files in `docu/code_examples/` are for illustrative purposes *only*. They do *not* represent a runnable project structure. Treat each file as an isolated example. The package declarations within these files (e.g., `package models`, `package repositories`, `package main`) are conceptual and should be interpreted in the context of the described architecture, *not* as a literal directory structure. Do not attempt to run `go get` or similar commands based on these examples, as the necessary project structure and dependencies are not present.
|
||||||
|
|
||||||
## Rules and Guidelines
|
## Rules and Guidelines
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { FlatCompat } from "@eslint/eslintrc";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eslintConfig = [
|
||||||
|
...compat.extends("next/core-web-vitals", "next/typescript"),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+5468
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"next": "15.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^19",
|
||||||
|
"@types/react-dom": "^19",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "15.2.1",
|
||||||
|
"@eslint/eslintrc": "^3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: ["@tailwindcss/postcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||||
|
After Width: | Height: | Size: 385 B |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,26 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
const geistSans = Geist({
|
||||||
|
variable: "--font-geist-sans",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const geistMono = Geist_Mono({
|
||||||
|
variable: "--font-geist-mono",
|
||||||
|
subsets: ["latin"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Create Next App",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||||
|
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/next.svg"
|
||||||
|
alt="Next.js logo"
|
||||||
|
width={180}
|
||||||
|
height={38}
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||||
|
<li className="mb-2">
|
||||||
|
Get started by editing{" "}
|
||||||
|
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
|
||||||
|
src/app/page.tsx
|
||||||
|
</code>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
<li>Save and see your changes instantly.</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||||
|
<a
|
||||||
|
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
|
||||||
|
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="dark:invert"
|
||||||
|
src="/vercel.svg"
|
||||||
|
alt="Vercel logomark"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
Deploy now
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
|
||||||
|
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Read our docs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
|
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
aria-hidden
|
||||||
|
src="/file.svg"
|
||||||
|
alt="File icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Learn
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
|
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
aria-hidden
|
||||||
|
src="/window.svg"
|
||||||
|
alt="Window icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Examples
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||||
|
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
aria-hidden
|
||||||
|
src="/globe.svg"
|
||||||
|
alt="Globe icon"
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
Go to nextjs.org →
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BaseEntity } from "./base";
|
||||||
|
import { ActivityDto, ActivityCreateDto, ActivityUpdateDto } from "./dto";
|
||||||
|
import { UserId, ActivityId } from "./value-ids";
|
||||||
|
|
||||||
|
export type Activity = Omit<ActivityDto, "id" | "createdAt" | "updatedAt" | "lastEditorID"> & {
|
||||||
|
id: ActivityId;
|
||||||
|
} & BaseEntity;
|
||||||
|
|
||||||
|
export const mapActivityDtoToActivity = (dto: ActivityDto): Activity => ({
|
||||||
|
...dto,
|
||||||
|
id: dto.id as ActivityId,
|
||||||
|
createdAt: new Date(dto.createdAt),
|
||||||
|
updatedAt: new Date(dto.updatedAt),
|
||||||
|
lastEditorID: dto.lastEditorID as UserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ActivityCreate = ActivityCreateDto;
|
||||||
|
|
||||||
|
export const mapActivityCreateDtoToActivityCreate = (dto: ActivityCreateDto): ActivityCreate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ActivityUpdate = ActivityUpdateDto;
|
||||||
|
|
||||||
|
export const mapActivityUpdateDtoToActivityUpdate = (dto: ActivityUpdateDto): ActivityUpdate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { UserId } from "./value-ids";
|
||||||
|
|
||||||
|
export type BaseEntity = {
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
lastEditorID: UserId;
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BaseEntity } from "./base";
|
||||||
|
import { CompanyDto, CompanyCreateDto, CompanyUpdateDto } from "./dto";
|
||||||
|
import { UserId, CompanyId } from "./value-ids";
|
||||||
|
|
||||||
|
export type Company = Omit<CompanyDto, "id" | "createdAt" | "updatedAt" | "lastEditorID"> & {
|
||||||
|
id: CompanyId;
|
||||||
|
} & BaseEntity;
|
||||||
|
|
||||||
|
export const mapCompanyDtoToCompany = (dto: CompanyDto): Company => ({
|
||||||
|
...dto,
|
||||||
|
id: dto.id as CompanyId,
|
||||||
|
createdAt: new Date(dto.createdAt),
|
||||||
|
updatedAt: new Date(dto.updatedAt),
|
||||||
|
lastEditorID: dto.lastEditorID as UserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CompanyCreate = CompanyCreateDto;
|
||||||
|
|
||||||
|
export const mapCompanyCreateDtoToCompanyCreate = (dto: CompanyCreateDto): CompanyCreate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CompanyUpdate = CompanyUpdateDto;
|
||||||
|
|
||||||
|
export const mapCompanyUpdateDtoToCompanyUpdate = (dto: CompanyUpdateDto): CompanyUpdate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BaseEntity } from "./base";
|
||||||
|
import { CustomerDto, CustomerCreateDto, CustomerUpdateDto } from "./dto";
|
||||||
|
import { UserId, CustomerId } from "./value-ids";
|
||||||
|
|
||||||
|
export type Customer = Omit<CustomerDto, "id" | "createdAt" | "updatedAt" | "lastEditorID"> & {
|
||||||
|
id: CustomerId;
|
||||||
|
} & BaseEntity;
|
||||||
|
|
||||||
|
export const mapCustomerDtoToCustomer = (dto: CustomerDto): Customer => ({
|
||||||
|
...dto,
|
||||||
|
id: dto.id as CustomerId,
|
||||||
|
createdAt: new Date(dto.createdAt),
|
||||||
|
updatedAt: new Date(dto.updatedAt),
|
||||||
|
lastEditorID: dto.lastEditorID as UserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CustomerCreate = CustomerCreateDto;
|
||||||
|
|
||||||
|
export const mapCustomerCreateDtoToCustomerCreate = (dto: CustomerCreateDto): CustomerCreate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CustomerUpdate = CustomerUpdateDto;
|
||||||
|
|
||||||
|
export const mapCustomerUpdateDtoToCustomerUpdate = (dto: CustomerUpdateDto): CustomerUpdate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
// Code generated by tygo. DO NOT EDIT.
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// source: activity_dto.go
|
||||||
|
|
||||||
|
export interface ActivityDto {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastEditorID: string;
|
||||||
|
name: string;
|
||||||
|
billingRate: number /* float64 */;
|
||||||
|
}
|
||||||
|
export interface ActivityCreateDto {
|
||||||
|
name: string;
|
||||||
|
billingRate: number /* float64 */;
|
||||||
|
}
|
||||||
|
export interface ActivityUpdateDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
lastEditorID?: string;
|
||||||
|
name?: string;
|
||||||
|
billingRate?: number /* float64 */;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// source: company_dto.go
|
||||||
|
|
||||||
|
export interface CompanyDto {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastEditorID: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface CompanyCreateDto {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface CompanyUpdateDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
lastEditorID?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// source: customer_dto.go
|
||||||
|
|
||||||
|
export interface CustomerDto {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastEditorID: string;
|
||||||
|
name: string;
|
||||||
|
companyId: number /* int */;
|
||||||
|
}
|
||||||
|
export interface CustomerCreateDto {
|
||||||
|
name: string;
|
||||||
|
companyId: number /* int */;
|
||||||
|
}
|
||||||
|
export interface CustomerUpdateDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
lastEditorID?: string;
|
||||||
|
name?: string;
|
||||||
|
companyId?: number /* int */;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// source: project_dto.go
|
||||||
|
|
||||||
|
export interface ProjectDto {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastEditorID: string;
|
||||||
|
name: string;
|
||||||
|
customerId: number /* int */;
|
||||||
|
}
|
||||||
|
export interface ProjectCreateDto {
|
||||||
|
name: string;
|
||||||
|
customerId: number /* int */;
|
||||||
|
}
|
||||||
|
export interface ProjectUpdateDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
lastEditorID?: string;
|
||||||
|
name?: string;
|
||||||
|
customerId?: number /* int */;
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// source: timeentry_dto.go
|
||||||
|
|
||||||
|
export interface TimeEntryDto {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastEditorID: string;
|
||||||
|
userId: number /* int */;
|
||||||
|
projectId: number /* int */;
|
||||||
|
activityId: number /* int */;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
description: string;
|
||||||
|
billable: number /* int */; // Percentage (0-100)
|
||||||
|
}
|
||||||
|
export interface TimeEntryCreateDto {
|
||||||
|
userId: number /* int */;
|
||||||
|
projectId: number /* int */;
|
||||||
|
activityId: number /* int */;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
description: string;
|
||||||
|
billable: number /* int */; // Percentage (0-100)
|
||||||
|
}
|
||||||
|
export interface TimeEntryUpdateDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
lastEditorID?: string;
|
||||||
|
userId?: number /* int */;
|
||||||
|
projectId?: number /* int */;
|
||||||
|
activityId?: number /* int */;
|
||||||
|
start?: string;
|
||||||
|
end?: string;
|
||||||
|
description?: string;
|
||||||
|
billable?: number /* int */; // Percentage (0-100)
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////
|
||||||
|
// source: user_dto.go
|
||||||
|
|
||||||
|
export interface UserDto {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastEditorID: string;
|
||||||
|
email: string;
|
||||||
|
password: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
|
||||||
|
role: string;
|
||||||
|
companyId: number /* int */;
|
||||||
|
hourlyRate: number /* float64 */;
|
||||||
|
}
|
||||||
|
export interface UserCreateDto {
|
||||||
|
email: string;
|
||||||
|
password: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
|
||||||
|
role: string;
|
||||||
|
companyId: number /* int */;
|
||||||
|
hourlyRate: number /* float64 */;
|
||||||
|
}
|
||||||
|
export interface UserUpdateDto {
|
||||||
|
id: string;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
lastEditorID?: string;
|
||||||
|
email?: string;
|
||||||
|
password?: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
|
||||||
|
role?: string;
|
||||||
|
companyId?: number /* int */;
|
||||||
|
hourlyRate?: number /* float64 */;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BaseEntity } from "./base";
|
||||||
|
import { ProjectDto, ProjectCreateDto, ProjectUpdateDto } from "./dto";
|
||||||
|
import { UserId, ProjectId } from "./value-ids";
|
||||||
|
|
||||||
|
export type Project = Omit<ProjectDto, "id" | "createdAt" | "updatedAt" | "lastEditorID"> & {
|
||||||
|
id: ProjectId;
|
||||||
|
} & BaseEntity;
|
||||||
|
|
||||||
|
export const mapProjectDtoToProject = (dto: ProjectDto): Project => ({
|
||||||
|
...dto,
|
||||||
|
id: dto.id as ProjectId,
|
||||||
|
createdAt: new Date(dto.createdAt),
|
||||||
|
updatedAt: new Date(dto.updatedAt),
|
||||||
|
lastEditorID: dto.lastEditorID as UserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProjectCreate = ProjectCreateDto;
|
||||||
|
|
||||||
|
export const mapProjectCreateDtoToProjectCreate = (dto: ProjectCreateDto): ProjectCreate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ProjectUpdate = ProjectUpdateDto;
|
||||||
|
|
||||||
|
export const mapProjectUpdateDtoToProjectUpdate = (dto: ProjectUpdateDto): ProjectUpdate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { BaseEntity } from "./base";
|
||||||
|
import { TimeEntryDto, TimeEntryCreateDto, TimeEntryUpdateDto } from "./dto";
|
||||||
|
import { UserId, ProjectId, ActivityId, TimeEntryId } from "./value-ids";
|
||||||
|
|
||||||
|
export type TimeEntry = Omit<TimeEntryDto, "id" | "start" | "end" | "userId" | "projectId" | "activityId" | "lastEditorID" | "createdAt" | "updatedAt"> & {
|
||||||
|
id: TimeEntryId;
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
userId: UserId;
|
||||||
|
projectId: ProjectId;
|
||||||
|
activityId: ActivityId;
|
||||||
|
} & BaseEntity;
|
||||||
|
|
||||||
|
export const mapTimeEntryDtoToTimeEntry = (dto: TimeEntryDto): TimeEntry => ({
|
||||||
|
...dto,
|
||||||
|
id: dto.id as TimeEntryId,
|
||||||
|
start: new Date(dto.start),
|
||||||
|
end: new Date(dto.end),
|
||||||
|
userId: dto.userId.toString() as UserId,
|
||||||
|
projectId: dto.projectId.toString() as ProjectId,
|
||||||
|
activityId: dto.activityId.toString() as ActivityId,
|
||||||
|
createdAt: new Date(dto.createdAt),
|
||||||
|
updatedAt: new Date(dto.updatedAt),
|
||||||
|
lastEditorID: dto.lastEditorID as UserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TimeEntryCreate = Omit<TimeEntryCreateDto, "start" | "end"> & {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapTimeEntryCreateDtoToTimeEntryCreate = (dto: TimeEntryCreateDto): TimeEntryCreate => ({
|
||||||
|
...dto,
|
||||||
|
start: new Date(dto.start),
|
||||||
|
end: new Date(dto.end),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TimeEntryUpdate = Omit<TimeEntryUpdateDto, "start" | "end"> & {
|
||||||
|
start?: Date;
|
||||||
|
end?: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapTimeEntryUpdateDtoToTimeEntryUpdate = (dto: TimeEntryUpdateDto): TimeEntryUpdate => ({
|
||||||
|
...dto,
|
||||||
|
start: dto.start ? new Date(dto.start) : undefined,
|
||||||
|
end: dto.end ? new Date(dto.end) : undefined,
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { BaseEntity } from "./base";
|
||||||
|
import { UserDto, UserCreateDto, UserUpdateDto } from "./dto";
|
||||||
|
import { UserId } from "./value-ids";
|
||||||
|
|
||||||
|
export type User = Omit<UserDto, "id" | "createdAt" | "updatedAt" | "lastEditorID"> & {
|
||||||
|
id: UserId;
|
||||||
|
} & BaseEntity;
|
||||||
|
|
||||||
|
export const mapUserDtoToUser = (dto: UserDto): User => ({
|
||||||
|
...dto,
|
||||||
|
id: dto.id as UserId,
|
||||||
|
createdAt: new Date(dto.createdAt),
|
||||||
|
updatedAt: new Date(dto.updatedAt),
|
||||||
|
lastEditorID: dto.lastEditorID as UserId,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserCreate = Omit<UserCreateDto, never>;
|
||||||
|
|
||||||
|
export const mapUserCreateDtoToUserCreate = (dto: UserCreateDto): UserCreate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UserUpdate = Omit<UserUpdateDto, never>;
|
||||||
|
|
||||||
|
export const mapUserUpdateDtoToUserUpdate = (dto: UserUpdateDto): UserUpdate => ({
|
||||||
|
...dto,
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type ValueId<T = string> = string & { __valueId: T };
|
||||||
|
|
||||||
|
export type CustomerId = ValueId<"CustomerId">;
|
||||||
|
export type ProjectId = ValueId<"ProjectId">;
|
||||||
|
export type TimeEntryId = ValueId<"TimeEntryId">;
|
||||||
|
export type CompanyId = ValueId<"CompanyId">;
|
||||||
|
export type UserId = ValueId<"UserId">;
|
||||||
|
export type RoleId = ValueId<"RoleId">;
|
||||||
|
export type PermissionId = ValueId<"PermissionId">;
|
||||||
|
export type ActivityId = ValueId<"ActivityId">;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user