Compare commits

..

No commits in common. "main" and "v0.1.2" have entirely different histories.
main ... v0.1.2

111 changed files with 1752 additions and 2258 deletions

View file

@ -1,4 +0,0 @@
[build]
bin = "tmp/cafe-plaetschwiesle"
cmd = "go build -o tmp/cafe-plaetschwiesle cmd/cafe-plaetschwiesle/cafe-plaetschwiesle.go"
exclude_dir = [".gitlab", "docker", "scripts", "web", "storage", "tmp", "docs"]

View file

@ -1,17 +1,9 @@
stages: stages:
- test
- build - build
- deploy - deploy
include: include:
- local: .gitlab/_common.gitlab-ci.yml - local: .gitlab/_common.gitlab-ci.yml
- local: .gitlab/_rules.gitlab-ci.yml - local: .gitlab/_rules.gitlab-ci.yml
- local: /.gitlab/test.gitlab-ci.yml
- local: /.gitlab/build.gitlab-ci.yml - local: /.gitlab/build.gitlab-ci.yml
- local: /.gitlab/deploy.gitlab-ci.yml - local: /.gitlab/deploy.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml
secret_detection:
rules: !reference [.rules:default, rules]
stage: test

View file

@ -12,13 +12,3 @@ image: docker:$DOCKER_VERSION-git
.login_registry: .login_registry:
before_script: before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
.go-cache:
variables:
GOPATH: $CI_PROJECT_DIR/.go
before_script:
- mkdir -p .go
- export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
cache:
paths:
- .go/pkg/mod/

View file

@ -15,7 +15,7 @@ build_release:
--build-arg GOLANG_VERSION=$GOLANG_VERSION --build-arg GOLANG_VERSION=$GOLANG_VERSION
--build-arg NODE_VERSION=$NODE_VERSION --build-arg NODE_VERSION=$NODE_VERSION
--build-arg ALPINE_VERSION=$ALPINE_VERSION --build-arg ALPINE_VERSION=$ALPINE_VERSION
--build-arg VERSION=$CI_COMMIT_TAG --build-arg APP_VERSION=$CI_COMMIT_TAG
--build-arg BUILD_TIME=$CI_JOB_STARTED_AT --build-arg BUILD_TIME=$CI_JOB_STARTED_AT
--tag $CURRENT_IMAGE --tag $CURRENT_IMAGE
--tag $LATEST_IMAGE --tag $LATEST_IMAGE

View file

@ -1,17 +0,0 @@
unit_tests:
rules: !reference [.rules:default, rules]
stage: test
image: golang:$GOLANG_VERSION-alpine
extends:
- .go-cache
script:
- ./scripts/swagger.sh install
- ./scripts/swagger.sh init
- go install gotest.tools/gotestsum@latest
- gotestsum --junitfile report.xml --format testname -- ./... -coverprofile=profile.cov
- go tool cover -func profile.cov
coverage: '/\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/'
artifacts:
when: always
reports:
junit: report.xml

View file

@ -1,8 +1,76 @@
# Café Plätschwiesle # Café Plätschwiesle
![Alt vue](https://img.shields.io/badge/Framework-Vue3-informational?logo=vuedotjs&color=4FC08D) ![Alt vue](https://img.shields.io/badge/Framework-Vue3-informational?style=for-the-badge&logo=vuedotjs&color=4FC08D)
![Alt typescript](https://img.shields.io/badge/Language-Typescript-informational?logo=typescript&color=3178C6) ![Alt typescript](https://img.shields.io/badge/Language-Typescript-informational?style=for-the-badge&logo=typescript&color=3178C6)
![Alt go](https://img.shields.io/badge/Language-Go-informational?logo=go&color=00ADD8) ![Alt go](https://img.shields.io/badge/Language-Go-informational?style=for-the-badge&logo=go&color=00ADD8)
[![pipeline status](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/badges/main/pipeline.svg)](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/-/commits/main) ## docker-compose example
[![coverage report](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/badges/main/coverage.svg)](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/-/commits/main)
```yaml
services:
cafe:
image: ghcr.io/flohoss/cafe-plaetschwiesle:latest
container_name: cafe
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- ALLOWED_HOSTS=http://localhost:5000,https://home.example.com
- SWAGGER=true
- LOG_LEVEL=info # trace,debug,info,warn,error,fatal,panic
volumes:
- ./storage:/app/storage
ports:
- '127.0.0.1:5000:5000'
```
## docker-compose example with MariaDB as database
```yaml
networks:
net:
external: false
services:
cafe-db:
image: lscr.io/linuxserver/mariadb:latest
container_name: cafe-db
restart: unless-stopped
environment:
- PUID=1000
- PGID=1000
- MYSQL_ROOT_PASSWORD=root
- TZ=Europe/Berlin
- MYSQL_DATABASE=db
- MYSQL_USER=user
- MYSQL_PASSWORD=password
volumes:
- ./db:/config
expose:
- 3306
networks:
- net
cafe:
image: ghcr.io/flohoss/cafe-plaetschwiesle:latest
container_name: cafe
restart: unless-stopped
depends_on:
- cafe-db
environment:
- PUID=1000
- PGID=1000
- ALLOWED_HOSTS=http://localhost:5000,https://home.example.com
- SWAGGER=true
- LOG_LEVEL=info # trace,debug,info,warn,error,fatal,panic
- MYSQL_URL=cafe-db:3306
- MYSQL_USER=user
- MYSQL_PASSWORD=password
- MYSQL_DATABASE=db
volumes:
- ./storage:/app/storage
ports:
- '127.0.0.1:5000:5000'
networks:
- net
```

52
api/middlwares.go Normal file
View file

@ -0,0 +1,52 @@
package api
import (
"cafe/config"
"net/http"
"strings"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func myLogger() gin.HandlerFunc {
return func(c *gin.Context) {
if logrus.GetLevel() != logrus.TraceLevel {
return
}
reqUri := c.Request.RequestURI
if strings.Contains(reqUri, "/storage") {
return
}
startTime := time.Now()
c.Next()
endTime := time.Now()
latencyTime := endTime.Sub(startTime)
logrus.WithFields(logrus.Fields{
"status": http.StatusText(c.Writer.Status()),
"latency": latencyTime,
"client": c.ClientIP(),
"method": c.Request.Method,
}).Trace(reqUri)
}
}
func authHeader() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Remote-Groups", c.Request.Header.Get("Remote-Groups"))
c.Writer.Header().Set("Remote-Name", c.Request.Header.Get("Remote-Name"))
}
}
func (a *Api) SetMiddlewares() {
a.Router.Use(myLogger())
a.Router.Use(gin.Recovery())
a.Router.Use(cors.Default())
_ = a.Router.SetTrustedProxies(nil)
a.Router.MaxMultipartMemory = 8 << 20 // 8 MiB
logrus.WithFields(logrus.Fields{
"allowedOrigins": config.Cafe.AllowedHosts,
}).Debug("Middlewares set")
}

62
api/router.go Normal file
View file

@ -0,0 +1,62 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func (a *Api) SetupRouter() {
api := a.Router.Group("/api")
{
tableGroup := api.Group("/tables")
{
tableGroup.GET("", a.getTables)
tableGroup.POST("", a.createTable)
tableGroup.DELETE("", a.deleteTable)
}
orderGroup := api.Group("/orders")
{
orderGroup.GET("", a.getOrders)
orderGroup.POST("", a.createOrder)
orderGroup.DELETE("", a.deleteOrder)
orderGroup.PUT("", a.updateOrder)
orderGroup.GET("/ws", a.serveWs)
orderItemGroup := orderGroup.Group("/items")
{
orderItemGroup.GET("", a.getOrderItems)
orderItemGroup.POST("", a.createOrderItem)
orderItemGroup.PUT("", a.updateOrderItem)
orderItemGroup.DELETE("/:id", a.deleteOrderItem)
}
}
billGroup := api.Group("/bills")
{
billGroup.GET("", a.getBills)
billGroup.POST("", a.createBill)
billGroup.DELETE("/:id", a.deleteBill)
billItemGroup := billGroup.Group("/items")
{
billItemGroup.GET("", a.getBillItems)
}
}
userGroup := api.Group("/users")
{
userGroup.GET("/:username", a.getUser)
userGroup.PUT("", a.updateUser)
}
health := api.Group("/health")
{
health.Use(authHeader())
health.GET("", func(c *gin.Context) {
c.Status(http.StatusOK)
})
}
}
a.Router.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
logrus.WithField("amount", len(a.Router.Routes())).Debug("Routes initialized")
}

112
api/routesBill.go Normal file
View file

@ -0,0 +1,112 @@
package api
import (
"cafe/service"
"cafe/types"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
// @Schemes
// @Summary get all bills
// @Description gets all bills as array
// @Tags bills
// @Produce json
// @Param year query int true "year"
// @Param month query int true "month (1-12)"
// @Param day query int true "day (1-31)"
// @Success 200 {array} service.Bill
// @Router /bills [get]
func (a *Api) getBills(c *gin.Context) {
year, presentYear := c.GetQuery("year")
month, presentMonth := c.GetQuery("month")
day, presentDay := c.GetQuery("day")
if !presentYear || !presentMonth || !presentDay {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
bills, err := service.GetAllBills(year, month, day)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, bills)
}
}
// @Schemes
// @Summary get all billItems
// @Description gets all billItems for bill
// @Tags bills
// @Produce json
// @Param bill query int true "Bill ID"
// @Success 200 {array} service.BillItem
// @Router /bills/items [get]
func (a *Api) getBillItems(c *gin.Context) {
bill, err := service.DoesBillExist(c.Query("bill"))
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
billItems, err := service.GetAllBillItems(bill.ID)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, billItems)
}
}
// @Schemes
// @Summary create new bill
// @Description creates a new bill and returns it
// @Tags bills
// @Produce json
// @Param table query int true "Table ID"
// @Param filter query string false "filter"
// @Success 201 {object} service.Bill
// @Failure 404 "Not Found"
// @Failure 500 {object} errorResponse
// @Router /bills [post]
func (a *Api) createBill(c *gin.Context) {
table, tableErr := strconv.ParseUint(c.Query("table"), 10, 64)
if tableErr != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
stringFiler, filterPresent := c.GetQuery("filter")
var filter []string
if filterPresent {
filter = strings.Split(stringFiler, ",")
}
bill, err := service.CreateBill(service.GetOrderOptions{TableId: table, Grouped: true, Filter: filter})
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
}
c.JSON(http.StatusCreated, bill)
}
// @Schemes
// @Summary delete a bill
// @Description deletes a bill
// @Tags bills
// @Produce json
// @Param id path int true "Bill ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 {object} errorResponse
// @Router /bills/{id} [delete]
func (a *Api) deleteBill(c *gin.Context) {
id := c.Param("id")
bill, err := service.DoesBillExist(id)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.DeleteBill(&bill)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
}
c.Status(http.StatusOK)
}

258
api/routesOrder.go Normal file
View file

@ -0,0 +1,258 @@
package api
import (
"cafe/hub"
"cafe/service"
"cafe/types"
ws "cafe/websocket"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
)
// @Schemes
// @Summary get all orders
// @Description gets all orders as array
// @Tags orders
// @Produce json
// @Param table query int false "Table ID"
// @Param grouping query bool false "grouping"
// @Param filter query string false "filter"
// @Success 200 {array} service.Order
// @Router /orders [get]
func (a *Api) getOrders(c *gin.Context) {
table, _ := strconv.ParseUint(c.Query("table"), 10, 64)
grouping, _ := strconv.ParseBool(c.Query("grouping"))
stringFiler, filterPresent := c.GetQuery("filter")
var filter []string
if filterPresent {
filter = strings.Split(stringFiler, ",")
}
options := service.GetOrderOptions{TableId: table, Grouped: grouping, Filter: filter}
var orders []service.Order
if options.TableId == 0 {
orders = service.GetAllActiveOrders()
} else {
orders = service.GetAllOrdersForTable(options)
}
c.JSON(http.StatusOK, orders)
}
// @Schemes
// @Summary create new order
// @Description creates a new order and returns it
// @Tags orders
// @Accept json
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 201 {object} service.Order
// @Failure 400 {object} errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders [post]
func (a *Api) createOrder(c *gin.Context) {
table, err1 := strconv.ParseUint(c.Query("table"), 10, 64)
item, err2 := strconv.ParseUint(c.Query("item"), 10, 64)
if err1 != nil || err2 != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
order := service.Order{TableID: table, OrderItemID: item, IsServed: false}
err := service.CreateOrder(&order)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusCreated, order)
}
}
// @Schemes
// @Summary delete an order
// @Description deletes an order from the database
// @Tags orders
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 200 "OK"
// @Failure 400 {object} errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders [delete]
func (a *Api) deleteOrder(c *gin.Context) {
item := c.Query("item")
table := c.Query("table")
if table == "" || item == "" {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
err := service.DeleteOrder(table, item)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.Status(http.StatusOK)
}
}
// @Schemes
// @Summary update an order
// @Description updates an order with provided information
// @Tags orders
// @Accept json
// @Produce json
// @Param order body service.Order true "updated Order"
// @Success 200 {object} service.Order
// @Failure 400 {object} errorResponse
// @Failure 404 "Not Found" errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders [put]
func (a *Api) updateOrder(c *gin.Context) {
var newOrder service.Order
err := c.ShouldBindJSON(&newOrder)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
oldOrder, err := service.DoesOrderExist(strconv.Itoa(int(newOrder.ID)))
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.UpdateOrder(&oldOrder, &newOrder)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, newOrder)
}
}
func (a *Api) serveWs(c *gin.Context) {
conn, err := ws.Upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
logrus.WithField("error", err).Warning("Cannot upgrade websocket")
return
}
messageChan := make(hub.NotifierChan)
a.Hub.NewClients <- messageChan
defer func() {
a.Hub.ClosingClients <- messageChan
conn.Close()
}()
go ws.ReadPump(conn)
for {
msg, ok := <-messageChan
if !ok {
err := conn.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
return
}
return
}
err := conn.WriteJSON(msg)
if err != nil {
return
}
}
}
// @Schemes
// @Summary get all orderItems
// @Description gets all orderItems as array
// @Tags orderItems
// @Produce json
// @Param type query int true "ItemType"
// @Success 200 {array} service.OrderItem
// @Router /orders/items [get]
func (a *Api) getOrderItems(c *gin.Context) {
orderType := c.Query("type")
if orderType == "" {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
} else {
c.JSON(http.StatusOK, service.GetOrderItemsForType(orderType))
}
}
// @Schemes
// @Summary create new orderItem
// @Description creates a new orderItem and returns it
// @Tags orderItems
// @Accept json
// @Produce json
// @Param order body service.OrderItem true "OrderItem ID"
// @Success 201 {object} service.OrderItem
// @Failure 400 {object} errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders/items [post]
func (a *Api) createOrderItem(c *gin.Context) {
var orderItem service.OrderItem
err := c.ShouldBindJSON(&orderItem)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
err = service.CreateOrderItem(&orderItem)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusCreated, orderItem)
}
}
// @Schemes
// @Summary update a orderItem
// @Description updates a orderItem with provided information
// @Tags orderItems
// @Accept json
// @Produce json
// @Param orderItem body service.OrderItem true "updated OrderItem"
// @Success 200 {object} service.OrderItem
// @Failure 400 {object} errorResponse
// @Failure 404 "Not Found" errorResponse
// @Failure 500 {object} errorResponse
// @Router /orders/items [put]
func (a *Api) updateOrderItem(c *gin.Context) {
var newOrderItem service.OrderItem
err := c.ShouldBindJSON(&newOrderItem)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
oldOrderItem, err := service.DoesOrderItemExist(strconv.Itoa(int(newOrderItem.ID)))
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.UpdateOrderItem(&oldOrderItem, &newOrderItem)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, newOrderItem)
}
}
// @Schemes
// @Summary delete an orderItem
// @Description deletes an orderItem from the database
// @Tags orderItems
// @Produce json
// @Param id path int true "OrderItem ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 {object} errorResponse
// @Router /orders/items/{id} [delete]
func (a *Api) deleteOrderItem(c *gin.Context) {
id := c.Param("id")
orderItem, err := service.DoesOrderItemExist(id)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = service.DeleteOrderItem(&orderItem)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.Status(http.StatusOK)
}
}

54
api/routesTable.go Normal file
View file

@ -0,0 +1,54 @@
package api
import (
"cafe/service"
"net/http"
"github.com/gin-gonic/gin"
)
// @Schemes
// @Summary get all active tables
// @Description gets all active tables as array
// @Tags tables
// @Produce json
// @Success 200 {array} service.Table
// @Router /tables [get]
func (a *Api) getTables(c *gin.Context) {
c.JSON(http.StatusOK, service.GetAllTables())
}
// @Schemes
// @Summary create new table
// @Description creates a new table and returns it
// @Tags tables
// @Accept json
// @Produce json
// @Success 201 {object} service.Table "Table has been created"
// @Failure 500 {object} errorResponse "Cannot create table"
// @Router /tables [post]
func (a *Api) createTable(c *gin.Context) {
table, err := service.CreateNewTable()
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusCreated, table)
}
}
// @Schemes
// @Summary delete the latest table
// @Description deletes the latest table from the database
// @Tags tables
// @Produce json
// @Success 200 "OK"
// @Failure 500 {object} errorResponse "Cannot delete table"
// @Router /tables [delete]
func (a *Api) deleteTable(c *gin.Context) {
err := service.DeleteLatestTable()
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.Status(http.StatusOK)
}
}

60
api/routesUser.go Normal file
View file

@ -0,0 +1,60 @@
package api
import (
"cafe/types"
"cafe/user"
"net/http"
"github.com/gin-gonic/gin"
)
// @Schemes
// @Summary get a user
// @Description gets a user
// @Tags users
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} user.User
// @Failure 500 {object} errorResponse
// @Router /users/{username} [get]
func (a *Api) getUser(c *gin.Context) {
username := c.Param("username")
u, err := user.GetUserOrCreate(username)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, u)
}
}
// @Schemes
// @Summary update a user
// @Description updates a user with provided information
// @Tags users
// @Accept json
// @Produce json
// @Param user body user.User true "updated User"
// @Success 200 {object} user.User
// @Failure 400 {object} errorResponse
// @Failure 404 "Not Found" errorResponse
// @Failure 500 {object} errorResponse
// @Router /users [put]
func (a *Api) updateUser(c *gin.Context) {
var newUser user.User
err := c.ShouldBindJSON(&newUser)
if err != nil {
c.JSON(http.StatusBadRequest, errorResponse{types.MissingInformation.String()})
return
}
oldUser, err := user.DoesUserExist(newUser.Username)
if err != nil {
c.JSON(http.StatusNotFound, errorResponse{err.Error()})
return
}
err = user.UpdateUser(&oldUser, &newUser)
if err != nil {
c.JSON(http.StatusInternalServerError, errorResponse{err.Error()})
} else {
c.JSON(http.StatusOK, newUser)
}
}

26
api/static.go Normal file
View file

@ -0,0 +1,26 @@
package api
import (
"cafe/config"
"os"
"path/filepath"
"strings"
"github.com/gin-contrib/static"
"github.com/sirupsen/logrus"
)
func (a *Api) HandleStaticFiles() {
a.Router.LoadHTMLFiles(config.TemplatesDir + "index.html")
a.serveFoldersInTemplates()
}
func (a *Api) serveFoldersInTemplates() {
_ = filepath.WalkDir(config.TemplatesDir, func(path string, info os.DirEntry, err error) error {
if info.IsDir() && info.Name() != strings.TrimSuffix(config.TemplatesDir, "/") {
a.Router.Use(static.Serve("/"+info.Name(), static.LocalFile(config.TemplatesDir+info.Name(), false)))
logrus.WithField("folder", info.Name()).Debug("Serve static folder")
}
return err
})
}

31
api/swagger.go Normal file
View file

@ -0,0 +1,31 @@
package api
import (
"cafe/config"
"cafe/docs"
"net/http"
"net/url"
"os"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
func (a *Api) SetupSwagger() {
if config.Cafe.Swagger {
docs.SwaggerInfo.Title = "Cafe"
docs.SwaggerInfo.Description = "This is the backend of a cafe"
docs.SwaggerInfo.Version = os.Getenv("VERSION")
docs.SwaggerInfo.BasePath = "/api"
parsed, _ := url.Parse(config.Cafe.AllowedHosts[0])
docs.SwaggerInfo.Host = parsed.Host
a.Router.GET("/swagger", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/swagger/index.html")
})
a.Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
logrus.WithField("url", config.Cafe.AllowedHosts[0]+"/swagger").Info("Swagger running")
}
}

16
api/types.go Normal file
View file

@ -0,0 +1,16 @@
package api
import (
"cafe/hub"
"github.com/gin-gonic/gin"
)
type Api struct {
Router *gin.Engine
Hub hub.Hub
}
type errorResponse struct {
Error string `json:"error" validate:"required"`
}

View file

@ -1,30 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/controller"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/logging"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/router"
"go.uber.org/zap"
)
func main() {
env, err := env.Parse()
if err != nil {
log.Fatal(err)
}
zap.ReplaceGlobals(logging.CreateLogger(env.LogLevel))
r := router.InitRouter()
c := controller.NewController(env)
router.SetupRoutes(r, c, env)
zap.L().Info("starting server", zap.String("url", fmt.Sprintf("http://localhost:%d", env.Port)), zap.String("version", env.Version))
if err := r.Start(fmt.Sprintf(":%d", env.Port)); err != http.ErrServerClosed {
zap.L().Fatal("cannot start server", zap.Error(err))
}
}

10
config.toml Normal file
View file

@ -0,0 +1,10 @@
ALLOWED_HOSTS = "https://cafe.test"
LOG_LEVEL = "info"
PORT = 5000
SWAGGER = true
[MYSQL]
DATABASE = ""
PASSWORD = ""
URL = ""
USER = ""

72
config/config.go Normal file
View file

@ -0,0 +1,72 @@
package config
import (
"cafe/database"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/unjx-de/go-folder"
)
const StorageDir = "storage/"
const TemplatesDir = "templates/"
type Config struct {
Port int
AllowedHosts []string `mapstructure:"ALLOWED_HOSTS"`
Swagger bool
Bookmarks bool
LogLevel string `mapstructure:"LOG_LEVEL"`
Database database.MySQL `mapstructure:"MYSQL"`
}
var Cafe = Config{}
func configLogger() {
logrus.SetFormatter(&logrus.TextFormatter{TimestampFormat: "2006/01/02 15:04:05", FullTimestamp: true})
}
func readConfig() {
viper.AddConfigPath(".")
viper.SetConfigName("config")
viper.SetConfigType("toml")
err := viper.ReadInConfig()
if err != nil {
logrus.WithField("error", err).Fatal("Failed opening config file")
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
err = viper.Unmarshal(&Cafe, viper.DecodeHook(mapstructure.StringToSliceHookFunc(",")))
if err != nil {
logrus.WithField("error", err).Fatal("Failed reading environment variables")
}
logrus.WithField("file", viper.ConfigFileUsed()).Info("Initializing configuration")
}
func setLogLevel() {
logLevel, err := logrus.ParseLevel(Cafe.LogLevel)
if err != nil {
logrus.SetLevel(logrus.InfoLevel)
} else {
logrus.SetLevel(logLevel)
}
logrus.WithField("logLevel", logLevel.String()).Debug("Log level set")
}
func createFolderStructure() {
folders := []string{StorageDir, TemplatesDir}
err := folder.CreateFolders(folders, 0755)
if err != nil {
logrus.WithField("error", err).Fatal("Failed creating folders")
}
logrus.WithField("folders", folders).Debug("Folders created")
}
func init() {
configLogger()
readConfig()
setLogLevel()
createFolderStructure()
}

80
database/database.go Normal file
View file

@ -0,0 +1,80 @@
package database
import (
"fmt"
"net"
"time"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type MySQL struct {
Url string
User string
Password string
Database string
ORM *gorm.DB
}
func (config *MySQL) MigrateHelper(i interface{}, name string) {
err := config.ORM.AutoMigrate(i)
if err != nil {
logrus.WithField("error", err).Fatalf("Failed to migrate %s", name)
}
}
func (config *MySQL) tryDbConnection() {
i := 1
total := 20
for i <= total {
ln, err := net.DialTimeout("tcp", config.Url, 1*time.Second)
if err != nil {
if i == total {
logrus.WithField("attempt", i).Fatal("Failed connecting to database")
}
logrus.WithField("attempt", i).Warning("Connecting to database")
time.Sleep(2 * time.Second)
i++
} else {
_ = ln.Close()
logrus.WithField("attempt", i).Info("Connected to database")
i = total + 1
}
}
}
func (config *MySQL) initializeMySql() {
var err error
config.tryDbConnection()
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
config.User,
config.Password,
config.Url,
config.Database,
)
config.ORM, err = gorm.Open(mysql.Open(dsn), &gorm.Config{PrepareStmt: true})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to open database")
}
}
func (config *MySQL) initializeSqLite(storageDir string) {
var err error
absPath := storageDir + "db.sqlite"
config.ORM, err = gorm.Open(sqlite.Open(absPath), &gorm.Config{PrepareStmt: true})
if err != nil {
logrus.WithField("error", err).Fatal("Failed to open database")
}
}
func (config *MySQL) Initialize(storageDir string) {
if config.Url == "" {
config.initializeSqLite(storageDir)
} else {
config.initializeMySql()
}
logrus.WithField("dialect", config.ORM.Dialector.Name()).Debug("Database initialized")
}

View file

@ -102,7 +102,7 @@ services:
networks: networks:
- net - net
volumes: volumes:
- ./web:/app/ - ./frontend:/app/
cafe-backend: cafe-backend:
build: build:
@ -111,11 +111,10 @@ services:
args: args:
- GOLANG_VERSION=${GOLANG_VERSION} - GOLANG_VERSION=${GOLANG_VERSION}
container_name: cafe-backend container_name: cafe-backend
command: air -c .air.toml entrypoint: air --build.exclude_dir "node_modules,frontend,static,docs,storage,tmp,dist"
environment: environment:
- PUID=1000 - PUID=1000
- PGID=1000 - PGID=1000
- SWAGGER_HOST=https://cafe.test
labels: labels:
- 'traefik.enable=true' - 'traefik.enable=true'
- 'traefik.http.routers.backend.rule=Host(`cafe.test`) && PathPrefix(`/api`)' - 'traefik.http.routers.backend.rule=Host(`cafe.test`) && PathPrefix(`/api`)'
@ -123,7 +122,7 @@ services:
- 'traefik.http.routers.backend.tls=true' - 'traefik.http.routers.backend.tls=true'
- 'traefik.http.routers.backend.middlewares=authelia@docker' - 'traefik.http.routers.backend.middlewares=authelia@docker'
expose: expose:
- 8080 - 5000
networks: networks:
- net - net
volumes: volumes:

View file

@ -14,17 +14,17 @@ RUN go mod download
COPY . . COPY . .
RUN ./swagger.sh init RUN ./swagger.sh init
RUN go build -ldflags="-s -w" cmd/cafe-plaetschwiesle/cafe-plaetschwiesle.go RUN go build -ldflags="-s -w"
FROM node:${NODE_VERSION}-alpine AS nodeBuilder FROM node:${NODE_VERSION}-alpine AS nodeBuilder
WORKDIR /app WORKDIR /app
COPY ./web/package.json . COPY ./frontend/package.json .
COPY ./web/yarn.lock . COPY ./frontend/yarn.lock .
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
COPY --from=goBuilder /app/docs/swagger.json ../docs/swagger.json COPY --from=goBuilder /app/docs/swagger.json ../docs/swagger.json
COPY ./web/ . COPY ./frontend/ .
RUN yarn run types:openapi RUN yarn run types:openapi
RUN yarn run build RUN yarn run build
@ -39,8 +39,9 @@ WORKDIR /app
COPY ./scripts/entrypoint.sh . COPY ./scripts/entrypoint.sh .
COPY --from=logo /logo.txt . COPY --from=logo /logo.txt .
COPY --from=nodeBuilder /app/dist/ ./web/ COPY --from=nodeBuilder /app/dist/ ./templates/
COPY --from=goBuilder /app/cafe-plaetschwiesle . COPY --from=goBuilder /app/cafe .
COPY config.toml .
ARG VERSION ARG VERSION
ENV VERSION=$VERSION ENV VERSION=$VERSION

View file

@ -8,7 +8,8 @@ COPY ./go.sum .
RUN go mod download RUN go mod download
RUN go install github.com/cosmtrek/air@latest RUN go install github.com/cosmtrek/air@latest
COPY ./.air.toml .
ENV VERSION=v0.0.0-DEV ENV VERSION=v0.0.0-DEV
ENV BUILD_TIME=2023-06-01T08:07:43.454Z ENV BUILD_TIME=2023-06-01T08:07:43.454Z
CMD ["air"]

View file

@ -1,35 +1,29 @@
# web # frontend
## Project setup ## Project setup
``` ```
npm install npm install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
``` ```
npm run serve npm run serve
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
``` ```
npm run build npm run build
``` ```
### Run your unit tests ### Run your unit tests
``` ```
npm run test:unit npm run test:unit
``` ```
### Lints and fixes files ### Lints and fixes files
``` ```
npm run lint npm run lint
``` ```
### Customize configuration ### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/). See [Configuration Reference](https://cli.vuejs.org/config/).

View file

@ -1,5 +1,5 @@
{ {
"name": "web", "name": "frontend",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {

View file

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

View file

@ -31,7 +31,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, onMounted, PropType, ref } from "vue"; import { computed, defineComponent, onMounted, PropType, ref } from "vue";
import { BillsService, controller_Bill, controller_BillItem } from "@/services/openapi"; import { BillsService, service_Bill, service_BillItem } from "@/services/openapi";
import { convertToEur } from "@/utils"; import { convertToEur } from "@/utils";
import WaveSpinner from "@/components/UI/WaveSpinner.vue"; import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import moment from "moment"; import moment from "moment";
@ -39,10 +39,10 @@ import moment from "moment";
export default defineComponent({ export default defineComponent({
name: "BillModal", name: "BillModal",
components: { WaveSpinner }, components: { WaveSpinner },
props: { bill: { type: Object as PropType<controller_Bill>, required: true } }, props: { bill: { type: Object as PropType<service_Bill>, required: true } },
setup(props) { setup(props) {
const isLoading = ref(true); const isLoading = ref(true);
const billItems = ref<controller_BillItem[]>(); const billItems = ref<service_BillItem[]>();
const date = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("DD.MM.YYYY")); const date = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("DD.MM.YYYY"));
const time = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("HH:mm") + " Uhr"); const time = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("HH:mm") + " Uhr");
onMounted(() => { onMounted(() => {

View file

@ -77,7 +77,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, ref, watch } from "vue"; import { defineComponent, PropType, ref, watch } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue"; import BaseCard from "@/components/UI/BaseCard.vue";
import { OrderItemsService, controller_OrderItem } from "@/services/openapi"; import { OrderItemsService, service_OrderItem } from "@/services/openapi";
import InputText from "primevue/inputtext"; import InputText from "primevue/inputtext";
import { FilterMatchMode } from "primevue/api"; import { FilterMatchMode } from "primevue/api";
import DataTable from "primevue/datatable"; import DataTable from "primevue/datatable";
@ -95,8 +95,8 @@ export default defineComponent({
// eslint-disable-next-line // eslint-disable-next-line
components: { BaseCard, InputText, DataTable, Column, Button, Dialog, InputNumber, ConfirmDialog }, components: { BaseCard, InputText, DataTable, Column, Button, Dialog, InputNumber, ConfirmDialog },
props: { props: {
orderItems: { type: Array as PropType<controller_OrderItem[]>, default: () => [] }, orderItems: { type: Array as PropType<service_OrderItem[]>, default: () => [] },
emptyOrderItem: { type: Object as PropType<controller_OrderItem>, default: () => ({}) }, emptyOrderItem: { type: Object as PropType<service_OrderItem>, default: () => ({}) },
title: { type: String, default: "" }, title: { type: String, default: "" },
}, },
emits: ["orderItemChanged", "orderItemDeleted", "orderItemCreated"], emits: ["orderItemChanged", "orderItemDeleted", "orderItemCreated"],
@ -108,8 +108,8 @@ export default defineComponent({
const filters = ref({ const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS }, global: { value: null, matchMode: FilterMatchMode.CONTAINS },
}); });
const orderItem = ref<controller_OrderItem>({ ...props.emptyOrderItem }); const orderItem = ref<service_OrderItem>({ ...props.emptyOrderItem });
function editOrderItem(item: controller_OrderItem) { function editOrderItem(item: service_OrderItem) {
orderItem.value = { ...item }; orderItem.value = { ...item };
modal.value = true; modal.value = true;
} }
@ -130,7 +130,7 @@ export default defineComponent({
} }
} }
function confirmDeleteProduct(item: controller_OrderItem) { function confirmDeleteProduct(item: service_OrderItem) {
if (isDisabled.value) return; if (isDisabled.value) return;
confirm.require({ confirm.require({
message: item.description + " löschen?", message: item.description + " löschen?",

View file

@ -14,7 +14,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref } from "vue"; import { computed, defineComponent, onMounted, onUnmounted, PropType, ref } from "vue";
import { controller_Order, controller_ItemType } from "@/services/openapi"; import { service_Order, types_ItemType } from "@/services/openapi";
import { convertToEur, getCurrentTimeSince, lessThan15SecondsAgo } from "@/utils"; import { convertToEur, getCurrentTimeSince, lessThan15SecondsAgo } from "@/utils";
import Button from "primevue/button"; import Button from "primevue/button";
import moment from "moment"; import moment from "moment";
@ -26,9 +26,9 @@ export default defineComponent({
// eslint-disable-next-line // eslint-disable-next-line
components: { SmallCard, TheBadge, Button }, components: { SmallCard, TheBadge, Button },
props: { props: {
order: { type: Object as PropType<controller_Order>, required: true }, order: { type: Object as PropType<service_Order>, required: true },
isDisabled: { type: Boolean, default: false }, isDisabled: { type: Boolean, default: false },
itemType: { type: Number as PropType<controller_ItemType>, required: false }, itemType: { type: Number as PropType<types_ItemType>, required: false },
}, },
emits: ["orderDone"], emits: ["orderDone"],
setup(props) { setup(props) {
@ -37,7 +37,7 @@ export default defineComponent({
let ticker: any; let ticker: any;
const since = ref(getCurrentTimeSince(props.order.updated_at)); const since = ref(getCurrentTimeSince(props.order.updated_at));
const newOrder = ref(lessThan15SecondsAgo(props.order.updated_at)); const newOrder = ref(lessThan15SecondsAgo(props.order.updated_at));
const badgeTwo = computed(() => props.itemType === controller_ItemType.ColdDrink); const badgeTwo = computed(() => props.itemType === types_ItemType.ColdDrink);
onMounted(() => { onMounted(() => {
ticker = setInterval(() => { ticker = setInterval(() => {

View file

@ -17,7 +17,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, PropType, ref } from "vue"; import { computed, defineComponent, inject, PropType, ref } from "vue";
import { OrdersService, controller_Order, controller_ItemType } from "@/services/openapi"; import { OrdersService, service_Order, types_ItemType } from "@/services/openapi";
import { detailedItemTypeIcon, detailedItemTypeString, errorToast, lessThan15SecondsAgo } from "@/utils"; import { detailedItemTypeIcon, detailedItemTypeString, errorToast, lessThan15SecondsAgo } from "@/utils";
import OrderCard from "@/components/Orders/OrderCard.vue"; import OrderCard from "@/components/Orders/OrderCard.vue";
import BaseToolbar from "@/components/UI/BaseToolbar.vue"; import BaseToolbar from "@/components/UI/BaseToolbar.vue";
@ -28,10 +28,10 @@ export default defineComponent({
name: "OrderSection", name: "OrderSection",
components: { OrderCard, BaseToolbar }, components: { OrderCard, BaseToolbar },
props: { props: {
orders: { type: Object as PropType<controller_Order[]>, required: true }, orders: { type: Object as PropType<service_Order[]>, required: true },
icon: { type: String, required: false }, icon: { type: String, required: false },
title: { type: String, required: false }, title: { type: String, required: false },
itemType: { type: Number as PropType<controller_ItemType>, required: false }, itemType: { type: Number as PropType<types_ItemType>, required: false },
}, },
emits: ["filterOrders"], emits: ["filterOrders"],
setup(props, { emit }) { setup(props, { emit }) {
@ -46,7 +46,7 @@ export default defineComponent({
}); });
} }
function orderDone(order: controller_Order) { function orderDone(order: service_Order) {
isDisabled.value = true; isDisabled.value = true;
order.is_served = true; order.is_served = true;
OrdersService.putOrders(order) OrdersService.putOrders(order)

View file

@ -12,12 +12,12 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { controller_Order } from "@/services/openapi"; import { service_Order } from "@/services/openapi";
export default defineComponent({ export default defineComponent({
name: "OrderAmountChange", name: "OrderAmountChange",
props: { props: {
order: { type: Object as PropType<controller_Order>, required: true }, order: { type: Object as PropType<service_Order>, required: true },
isDisabled: { type: Boolean, default: false }, isDisabled: { type: Boolean, default: false },
}, },
emits: ["incrementOrder", "decrementOrder"], emits: ["incrementOrder", "decrementOrder"],

View file

@ -13,7 +13,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, PropType, ref } from "vue"; import { computed, defineComponent, inject, PropType, ref } from "vue";
import { OrdersService, controller_Order } from "@/services/openapi"; import { OrdersService, service_Order } from "@/services/openapi";
import { convertToEur, generalItemTypeString, generalItemTypeIcon } from "@/utils"; import { convertToEur, generalItemTypeString, generalItemTypeIcon } from "@/utils";
import BaseToolbar from "@/components/UI/BaseToolbar.vue"; import BaseToolbar from "@/components/UI/BaseToolbar.vue";
import TableOrderCard from "@/components/Tables/TableOrderCard.vue"; import TableOrderCard from "@/components/Tables/TableOrderCard.vue";
@ -24,7 +24,7 @@ export default defineComponent({
name: "OverviewPerType", name: "OverviewPerType",
components: { TableOrderCard, BaseToolbar, OrderAmountChange }, components: { TableOrderCard, BaseToolbar, OrderAmountChange },
props: { props: {
orders: { type: Array as PropType<controller_Order[]>, default: () => [] }, orders: { type: Array as PropType<service_Order[]>, default: () => [] },
type: { type: Array as PropType<number[]>, required: true }, type: { type: Array as PropType<number[]>, required: true },
}, },
emits: ["openModal", "getData"], emits: ["openModal", "getData"],
@ -32,12 +32,12 @@ export default defineComponent({
const OrdersForType = computed(() => props.orders.filter((order) => props.type.includes(order.order_item.item_type))); const OrdersForType = computed(() => props.orders.filter((order) => props.type.includes(order.order_item.item_type)));
const isLoading = inject(loading, ref(false)); const isLoading = inject(loading, ref(false));
function incrementOrder(order: controller_Order) { function incrementOrder(order: service_Order) {
isLoading.value = true; isLoading.value = true;
OrdersService.postOrders(order.order_item_id, order.table_id).finally(() => emit("getData")); OrdersService.postOrders(order.order_item_id, order.table_id).finally(() => emit("getData"));
} }
function decrementOrder(order: controller_Order) { function decrementOrder(order: service_Order) {
isLoading.value = true; isLoading.value = true;
OrdersService.deleteOrders(order.order_item_id, order.table_id).finally(() => emit("getData")); OrdersService.deleteOrders(order.order_item_id, order.table_id).finally(() => emit("getData"));
} }

View file

@ -13,7 +13,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, onUnmounted, PropType, ref } from "vue"; import { defineComponent, onMounted, onUnmounted, PropType, ref } from "vue";
import { controller_Table } from "@/services/openapi"; import { service_Table } from "@/services/openapi";
import moment from "moment"; import moment from "moment";
import { convertToEur, getCurrentTimeSince } from "@/utils"; import { convertToEur, getCurrentTimeSince } from "@/utils";
import TheBadge from "@/components/UI/TheBadge.vue"; import TheBadge from "@/components/UI/TheBadge.vue";
@ -22,7 +22,7 @@ import SmallCard from "@/components/UI/SmallCard.vue";
export default defineComponent({ export default defineComponent({
name: "TableCard", name: "TableCard",
components: { TheBadge, SmallCard }, components: { TheBadge, SmallCard },
props: { table: { type: Object as PropType<controller_Table>, required: true } }, props: { table: { type: Object as PropType<service_Table>, required: true } },
setup(props) { setup(props) {
moment.locale("de"); moment.locale("de");
// eslint-disable-next-line // eslint-disable-next-line

View file

@ -11,7 +11,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, PropType } from "vue"; import { computed, defineComponent, PropType } from "vue";
import { controller_Order, controller_ItemType } from "@/services/openapi"; import { service_Order, types_ItemType } from "@/services/openapi";
import { convertToEur } from "@/utils"; import { convertToEur } from "@/utils";
import SmallCard from "@/components/UI/SmallCard.vue"; import SmallCard from "@/components/UI/SmallCard.vue";
@ -19,12 +19,12 @@ export default defineComponent({
name: "TableOrderCard", name: "TableOrderCard",
components: { SmallCard }, components: { SmallCard },
props: { props: {
order: { type: Object as PropType<controller_Order>, required: true }, order: { type: Object as PropType<service_Order>, required: true },
}, },
emits: ["decrementOrder", "incrementOrder"], emits: ["decrementOrder", "incrementOrder"],
setup(props) { setup(props) {
const showTotal = computed(() => props.order.order_item.price !== props.order.total); const showTotal = computed(() => props.order.order_item.price !== props.order.total);
return { convertToEur, controller_ItemType, showTotal }; return { convertToEur, types_ItemType, showTotal };
}, },
}); });
</script> </script>

View file

@ -3,13 +3,8 @@
<Transition> <Transition>
<WaveSpinner v-if="initialLoading" /> <WaveSpinner v-if="initialLoading" />
<div v-else> <div v-else>
<OverviewPerType :type="[controller_ItemType.Food]" :orders="orders" @getData="getData" @openModal="(t) => addBeverage(t)" /> <OverviewPerType :type="[types_ItemType.Food]" :orders="orders" @getData="getData" @openModal="(t) => addBeverage(t)" />
<OverviewPerType <OverviewPerType :type="[types_ItemType.ColdDrink, types_ItemType.HotDrink]" :orders="orders" @getData="getData" @openModal="(t) => addBeverage(t)" />
:type="[controller_ItemType.ColdDrink, controller_ItemType.HotDrink]"
:orders="orders"
@getData="getData"
@openModal="(t) => addBeverage(t)"
/>
<div class="h-4rem"></div> <div class="h-4rem"></div>
<BottomNavigation> <BottomNavigation>
@ -61,7 +56,7 @@
import { computed, defineComponent, provide, ref } from "vue"; import { computed, defineComponent, provide, ref } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue"; import BaseCard from "@/components/UI/BaseCard.vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { OrdersService, controller_Order, controller_OrderItem, controller_ItemType } from "@/services/openapi"; import { OrdersService, service_Order, service_OrderItem, types_ItemType } from "@/services/openapi";
import BottomNavigation from "@/components/UI/BottomNavigation.vue"; import BottomNavigation from "@/components/UI/BottomNavigation.vue";
import Button from "primevue/button"; import Button from "primevue/button";
import { convertToEur } from "@/utils"; import { convertToEur } from "@/utils";
@ -87,7 +82,7 @@ export default defineComponent({
const total = ref(0); const total = ref(0);
const orderItems = computed(() => store.getters.getOrderItems); const orderItems = computed(() => store.getters.getOrderItems);
const options = ref(); const options = ref();
const orders = ref<controller_Order[]>([]); const orders = ref<service_Order[]>([]);
store.dispatch("getAllOrderItems"); store.dispatch("getAllOrderItems");
@ -115,13 +110,13 @@ export default defineComponent({
total.value = temp; total.value = temp;
} }
function addBeverage(itemType: controller_ItemType[]) { function addBeverage(itemType: types_ItemType[]) {
newOrderModal.value = true; newOrderModal.value = true;
options.value = []; options.value = [];
itemType.forEach((type) => { itemType.forEach((type) => {
options.value = options.value.concat(orderItems.value.get(type)); options.value = options.value.concat(orderItems.value.get(type));
}); });
options.value.sort((a: controller_OrderItem, b: controller_OrderItem) => { options.value.sort((a: service_OrderItem, b: service_OrderItem) => {
const x = a.description.toLowerCase(); const x = a.description.toLowerCase();
const y = b.description.toLowerCase(); const y = b.description.toLowerCase();
if (x < y) return -1; if (x < y) return -1;
@ -147,7 +142,7 @@ export default defineComponent({
total, total,
convertToEur, convertToEur,
addBeverage, addBeverage,
controller_ItemType, types_ItemType,
postOrder, postOrder,
orders, orders,
getData, getData,

View file

@ -24,7 +24,7 @@ import Menubar from "primevue/menubar";
import { useStore } from "vuex"; import { useStore } from "vuex";
import Button from "primevue/button"; import Button from "primevue/button";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { TablesService, controller_ItemType } from "@/services/openapi"; import { TablesService, types_ItemType } from "@/services/openapi";
import { detailedItemTypeString, errorToast } from "@/utils"; import { detailedItemTypeString, errorToast } from "@/utils";
import { useToast } from "primevue/usetoast"; import { useToast } from "primevue/usetoast";
import { visible } from "@/keys"; import { visible } from "@/keys";
@ -84,9 +84,9 @@ export default defineComponent({
label: "Artikel", label: "Artikel",
icon: "pi pi-fw pi-shopping-cart", icon: "pi pi-fw pi-shopping-cart",
items: [ items: [
{ label: detailedItemTypeString(controller_ItemType.Food), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.Food }, { label: detailedItemTypeString(types_ItemType.Food), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.Food },
{ label: detailedItemTypeString(controller_ItemType.ColdDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.ColdDrink }, { label: detailedItemTypeString(types_ItemType.ColdDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.ColdDrink },
{ label: detailedItemTypeString(controller_ItemType.HotDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.HotDrink }, { label: detailedItemTypeString(types_ItemType.HotDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.HotDrink },
], ],
visible: () => editor.value, visible: () => editor.value,
}, },
@ -103,7 +103,6 @@ export default defineComponent({
visible: () => maker.value, visible: () => maker.value,
}, },
{ label: "Abmelden", icon: "pi pi-fw pi-power-off", command: () => emit("logout") }, { label: "Abmelden", icon: "pi pi-fw pi-power-off", command: () => emit("logout") },
{ label: store.getters.getVersion, icon: "pi pi-fw pi-github", disabled: true },
], ],
}, },
]); ]);

View file

@ -16,7 +16,7 @@
import { computed, defineComponent, inject, ref } from "vue"; import { computed, defineComponent, inject, ref } from "vue";
import Sidebar from "primevue/sidebar"; import Sidebar from "primevue/sidebar";
import { visible } from "@/keys"; import { visible } from "@/keys";
import { controller_User, UsersService } from "@/services/openapi"; import { user_User, UsersService } from "@/services/openapi";
import InputSwitch from "primevue/inputswitch"; import InputSwitch from "primevue/inputswitch";
import { useStore } from "vuex"; import { useStore } from "vuex";
import { errorToast } from "@/utils"; import { errorToast } from "@/utils";
@ -30,7 +30,7 @@ export default defineComponent({
const toast = useToast(); const toast = useToast();
const isLoading = ref(false); const isLoading = ref(false);
const isVisible = inject(visible, ref(false)); const isVisible = inject(visible, ref(false));
const user = computed<controller_User>(() => store.getters.getUser); const user = computed<user_User>(() => store.getters.getUser);
function updateUser() { function updateUser() {
isLoading.value = true; isLoading.value = true;

View file

@ -1,4 +1,4 @@
import { createApp, version } from "vue"; import { createApp } from "vue";
import { OpenAPI, UsersService } from "@/services/openapi"; import { OpenAPI, UsersService } from "@/services/openapi";
import App from "./App.vue"; import App from "./App.vue";
import router from "./router"; import router from "./router";
@ -18,6 +18,7 @@ import "primeicons/primeicons.css";
import "primeflex/primeflex.css"; import "primeflex/primeflex.css";
export const API_ENDPOINT_URL = window.origin + "/api"; export const API_ENDPOINT_URL = window.origin + "/api";
export const WEBSOCKET_ENDPOINT_URL = API_ENDPOINT_URL.replace("http", "ws") + "/orders/ws";
OpenAPI.BASE = API_ENDPOINT_URL; OpenAPI.BASE = API_ENDPOINT_URL;
async function getHealth() { async function getHealth() {
@ -26,7 +27,6 @@ async function getHealth() {
store.commit("setGroups", groups); store.commit("setGroups", groups);
const user = await UsersService.getUsers(response.headers.get("remote-name") || "Benutzer"); const user = await UsersService.getUsers(response.headers.get("remote-name") || "Benutzer");
store.commit("setUser", user); store.commit("setUser", user);
store.commit("setVersion", await response.text());
} }
getHealth().then(() => { getHealth().then(() => {

View file

@ -1,12 +1,11 @@
import { createStore } from "vuex"; import { createStore } from "vuex";
import tableStore from "@/store/tables"; import tableStore from "@/store/tables";
import orderItemStore from "@/store/orderItems"; import orderItemStore from "@/store/orderItems";
import { controller_User } from "@/services/openapi"; import { user_User } from "@/services/openapi";
interface AppStateModel { interface AppStateModel {
user: controller_User; user: user_User;
groups: string[]; groups: string[];
version: string;
} }
export default createStore({ export default createStore({
state: { state: {
@ -16,7 +15,6 @@ export default createStore({
show_hot_drinks: true, show_hot_drinks: true,
}, },
groups: [""], groups: [""],
version: "",
}, },
getters: { getters: {
getUser(state: AppStateModel) { getUser(state: AppStateModel) {
@ -28,20 +26,14 @@ export default createStore({
getUsername(state: AppStateModel) { getUsername(state: AppStateModel) {
return state.user.username; return state.user.username;
}, },
getVersion(state: AppStateModel) {
return state.version;
},
}, },
mutations: { mutations: {
setUser(state: AppStateModel, _user: controller_User) { setUser(state: AppStateModel, _user: user_User) {
state.user = _user; state.user = _user;
}, },
setGroups(state: AppStateModel, groups: string[]) { setGroups(state: AppStateModel, groups: string[]) {
state.groups = groups; state.groups = groups;
}, },
setVersion(state: AppStateModel, version: string) {
state.version = version;
},
}, },
actions: {}, actions: {},
modules: { modules: {

View file

@ -1,17 +1,17 @@
import { OrderItemsService, controller_OrderItem, controller_ItemType } from "@/services/openapi"; import { OrderItemsService, service_OrderItem, types_ItemType } from "@/services/openapi";
interface AppStateModel { interface AppStateModel {
orderItems: Map<number, controller_OrderItem[]>; orderItems: Map<number, service_OrderItem[]>;
} }
interface mutationOrderItems { interface mutationOrderItems {
orderItems: controller_OrderItem[]; orderItems: service_OrderItem[];
orderType: controller_ItemType; orderType: types_ItemType;
} }
const orderItemStore = { const orderItemStore = {
state: () => ({ state: () => ({
orderItems: new Map<number, controller_OrderItem[]>(), orderItems: new Map<number, service_OrderItem[]>(),
}), }),
getters: { getters: {
getOrderItems(state: AppStateModel) { getOrderItems(state: AppStateModel) {
@ -22,52 +22,52 @@ const orderItemStore = {
setOrderItems(state: AppStateModel, payload: mutationOrderItems) { setOrderItems(state: AppStateModel, payload: mutationOrderItems) {
state.orderItems.set(payload.orderType, payload.orderItems); state.orderItems.set(payload.orderType, payload.orderItems);
}, },
pushOrderItem(state: AppStateModel, orderItem: controller_OrderItem) { pushOrderItem(state: AppStateModel, orderItem: service_OrderItem) {
const tempOrderItems = state.orderItems.get(orderItem.item_type); const tempOrderItems = state.orderItems.get(orderItem.item_type);
tempOrderItems && tempOrderItems.push(orderItem); tempOrderItems && tempOrderItems.push(orderItem);
}, },
filterOrderItem(state: AppStateModel, orderItem: controller_OrderItem) { filterOrderItem(state: AppStateModel, orderItem: service_OrderItem) {
const tempOrderItems = state.orderItems.get(orderItem.item_type); const tempOrderItems = state.orderItems.get(orderItem.item_type);
tempOrderItems && tempOrderItems &&
state.orderItems.set( state.orderItems.set(
orderItem.item_type, orderItem.item_type,
tempOrderItems.filter((origItem: controller_OrderItem) => origItem.id !== orderItem.id) tempOrderItems.filter((origItem: service_OrderItem) => origItem.id !== orderItem.id)
); );
}, },
putOrderItem(state: AppStateModel, orderItem: controller_OrderItem) { putOrderItem(state: AppStateModel, orderItem: service_OrderItem) {
const tempOrderItems = state.orderItems.get(orderItem.item_type); const tempOrderItems = state.orderItems.get(orderItem.item_type);
tempOrderItems && tempOrderItems &&
state.orderItems.set( state.orderItems.set(
orderItem.item_type, orderItem.item_type,
tempOrderItems.map((origItem: controller_OrderItem) => (origItem.id === orderItem.id ? orderItem : origItem)) tempOrderItems.map((origItem: service_OrderItem) => (origItem.id === orderItem.id ? orderItem : origItem))
); );
}, },
}, },
actions: { actions: {
// eslint-disable-next-line // eslint-disable-next-line
async getAllOrderItems(context: any) { async getAllOrderItems(context: any) {
await context.dispatch("getOrderItems", controller_ItemType.Food); await context.dispatch("getOrderItems", types_ItemType.Food);
await context.dispatch("getOrderItems", controller_ItemType.ColdDrink); await context.dispatch("getOrderItems", types_ItemType.ColdDrink);
await context.dispatch("getOrderItems", controller_ItemType.HotDrink); await context.dispatch("getOrderItems", types_ItemType.HotDrink);
}, },
// eslint-disable-next-line // eslint-disable-next-line
async getOrderItems(context: any, orderType: controller_ItemType) { async getOrderItems(context: any, orderType: types_ItemType) {
const orderTypeArray = context.getters.getOrderItems; const orderTypeArray = context.getters.getOrderItems;
if (!orderTypeArray.get(orderType)) { if (!orderTypeArray.get(orderType)) {
const orderItems: controller_OrderItem[] | null = await OrderItemsService.getOrdersItems(orderType); const orderItems: service_OrderItem[] | null = await OrderItemsService.getOrdersItems(orderType);
context.commit("setOrderItems", { orderItems, orderType }); context.commit("setOrderItems", { orderItems, orderType });
} }
}, },
// eslint-disable-next-line // eslint-disable-next-line
addOrderItem(context: any, orderItem: controller_OrderItem) { addOrderItem(context: any, orderItem: service_OrderItem) {
context.commit("pushOrderItem", orderItem); context.commit("pushOrderItem", orderItem);
}, },
// eslint-disable-next-line // eslint-disable-next-line
deleteOrderItem(context: any, orderItem: controller_OrderItem) { deleteOrderItem(context: any, orderItem: service_OrderItem) {
context.commit("filterOrderItem", orderItem); context.commit("filterOrderItem", orderItem);
}, },
// eslint-disable-next-line // eslint-disable-next-line
updateOrderItem(context: any, orderItem: controller_OrderItem) { updateOrderItem(context: any, orderItem: service_OrderItem) {
context.commit("putOrderItem", orderItem); context.commit("putOrderItem", orderItem);
}, },
}, },

View file

@ -1,7 +1,7 @@
import { controller_Table, TablesService } from "@/services/openapi"; import { service_Table, TablesService } from "@/services/openapi";
interface AppStateModel { interface AppStateModel {
tables: controller_Table[] | null; tables: service_Table[] | null;
} }
const tableStore = { const tableStore = {
@ -17,13 +17,13 @@ const tableStore = {
}, },
}, },
mutations: { mutations: {
setTables(state: AppStateModel, tables: controller_Table[]) { setTables(state: AppStateModel, tables: service_Table[]) {
state.tables = tables; state.tables = tables;
}, },
popTables(state: AppStateModel) { popTables(state: AppStateModel) {
state.tables && state.tables.pop(); state.tables && state.tables.pop();
}, },
pushTable(state: AppStateModel, table: controller_Table) { pushTable(state: AppStateModel, table: service_Table) {
state.tables && state.tables.push(table); state.tables && state.tables.push(table);
}, },
}, },
@ -38,7 +38,7 @@ const tableStore = {
context.commit("popTables"); context.commit("popTables");
}, },
// eslint-disable-next-line // eslint-disable-next-line
addTable(context: any, table: controller_Table) { addTable(context: any, table: service_Table) {
context.commit("pushTable", table); context.commit("pushTable", table);
}, },
}, },

View file

@ -1,44 +1,44 @@
import { controller_Bill, controller_Order, controller_ItemType } from "@/services/openapi"; import { service_Bill, service_Order, types_ItemType } from "@/services/openapi";
export function convertToEur(value: number | undefined) { export function convertToEur(value: number | undefined) {
const temp: number = value ? value : 0; const temp: number = value ? value : 0;
return temp.toLocaleString("de-DE", { style: "currency", currency: "EUR" }); return temp.toLocaleString("de-DE", { style: "currency", currency: "EUR" });
} }
export function detailedItemTypeString(type: controller_ItemType | undefined) { export function detailedItemTypeString(type: types_ItemType | undefined) {
switch (type) { switch (type) {
case controller_ItemType.Food: case types_ItemType.Food:
return "Speisen"; return "Speisen";
case controller_ItemType.ColdDrink: case types_ItemType.ColdDrink:
return "Kaltgetränke"; return "Kaltgetränke";
default: default:
return "Heiß/Eiskaffee"; return "Heiß/Eiskaffee";
} }
} }
export function generalItemTypeString(type: controller_ItemType[]) { export function generalItemTypeString(type: types_ItemType[]) {
if (type.includes(controller_ItemType.Food)) { if (type.includes(types_ItemType.Food)) {
return "Speisen"; return "Speisen";
} else { } else {
return "Getränke"; return "Getränke";
} }
} }
export function detailedItemTypeIcon(type: controller_ItemType | undefined) { export function detailedItemTypeIcon(type: types_ItemType | undefined) {
switch (type) { switch (type) {
case controller_ItemType.Food: case types_ItemType.Food:
return "fa-cheese"; return "fa-cheese";
case controller_ItemType.ColdDrink: case types_ItemType.ColdDrink:
return "fa-champagne-glasses"; return "fa-champagne-glasses";
case controller_ItemType.HotDrink: case types_ItemType.HotDrink:
return "fa-mug-hot"; return "fa-mug-hot";
default: default:
return ""; return "";
} }
} }
export function generalItemTypeIcon(type: controller_ItemType[]) { export function generalItemTypeIcon(type: types_ItemType[]) {
if (type.includes(controller_ItemType.Food)) { if (type.includes(types_ItemType.Food)) {
return "fa-cheese"; return "fa-cheese";
} else { } else {
return "fa-champagne-glasses"; return "fa-champagne-glasses";
@ -47,7 +47,7 @@ export function generalItemTypeIcon(type: controller_ItemType[]) {
export interface WebSocketMsg { export interface WebSocketMsg {
type: NotifierType; type: NotifierType;
payload: controller_Order[]; payload: service_Order[];
} }
export enum NotifierType { export enum NotifierType {
@ -75,4 +75,4 @@ export function lessThan15SecondsAgo(updated_at: number | undefined) {
return moment().diff(updated, "seconds") < 15; return moment().diff(updated, "seconds") < 15;
} }
export const emptyBill: controller_Bill = { table_id: 0, total: 0 }; export const emptyBill: service_Bill = { table_id: 0, total: 0 };

View file

@ -59,7 +59,7 @@
import { defineComponent, ref, watch } from "vue"; import { defineComponent, ref, watch } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue"; import BaseCard from "@/components/UI/BaseCard.vue";
import Calendar from "primevue/calendar"; import Calendar from "primevue/calendar";
import { BillsService, controller_Bill } from "@/services/openapi"; import { BillsService, service_Bill } from "@/services/openapi";
import Sidebar from "primevue/sidebar"; import Sidebar from "primevue/sidebar";
import BillModal from "@/components/Bills/BillModal.vue"; import BillModal from "@/components/Bills/BillModal.vue";
import { convertToEur, emptyBill, errorToast } from "@/utils"; import { convertToEur, emptyBill, errorToast } from "@/utils";
@ -80,11 +80,11 @@ export default defineComponent({
const confirm = useConfirm(); const confirm = useConfirm();
const toast = useToast(); const toast = useToast();
const today = ref(new Date()); const today = ref(new Date());
const bills = ref<controller_Bill[]>([]); const bills = ref<service_Bill[]>([]);
const isLoading = ref(false); const isLoading = ref(false);
const isDisabled = ref(false); const isDisabled = ref(false);
const billModal = ref(false); const billModal = ref(false);
const bill = ref<controller_Bill>({ ...emptyBill }); const bill = ref<service_Bill>({ ...emptyBill });
const filters = ref({ const filters = ref({
global: { value: null, matchMode: FilterMatchMode.CONTAINS }, global: { value: null, matchMode: FilterMatchMode.CONTAINS },
}); });
@ -104,7 +104,7 @@ export default defineComponent({
function openBill(billId: number) { function openBill(billId: number) {
if (isDisabled.value) return; if (isDisabled.value) return;
const temp: controller_Bill | undefined = bills.value.find((bill) => bill.id === billId); const temp: service_Bill | undefined = bills.value.find((bill) => bill.id === billId);
temp && (bill.value = temp); temp && (bill.value = temp);
billModal.value = true; billModal.value = true;
} }

View file

@ -55,7 +55,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, ref, watch } from "vue"; import { computed, defineComponent, ref, watch } from "vue";
import { BillsService, OrdersService, controller_Bill, controller_Order } from "@/services/openapi"; import { BillsService, OrdersService, service_Bill, service_Order } from "@/services/openapi";
import Checkbox from "primevue/checkbox"; import Checkbox from "primevue/checkbox";
import { convertToEur, emptyBill, errorToast } from "@/utils"; import { convertToEur, emptyBill, errorToast } from "@/utils";
import Button from "primevue/button"; import Button from "primevue/button";
@ -80,13 +80,13 @@ export default defineComponent({
const toast = useToast(); const toast = useToast();
const router = useRouter(); const router = useRouter();
const table = computed(() => parseInt(props.id)); const table = computed(() => parseInt(props.id));
const orders = ref<controller_Order[]>([]); const orders = ref<service_Order[]>([]);
const orderFilter = ref<number[]>([]); const orderFilter = ref<number[]>([]);
const isLoading = ref(false); const isLoading = ref(false);
const applyFilterLoading = ref(false); const applyFilterLoading = ref(false);
const checkAll = ref(false); const checkAll = ref(false);
const total = ref(0); const total = ref(0);
const bill = ref<controller_Bill>({ ...emptyBill }); const bill = ref<service_Bill>({ ...emptyBill });
const billModal = ref(false); const billModal = ref(false);
function checkAllCheck() { function checkAllCheck() {

View file

@ -17,7 +17,7 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, reactive, ref, watch } from "vue"; import { computed, defineComponent, reactive, ref, watch } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue"; import BaseCard from "@/components/UI/BaseCard.vue";
import { controller_OrderItem, controller_ItemType } from "@/services/openapi"; import { service_OrderItem, types_ItemType } from "@/services/openapi";
import OrderItemList from "@/components/OrderItem/OrderItemList.vue"; import OrderItemList from "@/components/OrderItem/OrderItemList.vue";
import { useStore } from "vuex"; import { useStore } from "vuex";
import WaveSpinner from "@/components/UI/WaveSpinner.vue"; import WaveSpinner from "@/components/UI/WaveSpinner.vue";
@ -31,8 +31,8 @@ export default defineComponent({
const isLoading = ref(true); const isLoading = ref(true);
const orderItems = computed(() => store.getters.getOrderItems); const orderItems = computed(() => store.getters.getOrderItems);
const currentOrderItems = ref(); const currentOrderItems = ref();
const emptyOrderItem = reactive<controller_OrderItem>({ description: "", item_type: 0, price: 0 }); const emptyOrderItem = reactive<service_OrderItem>({ description: "", item_type: 0, price: 0 });
const intId = ref<controller_ItemType>(parseInt(props.id)); const intId = ref<types_ItemType>(parseInt(props.id));
getData(); getData();
async function getData() { async function getData() {
@ -50,15 +50,15 @@ export default defineComponent({
watch(props, () => getData()); watch(props, () => getData());
function orderItemChanged(item: controller_OrderItem) { function orderItemChanged(item: service_OrderItem) {
store.dispatch("updateOrderItem", item); store.dispatch("updateOrderItem", item);
refreshMap(); refreshMap();
} }
function orderItemDeleted(item: controller_OrderItem) { function orderItemDeleted(item: service_OrderItem) {
store.dispatch("deleteOrderItem", item); store.dispatch("deleteOrderItem", item);
refreshMap(); refreshMap();
} }
function orderItemCreated(item: controller_OrderItem) { function orderItemCreated(item: service_OrderItem) {
store.dispatch("addOrderItem", item); store.dispatch("addOrderItem", item);
refreshMap(); refreshMap();
} }

View file

@ -15,7 +15,7 @@
/> />
</template> </template>
<template v-if="user.show_cold_drinks"> <template v-if="user.show_cold_drinks">
<OrderSection :orders="coldOrders" :itemType="controller_ItemType.ColdDrink" @filterOrders="(id) => filterOrder(id)" /> <OrderSection :orders="coldOrders" :itemType="types_ItemType.ColdDrink" @filterOrders="(id) => filterOrder(id)" />
</template> </template>
</div> </div>
</Transition> </Transition>
@ -25,9 +25,9 @@
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, onUnmounted, provide, ref } from "vue"; import { computed, defineComponent, onUnmounted, provide, ref } from "vue";
import BaseCard from "@/components/UI/BaseCard.vue"; import BaseCard from "@/components/UI/BaseCard.vue";
import { OrdersService, controller_Order, controller_ItemType, controller_User } from "@/services/openapi"; import { OrdersService, service_Order, types_ItemType, user_User } from "@/services/openapi";
import { detailedItemTypeIcon, detailedItemTypeString, NotifierType, WebSocketMsg } from "@/utils"; import { detailedItemTypeIcon, detailedItemTypeString, NotifierType, WebSocketMsg } from "@/utils";
import { API_ENDPOINT_URL } from "@/main"; import { WEBSOCKET_ENDPOINT_URL } from "@/main";
import EmptyView from "@/views/Empty.vue"; import EmptyView from "@/views/Empty.vue";
import WaveSpinner from "@/components/UI/WaveSpinner.vue"; import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import { disabled, loading } from "@/keys"; import { disabled, loading } from "@/keys";
@ -43,8 +43,8 @@ export default defineComponent({
const isDisabled = ref(false); const isDisabled = ref(false);
provide(disabled, isDisabled); provide(disabled, isDisabled);
provide(loading, isDisabled); provide(loading, isDisabled);
const orders = ref<controller_Order[]>([]); const orders = ref<service_Order[]>([]);
const user = computed<controller_User>(() => store.getters.getUser); const user = computed<user_User>(() => store.getters.getUser);
const empty = computed(() => { const empty = computed(() => {
return ( return (
@ -55,9 +55,9 @@ export default defineComponent({
}); });
const otherOrders = computed(() => { const otherOrders = computed(() => {
const temp = new Map<number, controller_Order[]>(); const temp = new Map<number, service_Order[]>();
orders.value.forEach((order) => { orders.value.forEach((order) => {
if (order.order_item.item_type === controller_ItemType.ColdDrink) return; if (order.order_item.item_type === types_ItemType.ColdDrink) return;
const existing = temp.get(order.table_id); const existing = temp.get(order.table_id);
if (existing) { if (existing) {
existing.push(order); existing.push(order);
@ -67,8 +67,8 @@ export default defineComponent({
}); });
return new Map([...temp.entries()].sort()); return new Map([...temp.entries()].sort());
}); });
const coldOrders = computed(() => orders.value.filter((order) => order.order_item.item_type === controller_ItemType.ColdDrink)); const coldOrders = computed(() => orders.value.filter((order) => order.order_item.item_type === types_ItemType.ColdDrink));
const sse = ref<EventSource | null>(null); const ws = ref<WebSocket | null>(null);
getData(); getData();
function getData() { function getData() {
@ -77,21 +77,37 @@ export default defineComponent({
.then((res) => (orders.value = res)) .then((res) => (orders.value = res))
.finally(() => { .finally(() => {
isLoading.value = false; isLoading.value = false;
setupSSE(); startWebsocket();
}); });
} }
onUnmounted(() => sse.value && sse.value.close()); onUnmounted(() => stopWebsocket());
addEventListener("beforeunload", () => {
sse.value && sse.value.close();
});
function filterOrder(id: number) { function filterOrder(id: number) {
orders.value = orders.value.filter((old) => old.id !== id); orders.value = orders.value.filter((old) => old.id !== id);
} }
function setupSSE() { function startWebsocket() {
sse.value = new EventSource(API_ENDPOINT_URL + "/orders/sse?stream=sse"); ws.value = new WebSocket(WEBSOCKET_ENDPOINT_URL);
sse.value.onmessage = (evt: Event) => { ws.value.addEventListener("message", parseWebsocket);
ws.value.addEventListener("error", handleWebsocketError);
}
function stopWebsocket() {
if (ws.value) {
ws.value.removeEventListener("message", parseWebsocket);
ws.value.removeEventListener("error", handleWebsocketError);
ws.value.close();
}
}
function handleWebsocketError() {
stopWebsocket();
setTimeout(() => {
startWebsocket();
}, 1000);
}
function parseWebsocket(evt: Event) {
isDisabled.value = true; isDisabled.value = true;
const messageEvent = evt as MessageEvent; const messageEvent = evt as MessageEvent;
const webSocketMsg: WebSocketMsg = JSON.parse(messageEvent.data); const webSocketMsg: WebSocketMsg = JSON.parse(messageEvent.data);
@ -106,7 +122,6 @@ export default defineComponent({
} }
sortOrders(); sortOrders();
isDisabled.value = false; isDisabled.value = false;
};
} }
function sortOrders() { function sortOrders() {
@ -118,7 +133,7 @@ export default defineComponent({
otherOrders, otherOrders,
coldOrders, coldOrders,
filterOrder, filterOrder,
controller_ItemType, types_ItemType,
isLoading, isLoading,
isDisabled, isDisabled,
detailedItemTypeString, detailedItemTypeString,

61
go.mod
View file

@ -1,61 +1,70 @@
module gitlab.unjx.de/flohoss/cafe-plaetschwiesle module cafe
go 1.20 go 1.20
require ( require (
github.com/caarlos0/env/v8 v8.0.0 github.com/gin-contrib/cors v1.4.0
github.com/containrrr/shoutrrr v0.7.1 github.com/gin-contrib/static v0.0.1
github.com/go-playground/validator/v10 v10.14.1 github.com/gin-gonic/gin v1.9.1
github.com/labstack/echo/v4 v4.10.2 github.com/go-playground/assert/v2 v2.2.0
github.com/r3labs/sse/v2 v2.10.0 github.com/gorilla/websocket v1.5.0
github.com/stretchr/testify v1.8.4 github.com/mitchellh/mapstructure v1.5.0
github.com/swaggo/echo-swagger v1.4.0 github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.16.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.1 github.com/swaggo/swag v1.16.1
go.uber.org/zap v1.24.0 github.com/unjx-de/go-folder v1.0.7
gorm.io/driver/mysql v1.5.1 gorm.io/driver/mysql v1.5.1
gorm.io/driver/sqlite v1.5.2 gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2 gorm.io/gorm v1.25.2
gorm.io/plugin/soft_delete v1.2.1 gorm.io/plugin/soft_delete v1.2.1
moul.io/zapgorm2 v1.3.0
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/bytedance/sonic v1.9.2 // indirect
github.com/fatih/color v1.15.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/labstack/gommon v0.4.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/spf13/afero v1.9.5 // indirect
go.uber.org/atomic v1.11.0 // indirect github.com/spf13/cast v1.5.1 // indirect
go.uber.org/multierr v1.11.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.10.0 // indirect golang.org/x/crypto v0.10.0 // indirect
golang.org/x/net v0.11.0 // indirect golang.org/x/net v0.11.0 // indirect
golang.org/x/sys v0.9.0 // indirect golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect golang.org/x/text v0.10.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.10.0 // indirect golang.org/x/tools v0.10.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

780
go.sum

File diff suppressed because it is too large Load diff

55
hub/hub.go Normal file
View file

@ -0,0 +1,55 @@
package hub
import (
"cafe/service"
"github.com/sirupsen/logrus"
)
type (
NotifierChan chan service.WebSocketMsg
Hub struct {
Notifier NotifierChan
NewClients chan NotifierChan
ClosingClients chan NotifierChan
clients map[NotifierChan]struct{}
}
)
func (h *Hub) Initialize() {
h.Notifier = make(NotifierChan)
h.NewClients = make(chan NotifierChan)
h.ClosingClients = make(chan NotifierChan)
h.clients = make(map[NotifierChan]struct{})
go h.listen()
go func() {
for {
if msg, ok := <-service.LiveCh; ok {
h.Notifier <- msg
}
}
}()
}
func (h *Hub) listen() {
for {
select {
case s := <-h.NewClients:
h.clients[s] = struct{}{}
logrus.WithField("openConnections", len(h.clients)).Trace("Websocket connection added")
case s := <-h.ClosingClients:
delete(h.clients, s)
logrus.WithField("openConnections", len(h.clients)).Trace("Websocket connection removed")
case event := <-h.Notifier:
for client := range h.clients {
select {
case client <- event:
default:
close(client)
delete(h.clients, client)
}
}
}
}
}

View file

@ -1,122 +0,0 @@
package controller
import (
"fmt"
"strconv"
"time"
)
type (
Bill struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
TableID uint64 `json:"table_id" validate:"required"`
Total float32 `json:"total" validate:"required"`
CreatedAt int64 `json:"created_at" validate:"optional"`
}
BillItem struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
BillID uint64 `json:"bill_id" validate:"required"`
Description string `json:"description" validate:"required"`
Total float32 `json:"total" validate:"required"`
Price float32 `json:"price" validate:"required"`
Amount uint64 `json:"amount" validate:"required"`
ItemType ItemType `json:"item_type" validate:"required"`
}
)
func (c *Controller) DoesBillExist(id string) (Bill, error) {
var bill Bill
result := c.orm.Limit(1).Find(&bill, id)
if result.RowsAffected == 0 {
return bill, fmt.Errorf(CannotFind.String())
}
return bill, nil
}
func (c *Controller) GetAllBillItems(billId uint64) ([]BillItem, error) {
var billItems []BillItem
result := c.orm.Where("bill_id = ?", billId).Find(&billItems)
if result.RowsAffected == 0 {
return billItems, fmt.Errorf(CannotFind.String())
}
return billItems, nil
}
func getDate(year string, month string, day string) (time.Time, error) {
yearI, yearErr := strconv.Atoi(year)
if yearErr != nil {
return time.Time{}, fmt.Errorf("jahr " + CannotParse.String())
}
monthI, monthErr := strconv.Atoi(month)
if monthErr != nil {
return time.Time{}, fmt.Errorf("monat " + CannotParse.String())
}
dayI, dayErr := strconv.Atoi(day)
if dayErr != nil {
return time.Time{}, fmt.Errorf("tag " + CannotParse.String())
}
loc, locErr := time.LoadLocation("Local")
if locErr != nil {
return time.Time{}, fmt.Errorf("timezone " + CannotParse.String())
}
return time.Date(yearI, time.Month(monthI), dayI, 0, 0, 0, 0, loc), nil
}
func (c *Controller) GetAllBills(year string, month string, day string) ([]Bill, error) {
var bills []Bill
today, err := getDate(year, month, day)
if err != nil {
return bills, err
}
beginningOfDay := today.Unix()
endOfDay := today.Add(23 * time.Hour).Add(59 * time.Minute).Add(59 * time.Second).Unix()
c.orm.Where("created_at BETWEEN ? AND ?", beginningOfDay, endOfDay).Order("created_at").Find(&bills)
return bills, nil
}
func (c *Controller) createBill(options GetOrderOptions) (Bill, error) {
orders := c.getAllOrdersForTable(options)
var bill Bill
var total float32 = 0
for _, order := range orders {
total += order.Total
}
bill.TableID = options.TableId
bill.Total = total
err := c.orm.Create(&bill).Error
if err != nil {
return bill, fmt.Errorf(CannotCreate.String())
}
for _, order := range orders {
billItem := BillItem{
BillID: bill.ID,
Description: order.OrderItem.Description,
Total: order.Total,
Price: order.OrderItem.Price,
Amount: order.OrderCount,
ItemType: order.OrderItem.ItemType,
}
c.orm.Create(&billItem)
}
ordersToDelete := c.getAllOrdersForTable(GetOrderOptions{TableId: options.TableId, Grouped: false, Filter: options.Filter})
err = c.orm.Delete(&ordersToDelete).Error
if err != nil {
return bill, fmt.Errorf(CannotDelete.String())
}
c.publishMessage(StatusMessage{
Type: DeleteAll,
Payload: ordersToDelete,
})
return bill, nil
}
func (c *Controller) deleteBill(bill *Bill) error {
err := c.orm.Delete(bill).Error
if err != nil {
return fmt.Errorf(CannotDelete.String())
}
billItemsToDelete, _ := c.GetAllBillItems(bill.ID)
c.orm.Delete(&billItemsToDelete)
return nil
}

View file

@ -1,34 +0,0 @@
package controller
import (
"github.com/r3labs/sse/v2"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/database"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"gorm.io/gorm"
)
type Controller struct {
orm *gorm.DB
env *env.Config
SSE *sse.Server
}
func NewController(env *env.Config) *Controller {
db := database.NewDatabaseConnection(&database.Database{
Host: env.DB_Host,
User: env.DB_User,
Password: env.DB_Password,
Database: env.DB_Database,
})
db.AutoMigrate(&Table{})
db.AutoMigrate(&Order{})
db.AutoMigrate(&OrderItem{})
db.AutoMigrate(&Bill{})
db.AutoMigrate(&BillItem{})
db.AutoMigrate(&User{})
ctrl := Controller{orm: db, env: env, SSE: sse.New()}
ctrl.setupEventChannel()
return &ctrl
}

View file

@ -1,32 +0,0 @@
package controller
import (
"encoding/json"
"github.com/r3labs/sse/v2"
)
const ServerSideEvent = "sse"
type NotifierType uint
const (
Create NotifierType = iota
Delete
DeleteAll
)
type StatusMessage struct {
Type NotifierType `json:"type"`
Payload []Order `json:"payload"`
}
func (c *Controller) setupEventChannel() {
c.SSE.AutoReplay = false
c.SSE.CreateStream(ServerSideEvent)
}
func (c *Controller) publishMessage(msg StatusMessage) {
json, _ := json.Marshal(msg)
c.SSE.Publish(ServerSideEvent, &sse.Event{Data: json})
}

View file

@ -1,167 +0,0 @@
package controller
import (
"fmt"
"time"
"gorm.io/gorm"
)
type (
Order struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
TableID uint64 `json:"table_id" validate:"required"`
OrderItemID uint64 `json:"order_item_id" validate:"required"`
OrderItem OrderItem `json:"order_item" validate:"required"`
UpdatedAt int64 `json:"updated_at" validate:"optional"`
IsServed bool `json:"is_served" default:"false" validate:"required"`
Total float32 `json:"total" validate:"required"`
OrderCount uint64 `json:"order_count" validate:"required"`
}
OrderItem struct {
ID uint64 `gorm:"primaryKey" json:"id" validate:"optional"`
ItemType ItemType `json:"item_type" validate:"required"`
Description string `json:"description" validate:"required"`
Price float32 `json:"price" validate:"required"`
}
GetOrderOptions struct {
TableId uint64 `json:"table_id"`
Grouped bool `json:"grouped"`
Filter []string `json:"filter"`
}
)
func updateTableUpdatedAt(tx *gorm.DB, o *Order) {
var table Table
tx.Where("id = ?", o.TableID).First(&table)
table.UpdatedAt = time.Now().Unix()
tx.Save(&table)
}
func (o *Order) AfterCreate(tx *gorm.DB) (err error) {
updateTableUpdatedAt(tx, o)
return
}
func (o *Order) AfterDelete(tx *gorm.DB) (err error) {
updateTableUpdatedAt(tx, o)
return
}
func (c *Controller) doesOrderItemExist(id string) (OrderItem, error) {
var orderItem OrderItem
result := c.orm.Limit(1).Find(&orderItem, id)
if result.RowsAffected == 0 {
return orderItem, fmt.Errorf(CannotFind.String())
}
return orderItem, nil
}
func (c *Controller) doesOrderExist(id string) (Order, error) {
var order Order
result := c.orm.Limit(1).Find(&order, id)
if result.RowsAffected == 0 {
return order, fmt.Errorf(CannotFind.String())
}
return order, nil
}
func (c *Controller) getAllActiveOrders() []Order {
var orders []Order
c.orm.Model(&Order{}).Joins("OrderItem").Where("is_served = ?", 0).Order("updated_at").Find(&orders)
return orders
}
func (c *Controller) getAllOrdersForTable(options GetOrderOptions) []Order {
var orders []Order
if options.Grouped {
if len(options.Filter) == 0 {
c.orm.Model(&Order{}).Joins("OrderItem").Select("table_id, order_item_id, sum(price) as total, count(order_item_id) as order_count").Group("order_item_id").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
c.orm.Model(&Order{}).Find(&orders, options.Filter).Joins("OrderItem").Select("table_id, order_item_id, sum(price) as total, count(order_item_id) as order_count").Group("order_item_id").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
}
} else {
if len(options.Filter) == 0 {
c.orm.Model(&Order{}).Joins("OrderItem").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
c.orm.Model(&Order{}).Find(&orders, options.Filter).Where("table_id = ?", options.TableId).Find(&orders)
}
}
return orders
}
func (c *Controller) createOrder(order *Order) error {
err := c.orm.Create(order).Error
if err != nil {
return fmt.Errorf(CannotCreate.String())
}
c.orm.Model(&Order{}).Joins("OrderItem").First(order)
c.publishMessage(StatusMessage{
Type: Create,
Payload: []Order{*order},
})
return nil
}
func (c *Controller) updateOrder(old *Order, new *Order) error {
err := c.orm.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(CannotUpdate.String())
}
if new.IsServed {
c.publishMessage(StatusMessage{
Type: Delete,
Payload: []Order{*new},
})
}
return nil
}
func (c *Controller) deleteOrder(tableId string, orderItemId string) error {
var order Order
err := c.orm.Where("table_id = ? AND order_item_id = ?", tableId, orderItemId).Last(&order).Error
if err != nil {
return fmt.Errorf(CannotFind.String())
}
err = c.orm.Delete(&order).Error
if err != nil {
return fmt.Errorf(CannotDelete.String())
}
c.publishMessage(StatusMessage{
Type: Delete,
Payload: []Order{order},
})
return nil
}
func (c *Controller) getOrderItemsForType(itemType string) []OrderItem {
var orderItems []OrderItem
c.orm.Order("description").Where("item_type = ?", ParseItemType(itemType)).Find(&orderItems)
return orderItems
}
func (c *Controller) createOrderItem(oderItem *OrderItem) error {
err := c.orm.Create(oderItem).Error
if err != nil {
return fmt.Errorf(CannotCreate.String())
}
return nil
}
func (c *Controller) updateOrderItem(old *OrderItem, new *OrderItem) error {
err := c.orm.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(CannotUpdate.String())
}
return nil
}
func (c *Controller) deleteOrderItem(oderItem *OrderItem) error {
err := c.orm.Delete(oderItem).Error
if err != nil {
return fmt.Errorf(CannotDelete.String())
}
return nil
}

View file

@ -1,104 +0,0 @@
package controller
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get all bills
// @Description gets all bills as array
// @Tags bills
// @Produce json
// @Param year query int true "year"
// @Param month query int true "month (1-12)"
// @Param day query int true "day (1-31)"
// @Success 200 {array} Bill
// @Router /bills [get]
func (c *Controller) GetBills(ctx echo.Context) error {
year := ctx.QueryParam("year")
month := ctx.QueryParam("month")
day := ctx.QueryParam("day")
if year == "" || month == "" || day == "" {
return echo.NewHTTPError(http.StatusInternalServerError, MissingInformation.String())
}
bills, err := c.GetAllBills(year, month, day)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, bills)
}
// @Schemes
// @Summary get all billItems
// @Description gets all billItems for bill
// @Tags bills
// @Produce json
// @Param bill query int true "Bill ID"
// @Success 200 {array} BillItem
// @Router /bills/items [get]
func (c *Controller) GetBillItems(ctx echo.Context) error {
bill, err := c.DoesBillExist(ctx.QueryParam("bill"))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
billItems, err := c.GetAllBillItems(bill.ID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, billItems)
}
// @Schemes
// @Summary create new bill
// @Description creates a new bill and returns it
// @Tags bills
// @Produce json
// @Param table query int true "Table ID"
// @Param filter query string false "filter"
// @Success 201 {object} Bill
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /bills [post]
func (c *Controller) CreateBill(ctx echo.Context) error {
table, tableErr := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
if tableErr != nil {
return echo.NewHTTPError(http.StatusBadRequest, MissingInformation.String())
}
stringFiler := ctx.QueryParam("filter")
var filter []string
if stringFiler != "" {
filter = strings.Split(stringFiler, ",")
}
bill, err := c.createBill(GetOrderOptions{TableId: table, Grouped: true, Filter: filter})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, bill)
}
// @Schemes
// @Summary delete a bill
// @Description deletes a bill
// @Tags bills
// @Produce json
// @Param id path int true "Bill ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /bills/{id} [delete]
func (c *Controller) DeleteBill(ctx echo.Context) error {
id := ctx.Param("id")
bill, err := c.DoesBillExist(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.deleteBill(&bill)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -1,208 +0,0 @@
package controller
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get all orders
// @Description gets all orders as array
// @Tags orders
// @Produce json
// @Param table query int false "Table ID"
// @Param grouping query bool false "grouping"
// @Param filter query string false "filter"
// @Success 200 {array} Order
// @Router /orders [get]
func (c *Controller) GetOrders(ctx echo.Context) error {
table, _ := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
grouping, _ := strconv.ParseBool(ctx.QueryParam("grouping"))
stringFiler := ctx.QueryParam("filter")
var filter []string
if stringFiler != "" {
filter = strings.Split(stringFiler, ",")
}
options := GetOrderOptions{TableId: table, Grouped: grouping, Filter: filter}
var orders []Order
if options.TableId == 0 {
orders = c.getAllActiveOrders()
} else {
orders = c.getAllOrdersForTable(options)
}
return ctx.JSON(http.StatusOK, orders)
}
// @Schemes
// @Summary create new order
// @Description creates a new order and returns it
// @Tags orders
// @Accept json
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 201 {object} Order
// @Failure 400
// @Failure 500 "Internal Server Error"
// @Router /orders [post]
func (c *Controller) CreateOrder(ctx echo.Context) error {
table, err1 := strconv.ParseUint(ctx.QueryParam("table"), 10, 64)
item, err2 := strconv.ParseUint(ctx.QueryParam("item"), 10, 64)
if err1 != nil || err2 != nil {
return echo.NewHTTPError(http.StatusBadRequest, MissingInformation.String())
}
order := Order{TableID: table, OrderItemID: item, IsServed: false}
err := c.createOrder(&order)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, order)
}
// @Schemes
// @Summary delete an order
// @Description deletes an order from the database
// @Tags orders
// @Produce json
// @Param item query int true "OrderItem ID"
// @Param table query int true "Table ID"
// @Success 200 "OK"
// @Failure 400 "Bad Request"
// @Failure 500 "Internal Server Error"
// @Router /orders [delete]
func (c *Controller) DeleteOrder(ctx echo.Context) error {
item := ctx.QueryParam("item")
table := ctx.QueryParam("table")
if table == "" || item == "" {
return echo.NewHTTPError(http.StatusBadRequest, MissingInformation.String())
}
err := c.deleteOrder(table, item)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}
// @Schemes
// @Summary update an order
// @Description updates an order with provided information
// @Tags orders
// @Accept json
// @Produce json
// @Param order body Order true "updated Order"
// @Success 200 {object} Order
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders [put]
func (c *Controller) UpdateOrder(ctx echo.Context) error {
var newOrder Order
err := ctx.Bind(&newOrder)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldOrder, err := c.doesOrderExist(strconv.Itoa(int(newOrder.ID)))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateOrder(&oldOrder, &newOrder)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newOrder)
}
// @Schemes
// @Summary get all orderItems
// @Description gets all orderItems as array
// @Tags orderItems
// @Produce json
// @Param type query int true "ItemType"
// @Success 200 {array} OrderItem
// @Router /orders/items [get]
func (c *Controller) GetOrderItems(ctx echo.Context) error {
orderType := ctx.QueryParam("type")
if orderType == "" {
return echo.NewHTTPError(http.StatusBadRequest, MissingInformation.String())
}
return ctx.JSON(http.StatusOK, c.getOrderItemsForType(orderType))
}
// @Schemes
// @Summary create new orderItem
// @Description creates a new orderItem and returns it
// @Tags orderItems
// @Accept json
// @Produce json
// @Param order body OrderItem true "OrderItem ID"
// @Success 201 {object} OrderItem
// @Failure 400 "Bad Request"
// @Failure 500 "Internal Server Error"
// @Router /orders/items [post]
func (c *Controller) CreateOrderItem(ctx echo.Context) error {
var orderItem OrderItem
err := ctx.Bind(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
err = c.createOrderItem(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, orderItem)
}
// @Schemes
// @Summary update a orderItem
// @Description updates a orderItem with provided information
// @Tags orderItems
// @Accept json
// @Produce json
// @Param orderItem body OrderItem true "updated OrderItem"
// @Success 200 {object} OrderItem
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders/items [put]
func (c *Controller) UpdateOrderItem(ctx echo.Context) error {
var newOrderItem OrderItem
err := ctx.Bind(&newOrderItem)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldOrderItem, err := c.doesOrderItemExist(strconv.Itoa(int(newOrderItem.ID)))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateOrderItem(&oldOrderItem, &newOrderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newOrderItem)
}
// @Schemes
// @Summary delete an orderItem
// @Description deletes an orderItem from the database
// @Tags orderItems
// @Produce json
// @Param id path int true "OrderItem ID"
// @Success 200 "OK"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /orders/items/{id} [delete]
func (c *Controller) DeleteOrderItem(ctx echo.Context) error {
id := ctx.Param("id")
orderItem, err := c.doesOrderItemExist(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.deleteOrderItem(&orderItem)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -1,51 +0,0 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get all active tables
// @Description gets all active tables as array
// @Tags tables
// @Produce json
// @Success 200 {array} Table
// @Router /tables [get]
func (c *Controller) GetTables(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, c.GetAllTables())
}
// @Schemes
// @Summary create new table
// @Description creates a new table and returns it
// @Tags tables
// @Accept json
// @Produce json
// @Success 201 {object} Table "Table has been created"
// @Failure 500 "Internal Server Error"
// @Router /tables [post]
func (c *Controller) CreateTable(ctx echo.Context) error {
table, err := c.CreateNewTable()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusCreated, table)
}
// @Schemes
// @Summary delete the latest table
// @Description deletes the latest table from the database
// @Tags tables
// @Produce json
// @Success 200 "OK"
// @Failure 500 "Internal Server Error"
// @Router /tables [delete]
func (c *Controller) DeleteTable(ctx echo.Context) error {
err := c.DeleteLatestTable()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.NoContent(http.StatusOK)
}

View file

@ -1,54 +0,0 @@
package controller
import (
"net/http"
"github.com/labstack/echo/v4"
)
// @Schemes
// @Summary get a user
// @Description gets a user
// @Tags users
// @Produce json
// @Param username path string true "Username"
// @Success 200 {object} User
// @Failure 500 "Internal Server Error"
// @Router /users/{username} [get]
func (c *Controller) GetUser(ctx echo.Context) error {
username := ctx.Param("username")
u, err := c.getUserOrCreate(username)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return ctx.JSON(http.StatusOK, u)
}
// @Schemes
// @Summary update a user
// @Description updates a user with provided information
// @Tags users
// @Accept json
// @Produce json
// @Param user body User true "updated User"
// @Success 200 {object} User
// @Failure 400 "Bad Request"
// @Failure 404 "Not Found"
// @Failure 500 "Internal Server Error"
// @Router /users [put]
func (c *Controller) UpdateUser(ctx echo.Context) error {
var newUser User
err := ctx.Bind(&newUser)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
oldUser, err := c.doesUserExist(newUser.Username)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, err.Error())
}
err = c.updateUser(&oldUser, &newUser)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, newUser)
}

View file

@ -1,40 +0,0 @@
package controller
import (
"fmt"
)
type User struct {
Username string `gorm:"primaryKey" json:"username" validate:"required"`
ShowColdDrinks bool `json:"show_cold_drinks" validate:"required"`
ShowHotDrinks bool `json:"show_hot_drinks" validate:"required"`
}
func (c *Controller) doesUserExist(username string) (User, error) {
var user User
result := c.orm.Limit(1).Find(&user, "username = ?", username)
if result.RowsAffected == 0 {
return user, fmt.Errorf(CannotFind.String())
}
return user, nil
}
func (c *Controller) getUserOrCreate(username string) (User, error) {
var user User
err := c.orm.Where(User{Username: username}).Attrs(User{ShowHotDrinks: true, ShowColdDrinks: true}).FirstOrCreate(&user).Error
if err != nil {
return user, fmt.Errorf(CannotCreate.String())
}
return user, nil
}
func (c *Controller) updateUser(old *User, new *User) error {
err := c.orm.First(old).Updates(map[string]interface{}{
"Username": new.Username,
"ShowColdDrinks": new.ShowColdDrinks,
"ShowHotDrinks": new.ShowHotDrinks}).Error
if err != nil {
return fmt.Errorf(CannotUpdate.String())
}
return nil
}

View file

@ -1,81 +0,0 @@
package database
import (
"fmt"
"net"
"os"
"time"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"moul.io/zapgorm2"
)
type Database struct {
Host string
User string
Password string
Database string
}
const Storage = "storage/"
func init() {
os.Mkdir(Storage, os.ModePerm)
}
func (d *Database) tryDbConnection() {
i := 1
total := 20
for i <= total {
ln, err := net.DialTimeout("tcp", d.Host, 1*time.Second)
if err != nil {
if i == total {
zap.L().Fatal("Failed connecting to database", zap.Int("attempt", i))
}
zap.L().Warn("Connecting to database", zap.Int("attempt", i))
time.Sleep(2 * time.Second)
i++
} else {
_ = ln.Close()
zap.L().Info("Connected to database", zap.Int("attempt", i))
i = total + 1
}
}
}
func (d *Database) initializeMySql(conf *gorm.Config) *gorm.DB {
var err error
d.tryDbConnection()
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
d.User,
d.Password,
d.Host,
d.Database,
)
orm, err := gorm.Open(mysql.Open(dsn), conf)
if err != nil {
zap.L().Error(err.Error())
}
return orm
}
func (d *Database) initializeSqLite(conf *gorm.Config) *gorm.DB {
var err error
orm, err := gorm.Open(sqlite.Open(Storage+"db.sqlite?_pragma=foreign_keys(1)"), conf)
if err != nil {
zap.L().Error(err.Error())
}
return orm
}
func NewDatabaseConnection(d *Database) *gorm.DB {
logger := zapgorm2.New(zap.L())
conf := &gorm.Config{Logger: logger, PrepareStmt: true}
if d.Host == "" {
return d.initializeSqLite(conf)
}
return d.initializeMySql(conf)
}

70
internal/env/env.go vendored
View file

@ -1,70 +0,0 @@
package env
import (
"errors"
"fmt"
"os"
"github.com/caarlos0/env/v8"
"github.com/containrrr/shoutrrr"
"github.com/go-playground/validator/v10"
)
type Config struct {
TimeZone string `env:"TZ" envDefault:"Etc/UTC" validate:"timezone"`
Port int `env:"PORT" envDefault:"8080" validate:"min=1024,max=49151"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info" validate:"oneof=debug info warn error panic fatal"`
Version string `env:"VERSION" envDefault:"v0.0.0"`
SwaggerHost string `env:"SWAGGER_HOST"`
DB_Host string `env:"DB_HOST"`
DB_User string `env:"DB_USER"`
DB_Password string `env:"DB_PASSWORD"`
DB_Database string `env:"DB_DATABASE"`
}
var errParse = errors.New("error parsing environment variables")
func Parse() (*Config, error) {
cfg := &Config{}
if err := env.Parse(cfg); err != nil {
return cfg, err
}
if err := validateContent(cfg); err != nil {
return cfg, err
}
setAllDefaultEnvs(cfg)
return cfg, nil
}
func newEnvValidator() *validator.Validate {
validate := validator.New()
validate.RegisterValidation(`shoutrrr`, func(fl validator.FieldLevel) bool {
value := fl.Field().Interface().(string)
_, err := shoutrrr.CreateSender(value)
return err == nil
})
return validate
}
func validateContent(cfg *Config) error {
validate := newEnvValidator()
err := validate.Struct(cfg)
if err != nil {
if _, ok := err.(*validator.InvalidValidationError); ok {
return err
} else {
for _, err := range err.(validator.ValidationErrors) {
return err
}
}
return errParse
}
return nil
}
func setAllDefaultEnvs(cfg *Config) {
os.Setenv("TZ", cfg.TimeZone)
os.Setenv("PORT", fmt.Sprintf("%d", cfg.Port))
os.Setenv("LOG_LEVEL", cfg.LogLevel)
}

View file

@ -1,56 +0,0 @@
package env
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestPortParser(t *testing.T) {
key := "PORT"
var err error
defer func() {
os.Unsetenv(key)
}()
os.Setenv(key, "1024")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "-12")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.Port' Error:Field validation for 'Port' failed on the 'min' tag", "Validation should fail")
os.Setenv(key, "60000")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.Port' Error:Field validation for 'Port' failed on the 'max' tag", "Validation should fail")
os.Setenv(key, "abc")
_, err = Parse()
assert.Equal(t, err.Error(), "env: parse error on field \"Port\" of type \"int\": strconv.ParseInt: parsing \"abc\": invalid syntax", "Parsing should fail")
}
func TestTimeZoneParser(t *testing.T) {
key := "TZ"
var err error
defer func() {
os.Unsetenv(key)
}()
os.Setenv(key, "Europe/Berlin")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "Etc/UTC")
_, err = Parse()
assert.Equal(t, err, nil, "Parsing should pass")
os.Setenv(key, "abc")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.TimeZone' Error:Field validation for 'TimeZone' failed on the 'timezone' tag", "Validation should fail")
os.Setenv(key, "-1")
_, err = Parse()
assert.Equal(t, err.Error(), "Key: 'Config.TimeZone' Error:Field validation for 'TimeZone' failed on the 'timezone' tag", "Validation should fail")
}

View file

@ -1,44 +0,0 @@
package logging
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestInfoLogger(t *testing.T) {
level := "info"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestWarnLogger(t *testing.T) {
level := "warn"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestDebugLogger(t *testing.T) {
level := "debug"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), level, fmt.Sprintf("Level should be %s", level))
}
func TestInvalidLogger(t *testing.T) {
level := "invalid"
log := CreateLogger(level)
defer log.Sync()
assert.NotEmpty(t, log, "Logger should not be nil")
assert.Equal(t, log.Level().String(), "info", "Level should be info")
}

View file

@ -1,33 +0,0 @@
package logging
import (
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func CreateLogger(logLevel string) *zap.Logger {
encoderCfg := zap.NewProductionEncoderConfig()
encoderCfg.TimeKey = "time"
encoderCfg.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli)
level := zap.NewAtomicLevelAt(zap.InfoLevel)
zapLevel, err := zap.ParseAtomicLevel(logLevel)
if err == nil {
level = zapLevel
}
config := zap.Config{
Level: level,
Development: false,
DisableCaller: false,
DisableStacktrace: false,
Sampling: nil,
Encoding: "json",
EncoderConfig: encoderCfg,
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
}
return zap.Must(config.Build())
}

View file

@ -1,20 +0,0 @@
package router
import (
"github.com/labstack/echo/v4"
)
func longCacheLifetime(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=31536000")
return next(c)
}
}
func authHeader(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("Remote-Groups", c.Request().Header.Get("Remote-Groups"))
c.Response().Header().Set("Remote-Name", c.Request().Header.Get("Remote-Name"))
return next(c)
}
}

View file

@ -1,112 +0,0 @@
package router
import (
"net/http"
"net/url"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/docs"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/controller"
"gitlab.unjx.de/flohoss/cafe-plaetschwiesle/internal/env"
"go.uber.org/zap"
)
func InitRouter() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.Recover())
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c echo.Context) bool {
return strings.Contains(c.Request().URL.Path, "swagger")
},
}))
e.Pre(middleware.RemoveTrailingSlash())
e.Validator = &CustomValidator{Validator: newValidator()}
e.Renderer = initTemplates()
return e
}
func SetupRoutes(e *echo.Echo, ctrl *controller.Controller, env *env.Config) {
favicon := e.Group("/favicon", longCacheLifetime)
favicon.Static("/", "web/favicon")
fonts := e.Group("/fonts", longCacheLifetime)
fonts.Static("/", "web/fonts")
img := e.Group("/img", longCacheLifetime)
img.Static("/", "web/img")
e.Static("/css", "web/css")
e.Static("/js", "web/js")
api := e.Group("/api")
{
tableGroup := api.Group("/tables")
{
tableGroup.GET("", ctrl.GetTables)
tableGroup.POST("", ctrl.CreateTable)
tableGroup.DELETE("", ctrl.DeleteTable)
}
orderGroup := api.Group("/orders")
{
orderGroup.GET("", ctrl.GetOrders)
orderGroup.POST("", ctrl.CreateOrder)
orderGroup.DELETE("", ctrl.DeleteOrder)
orderGroup.PUT("", ctrl.UpdateOrder)
orderGroup.GET("/sse", echo.WrapHandler(http.HandlerFunc(ctrl.SSE.ServeHTTP)))
orderItemGroup := orderGroup.Group("/items")
{
orderItemGroup.GET("", ctrl.GetOrderItems)
orderItemGroup.POST("", ctrl.CreateOrderItem)
orderItemGroup.PUT("", ctrl.UpdateOrderItem)
orderItemGroup.DELETE("/:id", ctrl.DeleteOrderItem)
}
}
billGroup := api.Group("/bills")
{
billGroup.GET("", ctrl.GetBills)
billGroup.POST("", ctrl.CreateBill)
billGroup.DELETE("/:id", ctrl.DeleteBill)
billItemGroup := billGroup.Group("/items")
{
billItemGroup.GET("", ctrl.GetBillItems)
}
}
userGroup := api.Group("/users")
{
userGroup.GET("/:username", ctrl.GetUser)
userGroup.PUT("", ctrl.UpdateUser)
}
health := api.Group("/health", authHeader)
{
health.GET("", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, env.Version)
})
}
if env.SwaggerHost != "" {
docs.SwaggerInfo.Title = "Café Plätschwiesle"
docs.SwaggerInfo.Description = "This is the backend of Café Plätschwiesle"
docs.SwaggerInfo.Version = env.Version
docs.SwaggerInfo.BasePath = "/api"
parsed, _ := url.Parse(env.SwaggerHost)
docs.SwaggerInfo.Host = parsed.Host
api.GET("/swagger/*", echoSwagger.WrapHandler)
zap.L().Info("swagger running", zap.String("url", env.SwaggerHost+"/api/swagger/index.html"))
}
e.GET("/robots.txt", func(ctx echo.Context) error {
return ctx.String(http.StatusOK, "User-agent: *\nDisallow: /")
})
e.RouteNotFound("*", func(c echo.Context) error {
return c.Render(http.StatusOK, "index.html", nil)
})
}
}

View file

@ -1,22 +0,0 @@
package router
import (
"html/template"
"io"
"github.com/labstack/echo/v4"
)
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func initTemplates() *Template {
return &Template{
templates: template.Must(template.ParseGlob("web/*.html")),
}
}

View file

@ -1,17 +0,0 @@
package router
import (
"github.com/go-playground/validator/v10"
)
type CustomValidator struct {
Validator *validator.Validate
}
func (cv *CustomValidator) Validate(i interface{}) error {
return cv.Validator.Struct(i)
}
func newValidator() *validator.Validate {
return validator.New()
}

39
main.go Normal file
View file

@ -0,0 +1,39 @@
package main
import (
"cafe/api"
"cafe/config"
"cafe/service"
"cafe/user"
"fmt"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
func main() {
gin.SetMode(gin.ReleaseMode)
config.Cafe.Database.Initialize(config.StorageDir)
config.Cafe.Database.MigrateHelper(service.Table{}, "table")
config.Cafe.Database.MigrateHelper(service.Order{}, "order")
config.Cafe.Database.MigrateHelper(service.OrderItem{}, "orderItem")
config.Cafe.Database.MigrateHelper(service.Bill{}, "bill")
config.Cafe.Database.MigrateHelper(service.BillItem{}, "billItem")
config.Cafe.Database.MigrateHelper(user.User{}, "user")
a := api.Api{}
service.Initialize()
a.Hub.Initialize()
a.Router = gin.New()
a.SetMiddlewares()
a.HandleStaticFiles()
a.SetupSwagger()
a.SetupRouter()
logrus.WithField("port", config.Cafe.Port).Info("Server running")
err := a.Router.Run(fmt.Sprintf(":%d", config.Cafe.Port))
if err != nil {
logrus.WithField("error", err).Fatal("Cannot start server")
}
}

Some files were not shown because too many files have changed in this diff Show more