Compare commits

...

13 commits
v0.1.2 ... main

111 changed files with 2258 additions and 1752 deletions

4
.air.toml Normal file
View file

@ -0,0 +1,4 @@
[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,9 +1,17 @@
stages:
- test
- build
- deploy
include:
- local: .gitlab/_common.gitlab-ci.yml
- local: .gitlab/_rules.gitlab-ci.yml
- local: /.gitlab/test.gitlab-ci.yml
- local: /.gitlab/build.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,3 +12,13 @@ image: docker:$DOCKER_VERSION-git
.login_registry:
before_script:
- 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 NODE_VERSION=$NODE_VERSION
--build-arg ALPINE_VERSION=$ALPINE_VERSION
--build-arg APP_VERSION=$CI_COMMIT_TAG
--build-arg VERSION=$CI_COMMIT_TAG
--build-arg BUILD_TIME=$CI_JOB_STARTED_AT
--tag $CURRENT_IMAGE
--tag $LATEST_IMAGE

View file

@ -0,0 +1,17 @@
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,76 +1,8 @@
# Café Plätschwiesle
![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?style=for-the-badge&logo=typescript&color=3178C6)
![Alt go](https://img.shields.io/badge/Language-Go-informational?style=for-the-badge&logo=go&color=00ADD8)
![Alt vue](https://img.shields.io/badge/Framework-Vue3-informational?logo=vuedotjs&color=4FC08D)
![Alt typescript](https://img.shields.io/badge/Language-Typescript-informational?logo=typescript&color=3178C6)
![Alt go](https://img.shields.io/badge/Language-Go-informational?logo=go&color=00ADD8)
## docker-compose example
```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
```
[![pipeline status](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/badges/main/pipeline.svg)](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/-/commits/main)
[![coverage report](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/badges/main/coverage.svg)](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/-/commits/main)

View file

@ -1,52 +0,0 @@
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")
}

View file

@ -1,62 +0,0 @@
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")
}

View file

@ -1,112 +0,0 @@
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)
}

View file

@ -1,258 +0,0 @@
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)
}
}

View file

@ -1,54 +0,0 @@
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)
}
}

View file

@ -1,60 +0,0 @@
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)
}
}

View file

@ -1,26 +0,0 @@
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
})
}

View file

@ -1,31 +0,0 @@
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")
}
}

View file

@ -1,16 +0,0 @@
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

@ -0,0 +1,30 @@
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))
}
}

View file

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

View file

@ -1,72 +0,0 @@
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()
}

View file

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

View file

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

View file

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

61
go.mod
View file

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

780
go.sum

File diff suppressed because it is too large Load diff

View file

@ -1,55 +0,0 @@
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)
}
}
}
}
}

122
internal/controller/bill.go Normal file
View file

@ -0,0 +1,122 @@
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

@ -0,0 +1,34 @@
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,167 @@
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

@ -0,0 +1,104 @@
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

@ -0,0 +1,208 @@
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

@ -0,0 +1,51 @@
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

@ -0,0 +1,54 @@
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,8 +1,6 @@
package service
package controller
import (
"cafe/config"
"cafe/types"
"fmt"
"gorm.io/plugin/soft_delete"
@ -16,9 +14,9 @@ type Table struct {
IsDeleted soft_delete.DeletedAt `gorm:"softDelete:flag" json:"is_deleted" swaggerignore:"true"`
}
func GetAllTables() []Table {
func (c *Controller) GetAllTables() []Table {
var tables []Table
config.Cafe.Database.ORM.Model(
c.orm.Model(
&Table{},
).Joins(
"left join orders on tables.id = orders.table_id",
@ -32,25 +30,25 @@ func GetAllTables() []Table {
return tables
}
func CreateNewTable() (Table, error) {
func (c *Controller) CreateNewTable() (Table, error) {
var table Table
var err error
result := config.Cafe.Database.ORM.Unscoped().Where("is_deleted = ?", 1).Limit(1).Find(&table)
result := c.orm.Unscoped().Where("is_deleted = ?", 1).Limit(1).Find(&table)
if result.RowsAffected == 0 {
err = config.Cafe.Database.ORM.Create(&table).Error
err = c.orm.Create(&table).Error
} else {
table.IsDeleted = 0
err = config.Cafe.Database.ORM.Unscoped().Save(&table).Error
err = c.orm.Unscoped().Save(&table).Error
}
if err != nil {
return table, fmt.Errorf(types.CannotCreate.String())
return table, fmt.Errorf(CannotCreate.String())
}
return table, nil
}
func DeleteLatestTable() error {
func (c *Controller) DeleteLatestTable() error {
var table Table
err := config.Cafe.Database.ORM.Model(
err := c.orm.Model(
&Table{},
).Joins(
"left join orders on tables.id = orders.table_id",
@ -62,14 +60,14 @@ func DeleteLatestTable() error {
"tables.id",
).Last(&table).Error
if err != nil {
return fmt.Errorf(types.CannotFind.String())
return fmt.Errorf(CannotFind.String())
}
if table.OrderCount != 0 {
return fmt.Errorf(types.StillInUse.String())
return fmt.Errorf(StillInUse.String())
}
err = config.Cafe.Database.ORM.Delete(&table).Error
err = c.orm.Delete(&table).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
return fmt.Errorf(CannotDelete.String())
}
return nil
}

View file

@ -1,21 +1,8 @@
package types
package controller
type (
ErrorResponses uint
ItemType uint
NotifierType uint
)
const (
Create NotifierType = iota
Delete
DeleteAll
)
const (
Food ItemType = iota
ColdDrink
HotDrink
)
const (
@ -28,17 +15,6 @@ const (
CannotParse
)
func ParseItemType(itemType string) ItemType {
switch itemType {
case "0":
return Food
case "1":
return ColdDrink
default:
return HotDrink
}
}
func (e ErrorResponses) String() string {
switch e {
case MissingInformation:
@ -57,3 +33,20 @@ func (e ErrorResponses) String() string {
return "kann nicht verarbeitet werden"
}
}
const (
Food ItemType = iota
ColdDrink
HotDrink
)
func ParseItemType(itemType string) ItemType {
switch itemType {
case "0":
return Food
case "1":
return ColdDrink
default:
return HotDrink
}
}

View file

@ -0,0 +1,40 @@
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

@ -0,0 +1,81 @@
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 Normal file
View file

@ -0,0 +1,70 @@
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)
}

56
internal/env/env_test.go vendored Normal file
View file

@ -0,0 +1,56 @@
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

@ -0,0 +1,44 @@
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

@ -0,0 +1,33 @@
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

@ -0,0 +1,20 @@
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)
}
}

112
internal/router/router.go Normal file
View file

@ -0,0 +1,112 @@
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

@ -0,0 +1,22 @@
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

@ -0,0 +1,17 @@
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
View file

@ -1,39 +0,0 @@
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")
}
}

View file

@ -27,8 +27,8 @@ if [ -n "$PUID" ] || [ -n "$PGID" ]; then
chown "$USER":"$USER" "$HOME" -R
printf "UID: %s GID: %s\n\n" "$PUID" "$PGID"
exec su -c - $USER ./cafe
exec su -c - $USER ./cafe-plaetschwiesle
else
printf "WARNING: Running docker as root\n\n"
exec ./cafe
exec ./cafe-plaetschwiesle
fi

View file

@ -7,7 +7,10 @@ case $action in
go install github.com/swaggo/swag/cmd/swag@latest
;;
"init")
swag init -g api/swagger.go
swag init --dir internal/controller -g ../router/router.go
;;
"format")
swag fmt
;;
*)
exit 0

View file

@ -1,124 +0,0 @@
package service
import (
"cafe/config"
"cafe/types"
"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 types.ItemType `json:"item_type" validate:"required"`
}
)
func DoesBillExist(id string) (Bill, error) {
var bill Bill
result := config.Cafe.Database.ORM.Limit(1).Find(&bill, id)
if result.RowsAffected == 0 {
return bill, fmt.Errorf(types.CannotFind.String())
}
return bill, nil
}
func GetAllBillItems(billId uint64) ([]BillItem, error) {
var billItems []BillItem
result := config.Cafe.Database.ORM.Where("bill_id = ?", billId).Find(&billItems)
if result.RowsAffected == 0 {
return billItems, fmt.Errorf(types.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 " + types.CannotParse.String())
}
monthI, monthErr := strconv.Atoi(month)
if monthErr != nil {
return time.Time{}, fmt.Errorf("monat " + types.CannotParse.String())
}
dayI, dayErr := strconv.Atoi(day)
if dayErr != nil {
return time.Time{}, fmt.Errorf("tag " + types.CannotParse.String())
}
loc, locErr := time.LoadLocation("Local")
if locErr != nil {
return time.Time{}, fmt.Errorf("timezone " + types.CannotParse.String())
}
return time.Date(yearI, time.Month(monthI), dayI, 0, 0, 0, 0, loc), nil
}
func 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()
config.Cafe.Database.ORM.Where("created_at BETWEEN ? AND ?", beginningOfDay, endOfDay).Order("created_at").Find(&bills)
return bills, nil
}
func CreateBill(options GetOrderOptions) (Bill, error) {
orders := GetAllOrdersForTable(options)
var bill Bill
var total float32 = 0
for _, order := range orders {
total += order.Total
}
bill.TableID = options.TableId
bill.Total = total
err := config.Cafe.Database.ORM.Create(&bill).Error
if err != nil {
return bill, fmt.Errorf(types.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,
}
config.Cafe.Database.ORM.Create(&billItem)
}
ordersToDelete := GetAllOrdersForTable(GetOrderOptions{TableId: options.TableId, Grouped: false, Filter: options.Filter})
err = config.Cafe.Database.ORM.Delete(&ordersToDelete).Error
if err != nil {
return bill, fmt.Errorf(types.CannotDelete.String())
}
LiveCh <- WebSocketMsg{
Type: types.DeleteAll,
Payload: ordersToDelete,
}
return bill, nil
}
func DeleteBill(bill *Bill) error {
err := config.Cafe.Database.ORM.Delete(bill).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
billItemsToDelete, _ := GetAllBillItems(bill.ID)
config.Cafe.Database.ORM.Delete(&billItemsToDelete)
return nil
}

View file

@ -1,169 +0,0 @@
package service
import (
"cafe/config"
"cafe/types"
"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 types.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 DoesOrderItemExist(id string) (OrderItem, error) {
var orderItem OrderItem
result := config.Cafe.Database.ORM.Limit(1).Find(&orderItem, id)
if result.RowsAffected == 0 {
return orderItem, fmt.Errorf(types.CannotFind.String())
}
return orderItem, nil
}
func DoesOrderExist(id string) (Order, error) {
var order Order
result := config.Cafe.Database.ORM.Limit(1).Find(&order, id)
if result.RowsAffected == 0 {
return order, fmt.Errorf(types.CannotFind.String())
}
return order, nil
}
func GetAllActiveOrders() []Order {
var orders []Order
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Where("is_served = ?", 0).Order("updated_at").Find(&orders)
return orders
}
func GetAllOrdersForTable(options GetOrderOptions) []Order {
var orders []Order
if options.Grouped {
if len(options.Filter) == 0 {
config.Cafe.Database.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 {
config.Cafe.Database.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 {
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").Where("table_id = ?", options.TableId).Order("item_type, description").Find(&orders)
} else {
config.Cafe.Database.ORM.Model(&Order{}).Find(&orders, options.Filter).Where("table_id = ?", options.TableId).Find(&orders)
}
}
return orders
}
func CreateOrder(order *Order) error {
err := config.Cafe.Database.ORM.Create(order).Error
if err != nil {
return fmt.Errorf(types.CannotCreate.String())
}
config.Cafe.Database.ORM.Model(&Order{}).Joins("OrderItem").First(order)
LiveCh <- WebSocketMsg{
Type: types.Create,
Payload: []Order{*order},
}
return nil
}
func UpdateOrder(old *Order, new *Order) error {
err := config.Cafe.Database.ORM.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
if new.IsServed {
LiveCh <- WebSocketMsg{
Type: types.Delete,
Payload: []Order{*new},
}
}
return nil
}
func DeleteOrder(tableId string, orderItemId string) error {
var order Order
err := config.Cafe.Database.ORM.Where("table_id = ? AND order_item_id = ?", tableId, orderItemId).Last(&order).Error
if err != nil {
return fmt.Errorf(types.CannotFind.String())
}
err = config.Cafe.Database.ORM.Delete(&order).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
LiveCh <- WebSocketMsg{
Type: types.Delete,
Payload: []Order{order},
}
return nil
}
func GetOrderItemsForType(itemType string) []OrderItem {
var orderItems []OrderItem
config.Cafe.Database.ORM.Order("description").Where("item_type = ?", types.ParseItemType(itemType)).Find(&orderItems)
return orderItems
}
func CreateOrderItem(oderItem *OrderItem) error {
err := config.Cafe.Database.ORM.Create(oderItem).Error
if err != nil {
return fmt.Errorf(types.CannotCreate.String())
}
return nil
}
func UpdateOrderItem(old *OrderItem, new *OrderItem) error {
err := config.Cafe.Database.ORM.First(old).Updates(new).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
return nil
}
func DeleteOrderItem(oderItem *OrderItem) error {
err := config.Cafe.Database.ORM.Delete(oderItem).Error
if err != nil {
return fmt.Errorf(types.CannotDelete.String())
}
return nil
}

View file

@ -1,18 +0,0 @@
package service
import (
"cafe/types"
)
type (
WebSocketMsg struct {
Type types.NotifierType `json:"type"`
Payload []Order `json:"payload"`
}
)
var LiveCh chan WebSocketMsg
func Initialize() {
LiveCh = make(chan WebSocketMsg)
}

View file

@ -1,12 +0,0 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test</title>
</head>
<body>
<div>Working</div>
</body>
</html>

View file

@ -1,23 +0,0 @@
package types
import (
"testing"
"github.com/go-playground/assert/v2"
)
func TestErrorResponses_String(t *testing.T) {
assert.Equal(t, MissingInformation.String(), "fehlende Informationen")
assert.Equal(t, CannotCreate.String(), "kann nicht gespeichert werden")
assert.Equal(t, CannotUpdate.String(), "kann nicht geändert werden")
assert.Equal(t, CannotDelete.String(), "kann nicht gelöscht werden")
assert.Equal(t, CannotFind.String(), "kann nicht gefunden werden")
assert.Equal(t, StillInUse.String(), "noch in Verwendung")
assert.Equal(t, CannotParse.String(), "kann nicht verarbeitet werden")
}
func TestParseItemType(t *testing.T) {
assert.Equal(t, ParseItemType("0"), Food)
assert.Equal(t, ParseItemType("1"), ColdDrink)
assert.Equal(t, ParseItemType("2"), HotDrink)
}

View file

@ -1,42 +0,0 @@
package user
import (
"cafe/config"
"cafe/types"
"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 DoesUserExist(username string) (User, error) {
var user User
result := config.Cafe.Database.ORM.Limit(1).Find(&user, "username = ?", username)
if result.RowsAffected == 0 {
return user, fmt.Errorf(types.CannotFind.String())
}
return user, nil
}
func GetUserOrCreate(username string) (User, error) {
var user User
err := config.Cafe.Database.ORM.Where(User{Username: username}).Attrs(User{ShowHotDrinks: true, ShowColdDrinks: true}).FirstOrCreate(&user).Error
if err != nil {
return user, fmt.Errorf(types.CannotCreate.String())
}
return user, nil
}
func UpdateUser(old *User, new *User) error {
err := config.Cafe.Database.ORM.First(old).Updates(map[string]interface{}{
"Username": new.Username,
"ShowColdDrinks": new.ShowColdDrinks,
"ShowHotDrinks": new.ShowHotDrinks}).Error
if err != nil {
return fmt.Errorf(types.CannotUpdate.String())
}
return nil
}

View file

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

View file

@ -1,5 +1,5 @@
{
"name": "frontend",
"name": "web",
"version": "0.1.0",
"private": true,
"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">
import { computed, defineComponent, onMounted, PropType, ref } from "vue";
import { BillsService, service_Bill, service_BillItem } from "@/services/openapi";
import { BillsService, controller_Bill, controller_BillItem } from "@/services/openapi";
import { convertToEur } from "@/utils";
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
import moment from "moment";
@ -39,10 +39,10 @@ import moment from "moment";
export default defineComponent({
name: "BillModal",
components: { WaveSpinner },
props: { bill: { type: Object as PropType<service_Bill>, required: true } },
props: { bill: { type: Object as PropType<controller_Bill>, required: true } },
setup(props) {
const isLoading = ref(true);
const billItems = ref<service_BillItem[]>();
const billItems = ref<controller_BillItem[]>();
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");
onMounted(() => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ import Menubar from "primevue/menubar";
import { useStore } from "vuex";
import Button from "primevue/button";
import { useRoute } from "vue-router";
import { TablesService, types_ItemType } from "@/services/openapi";
import { TablesService, controller_ItemType } from "@/services/openapi";
import { detailedItemTypeString, errorToast } from "@/utils";
import { useToast } from "primevue/usetoast";
import { visible } from "@/keys";
@ -84,9 +84,9 @@ export default defineComponent({
label: "Artikel",
icon: "pi pi-fw pi-shopping-cart",
items: [
{ label: detailedItemTypeString(types_ItemType.Food), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.Food },
{ label: detailedItemTypeString(types_ItemType.ColdDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.ColdDrink },
{ label: detailedItemTypeString(types_ItemType.HotDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + types_ItemType.HotDrink },
{ label: detailedItemTypeString(controller_ItemType.Food), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.Food },
{ label: detailedItemTypeString(controller_ItemType.ColdDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.ColdDrink },
{ label: detailedItemTypeString(controller_ItemType.HotDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.HotDrink },
],
visible: () => editor.value,
},
@ -103,6 +103,7 @@ export default defineComponent({
visible: () => maker.value,
},
{ 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 Sidebar from "primevue/sidebar";
import { visible } from "@/keys";
import { user_User, UsersService } from "@/services/openapi";
import { controller_User, UsersService } from "@/services/openapi";
import InputSwitch from "primevue/inputswitch";
import { useStore } from "vuex";
import { errorToast } from "@/utils";
@ -30,7 +30,7 @@ export default defineComponent({
const toast = useToast();
const isLoading = ref(false);
const isVisible = inject(visible, ref(false));
const user = computed<user_User>(() => store.getters.getUser);
const user = computed<controller_User>(() => store.getters.getUser);
function updateUser() {
isLoading.value = true;

View file

@ -1,4 +1,4 @@
import { createApp } from "vue";
import { createApp, version } from "vue";
import { OpenAPI, UsersService } from "@/services/openapi";
import App from "./App.vue";
import router from "./router";
@ -18,7 +18,6 @@ import "primeicons/primeicons.css";
import "primeflex/primeflex.css";
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;
async function getHealth() {
@ -27,6 +26,7 @@ async function getHealth() {
store.commit("setGroups", groups);
const user = await UsersService.getUsers(response.headers.get("remote-name") || "Benutzer");
store.commit("setUser", user);
store.commit("setVersion", await response.text());
}
getHealth().then(() => {

View file

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

View file

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

View file

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

View file

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

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