Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
7a3780f3e1 | |||
34ca6f0f7e | |||
1629523d84 | |||
be890e2e75 | |||
4a7b2d84c8 | |||
f64d5c2420 | |||
99add9dbe2 | |||
1fe4a98655 | |||
c135c604f0 | |||
f63210272d | |||
28f068c890 | |||
16b2f17301 | |||
e44e7caa11 |
4
.air.toml
Normal 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"]
|
|
@ -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
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
17
.gitlab/test.gitlab-ci.yml
Normal 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
|
78
README.md
|
@ -1,76 +1,8 @@
|
|||
# Café Plätschwiesle
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
[](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/-/commits/main)
|
||||
[](https://gitlab.unjx.de/flohoss/cafe-plaetschwiesle/-/commits/main)
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
16
api/types.go
|
@ -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"`
|
||||
}
|
30
cmd/cafe-plaetschwiesle/cafe-plaetschwiesle.go
Normal 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))
|
||||
}
|
||||
}
|
10
config.toml
|
@ -1,10 +0,0 @@
|
|||
ALLOWED_HOSTS = "https://cafe.test"
|
||||
LOG_LEVEL = "info"
|
||||
PORT = 5000
|
||||
SWAGGER = true
|
||||
|
||||
[MYSQL]
|
||||
DATABASE = ""
|
||||
PASSWORD = ""
|
||||
URL = ""
|
||||
USER = ""
|
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
||||
)
|
||||
|
|
55
hub/hub.go
|
@ -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
|
@ -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
|
||||
}
|
34
internal/controller/controller.go
Normal 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
|
||||
}
|
32
internal/controller/events.go
Normal 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})
|
||||
}
|
167
internal/controller/order.go
Normal 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
|
||||
}
|
104
internal/controller/routesBill.go
Normal 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)
|
||||
}
|
208
internal/controller/routesOrder.go
Normal 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)
|
||||
}
|
51
internal/controller/routesTable.go
Normal 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)
|
||||
}
|
54
internal/controller/routesUser.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
40
internal/controller/user.go
Normal 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
|
||||
}
|
81
internal/database/database.go
Normal 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
|
@ -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
|
@ -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")
|
||||
}
|
44
internal/logging/logger_test.go
Normal 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")
|
||||
}
|
33
internal/logging/logging.go
Normal 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())
|
||||
}
|
20
internal/router/middleware.go
Normal 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
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
22
internal/router/templates.go
Normal 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")),
|
||||
}
|
||||
}
|
17
internal/router/validate.go
Normal 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
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
124
service/bill.go
|
@ -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
|
||||
}
|
169
service/order.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
42
user/user.go
|
@ -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
|
||||
}
|
0
frontend/.gitignore → web/.gitignore
vendored
|
@ -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/).
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
@ -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(() => {
|
|
@ -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?",
|
|
@ -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(() => {
|
|
@ -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)
|
|
@ -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"],
|
|
@ -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"));
|
||||
}
|
|
@ -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
|
|
@ -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>
|
|
@ -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,
|
|
@ -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 },
|
||||
],
|
||||
},
|
||||
]);
|
|
@ -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;
|
|
@ -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(() => {
|
|
@ -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: {
|
|
@ -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);
|
||||
},
|
||||
},
|
|
@ -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);
|
||||
},
|
||||
},
|
|
@ -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 };
|