first commit
This commit is contained in:
35
go.mod
Normal file
35
go.mod
Normal file
@@ -0,0 +1,35 @@
|
||||
module ais
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sessions v0.0.5
|
||||
github.com/gin-gonic/gin v1.8.1
|
||||
github.com/lib/pq v1.10.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/brianvoe/gofakeit/v6 v6.19.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.10.0 // indirect
|
||||
github.com/goccy/go-json v0.9.7 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/stretchr/testify v1.7.5 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
102
go.sum
Normal file
102
go.sum
Normal file
@@ -0,0 +1,102 @@
|
||||
github.com/brianvoe/gofakeit/v6 v6.19.0 h1:g+yJ+meWVEsAmR+bV4mNM/eXI0N+0pZ3D+Mi+G5+YQo=
|
||||
github.com/brianvoe/gofakeit/v6 v6.19.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
||||
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh+BF8dHX5nt/dr0=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
|
||||
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
13
main.go
Normal file
13
main.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ais/pkg/api"
|
||||
|
||||
)
|
||||
|
||||
|
||||
func main() {
|
||||
router := api.NewAPI()
|
||||
|
||||
router.Run(":8888")
|
||||
}
|
||||
100
pkg/api/api.go
Normal file
100
pkg/api/api.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
db, err = sql.Open("postgres", os.Getenv("POSTGRES"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("Successfully connected to postgres database")
|
||||
|
||||
// install tables
|
||||
if len(os.Args) >= 2 {
|
||||
if os.Args[1] == "install" {
|
||||
install()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// drop tables
|
||||
if os.Args[1] == "drop" {
|
||||
drop()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// reinstall
|
||||
if os.Args[1] == "reinstall" {
|
||||
drop()
|
||||
install()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// fake data
|
||||
if os.Args[1] == "fake" {
|
||||
fakeData()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// unknown Args
|
||||
log.Println("Unknown args", os.Args)
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func NewAPI() *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
// session
|
||||
store := cookie.NewStore([]byte("Miku saves the world!"))
|
||||
router.Use(sessions.Sessions("ais", store))
|
||||
|
||||
// entry point html
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.File("./web/public/index.html")
|
||||
})
|
||||
|
||||
group := router.Group("/api")
|
||||
|
||||
// json error middleware
|
||||
group.Use(func(c *gin.Context) {
|
||||
c.Next()
|
||||
if len(c.Errors) > 0 {
|
||||
c.JSON(-1, gin.H{
|
||||
"errors": c.Errors.Errors(),
|
||||
"note": "gin api error handler",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
group.POST("/login", handelLogin)
|
||||
group.GET("/login", handelGetLoginSession)
|
||||
group.POST("/register", handelRegister)
|
||||
group.POST("/logout", handelLogout)
|
||||
|
||||
customer := group.Group("/customer")
|
||||
customer.GET("/market", handleGetMarket)
|
||||
customer.GET("/market/distance", handleGetMarketByDistance)
|
||||
customer.GET("/market/:marketid", handleGetGoods)
|
||||
customer.POST("/market/:marketid/:goodsid", handleBuy)
|
||||
customer.GET("/purchase", handleGetPurchaseHistory)
|
||||
customer.DELETE("/purchase/:purchaseid", handleDeletePurchaseHistory)
|
||||
customer.GET("/purchase/report", handleCustomerReport)
|
||||
|
||||
supplier := group.Group("/supplier")
|
||||
supplier.GET("/goods", handleGetGodsBySupplier)
|
||||
supplier.POST("/goods", handleCreateGoods)
|
||||
|
||||
return router
|
||||
}
|
||||
38
pkg/api/drop.go
Normal file
38
pkg/api/drop.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var dropSQLString = `
|
||||
drop table purchase;
|
||||
|
||||
drop table tags_on_goods;
|
||||
|
||||
drop table goods;
|
||||
|
||||
drop table tag;
|
||||
|
||||
drop table market;
|
||||
|
||||
drop table users;
|
||||
`
|
||||
|
||||
func drop() {
|
||||
sqls := strings.Split(dropSQLString, "\n\n")
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, sql := range sqls {
|
||||
log.Println("Dropting table with SQL", sql)
|
||||
_, err = tx.Exec(sql)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
log.Println("Successfully drop all tables")
|
||||
}
|
||||
22
pkg/api/encrypt.go
Normal file
22
pkg/api/encrypt.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func EncryptPassword(password string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
log.Println("Warning: Failed to hash password, fallback to plaintext password")
|
||||
return password
|
||||
}
|
||||
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
func ComparePassword(hashedPassword string, plainTextPassword string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainTextPassword))
|
||||
return err
|
||||
}
|
||||
88
pkg/api/fake.go
Normal file
88
pkg/api/fake.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
)
|
||||
|
||||
func fakeData() {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fakeUsers(tx)
|
||||
fakeSupplier(tx)
|
||||
fakeMarket(tx)
|
||||
tx.Commit()
|
||||
}
|
||||
|
||||
func fakeUsers(tx *sql.Tx) {
|
||||
for i := 0; i < 10; i++ {
|
||||
username := gofakeit.Username()
|
||||
password := gofakeit.Password(true, true, true, true, true, 1)
|
||||
encryptedPassword := EncryptPassword(password)
|
||||
_, err := tx.Exec(`insert into users (username, password) values ($1, $2)`,
|
||||
username, encryptedPassword)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("fake users", username, password)
|
||||
}
|
||||
}
|
||||
|
||||
func fakeSupplier(tx *sql.Tx) {
|
||||
for i := 0; i < 10; i++ {
|
||||
username := gofakeit.Username()
|
||||
password := EncryptPassword(gofakeit.Password(true, true, true, true, true, 1))
|
||||
_, err := tx.Exec(`insert into users (username, password, role) values ($1, $2, 2)`,
|
||||
username, password)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("fake supplier", username, password)
|
||||
}
|
||||
}
|
||||
|
||||
func fakeMarket(tx *sql.Tx) {
|
||||
for i := 0; i < 10; i++ {
|
||||
addr := gofakeit.Address()
|
||||
name := addr.State
|
||||
description := addr.Address
|
||||
location := fmt.Sprintf("(%f, %f)", addr.Latitude, addr.Longitude)
|
||||
|
||||
row := tx.QueryRow(`insert into market (name, description, location) values ($1, $2, $3) returning id`,
|
||||
name, description, location)
|
||||
|
||||
var mid int64
|
||||
err := row.Scan(&mid)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("fake market", name, description, location)
|
||||
|
||||
fakeProduct(tx, int64(mid))
|
||||
}
|
||||
}
|
||||
|
||||
func fakeProduct(tx *sql.Tx, mid int64) {
|
||||
for i := 0; i < 10; i++ {
|
||||
name := gofakeit.BeerName()
|
||||
description := gofakeit.BeerStyle()
|
||||
quantity := rand.Intn(390)
|
||||
price := gofakeit.Price(39, 390)
|
||||
_, err := tx.Exec(`insert into goods (name, description, supplier_id, market_id, quantity, price) values ($1, $2, $3, $4, $5, $6)`,
|
||||
name, description, 1, mid, quantity, price)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Println("fake goods", name, description, quantity, price)
|
||||
}
|
||||
}
|
||||
1
pkg/api/handle_buy.go
Normal file
1
pkg/api/handle_buy.go
Normal file
@@ -0,0 +1 @@
|
||||
package api
|
||||
169
pkg/api/handle_goods.go
Normal file
169
pkg/api/handle_goods.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Goods struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
SupplierId int64 `json:"supplier_id"`
|
||||
MarketId int64 `json:"market_id"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
Price string `json:"price"`
|
||||
}
|
||||
|
||||
func handleGetGoods(c *gin.Context) {
|
||||
marketId, err := strconv.ParseInt(c.Param("marketid"), 10, 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
log.Println("select", marketId)
|
||||
rows, err := db.Query(`select id, name, description, quantity, price from goods where market_id = $1 order by name, description`, marketId)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
ret := make([]Goods, 0)
|
||||
var g Goods
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&g.Id, &g.Name, &g.Description, &g.Quantity, &g.Price)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
ret = append(ret, g)
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"goods": ret,
|
||||
})
|
||||
}
|
||||
|
||||
func handleBuy(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userId := session.Get("userid").(int64)
|
||||
goodsId := c.Param("goodsid")
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
_, err = tx.Exec(`insert into purchase (user_id, goods_id) values ($1, $2)`, userId, goodsId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
_, err = tx.Exec(`update goods set quantity = quantity - 1 where id = $1`, goodsId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
func handleGetPurchaseHistory(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userId := session.Get("userid").(int64)
|
||||
|
||||
rows, err := db.Query(`
|
||||
select p.id, p.quantity, p.purchased_time, g.name, g.price
|
||||
from purchase p
|
||||
join goods g on p.goods_id = g.id
|
||||
where p.user_id = $1
|
||||
order by p.purchased_time desc
|
||||
`, userId)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
type Ret struct {
|
||||
Id int64 `json:"id"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
PurchasedTime time.Time `json:"purchased_time"`
|
||||
Goods Goods `json:"goods"`
|
||||
}
|
||||
|
||||
ret := make([]Ret, 0)
|
||||
for rows.Next() {
|
||||
var i Ret
|
||||
err = rows.Scan(&i.Id, &i.Quantity, &i.PurchasedTime, &i.Goods.Name, &i.Goods.Price)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
ret = append(ret, i)
|
||||
}
|
||||
|
||||
c.JSON(200, ret)
|
||||
}
|
||||
|
||||
func handleDeletePurchaseHistory(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("purchaseid"), 10, 64)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = db.Exec(`delete from purchase where id = $1`, id)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
func handleGetGodsBySupplier(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userId := session.Get("userid").(int64)
|
||||
rows, err := db.Query(`select id, name, description, quantity, price from goods where supplier_id = $1 order by name, description`, userId)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
ret := make([]Goods, 0)
|
||||
for rows.Next() {
|
||||
var g Goods
|
||||
err = rows.Scan(&g.Id, &g.Name, &g.Description, &g.Quantity, &g.Price)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
ret = append(ret, g)
|
||||
}
|
||||
|
||||
c.JSON(200, ret)
|
||||
}
|
||||
|
||||
func handleCreateGoods(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userId := session.Get("userid").(int64)
|
||||
g := &Goods{}
|
||||
err := c.BindJSON(g)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
g.SupplierId = userId
|
||||
_, err = db.Exec(`insert into goods (name, description, supplier_id, market_id, quantity, price) values ($1, $2, $3, $4, $5, $6)`,
|
||||
g.Name, g.Description, g.SupplierId, g.MarketId, g.Quantity,g.Price)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
60
pkg/api/handle_market.go
Normal file
60
pkg/api/handle_market.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package api
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
type Market struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Location string `json:"location"`
|
||||
}
|
||||
|
||||
func handleGetMarket(c *gin.Context) {
|
||||
markets := make([]Market, 0)
|
||||
|
||||
rows, err := db.Query(`select id, name, description, location from market`)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var market Market
|
||||
err = rows.Scan(&market.Id, &market.Name, &market.Description, &market.Location)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
markets = append(markets, market)
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"markets": markets,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetMarketByDistance(c *gin.Context) {
|
||||
markets := make([]Market, 0)
|
||||
point := c.Query("point")
|
||||
|
||||
rows, err := db.Query(`select id, name, description, location, location<->$1 as dist from market order by dist`, point)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var x string
|
||||
for rows.Next() {
|
||||
var market Market
|
||||
err = rows.Scan(&market.Id, &market.Name, &market.Description, &market.Location, &x)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
markets = append(markets, market)
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"markets": markets,
|
||||
})
|
||||
}
|
||||
28
pkg/api/handle_report.go
Normal file
28
pkg/api/handle_report.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func handleCustomerReport(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userId := session.Get("userid").(int64)
|
||||
|
||||
row := db.QueryRow(`select sum(g.price)
|
||||
from purchase p
|
||||
join goods g on p.goods_id = g.id
|
||||
where p.user_id = $1`,
|
||||
userId)
|
||||
|
||||
var sumPrice string
|
||||
err := row.Scan(&sumPrice)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"sum": sumPrice,
|
||||
})
|
||||
}
|
||||
100
pkg/api/handle_user.go
Normal file
100
pkg/api/handle_user.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Balance string `json:"balance"`
|
||||
Location string `json:"location"`
|
||||
Role int64 `json:"role"`
|
||||
RegisterTime time.Time `json:"register_time"`
|
||||
}
|
||||
|
||||
var SESSION_NAME = "ais"
|
||||
|
||||
func handelLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
c.JSON(200, gin.H{})
|
||||
}
|
||||
|
||||
func handelGetLoginSession(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
userId := session.Get("userid")
|
||||
if userId == nil {
|
||||
c.JSON(200, gin.H{})
|
||||
return
|
||||
}
|
||||
user := &User{}
|
||||
row := db.QueryRow(`select id, username, balance, location, role from users where id=$1`, userId)
|
||||
err := row.Scan(&user.Id, &user.Username, &user.Balance, &user.Location, &user.Role)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
c.JSON(200, user)
|
||||
}
|
||||
|
||||
func handelLogin(c *gin.Context) {
|
||||
user := &User{}
|
||||
err := c.BindJSON(user)
|
||||
if err != nil {
|
||||
c.AbortWithError(500, err)
|
||||
return
|
||||
}
|
||||
|
||||
var encryptedPassowrd string
|
||||
row := db.QueryRow(`select id, username, balance, location, role, password from users where username=$1`,
|
||||
user.Username)
|
||||
err = row.Scan(&user.Id, &user.Username, &user.Balance, &user.Location, &user.Role, &encryptedPassowrd)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
|
||||
// validate password
|
||||
err = ComparePassword(encryptedPassowrd, user.Password)
|
||||
if err != nil {
|
||||
c.AbortWithError(403, err)
|
||||
return
|
||||
}
|
||||
|
||||
// set session
|
||||
session := sessions.Default(c)
|
||||
session.Set("userid", user.Id)
|
||||
session.Save()
|
||||
|
||||
c.JSON(200, user)
|
||||
|
||||
}
|
||||
|
||||
func handelRegister(c *gin.Context) {
|
||||
user := &User{}
|
||||
err := c.BindJSON(user)
|
||||
if err != nil {
|
||||
c.AbortWithError(401, err)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedPassowrd := EncryptPassword(user.Password)
|
||||
|
||||
ret := db.QueryRow(`insert into users(username, password) values ($1, $2) returning id`,
|
||||
user.Username, encryptedPassowrd)
|
||||
|
||||
err = ret.Scan(&user.Id)
|
||||
if err != nil {
|
||||
c.AbortWithError(401, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{})
|
||||
|
||||
}
|
||||
75
pkg/api/install.go
Normal file
75
pkg/api/install.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var initSQLString string = `create table users(
|
||||
id serial primary key,
|
||||
username varchar(30) not null unique,
|
||||
password varchar(64) not null,
|
||||
balance money not null default 0,
|
||||
role integer not null default 1,
|
||||
location point not null default '(0, 0)',
|
||||
registerd_time timestamp not null default now()
|
||||
);
|
||||
|
||||
create table market (
|
||||
id serial primary key,
|
||||
name varchar(100) not null,
|
||||
description text not null,
|
||||
location point not null
|
||||
)
|
||||
|
||||
create table tag (
|
||||
id serial primary key,
|
||||
name varchar(30) not null
|
||||
);
|
||||
|
||||
create table goods (
|
||||
id serial primary key,
|
||||
name varchar(100) not null,
|
||||
description text not null,
|
||||
create_time timestamp not null default now(),
|
||||
supplier_id integer not null references users(id),
|
||||
market_id integer not null references market(id),
|
||||
quantity numeric not null check (quantity >= 0),
|
||||
price money not null,
|
||||
data jsonb
|
||||
);
|
||||
|
||||
create table tags_on_goods(
|
||||
tag_id integer not null references tag(id),
|
||||
goods_id integer not null references goods(id),
|
||||
primary key (tag_id, goods_id)
|
||||
);
|
||||
|
||||
create table purchase (
|
||||
id serial primary key,
|
||||
user_id integer not null references users(id),
|
||||
goods_id integer not null references goods(id),
|
||||
quantity numeric not null default 1,
|
||||
purchased_time timestamp not null default now()
|
||||
);
|
||||
|
||||
insert into users (username, password) values ('a', 'a');
|
||||
`
|
||||
|
||||
func install() {
|
||||
sqls := strings.Split(initSQLString, "\n\n")
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, sql := range sqls {
|
||||
log.Println("Installing table with SQL", sql)
|
||||
_, err = tx.Exec(sql)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
log.Println("Successfully install all tables")
|
||||
}
|
||||
6
web/.gitignore
vendored
Normal file
6
web/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/.parcel-cache
|
||||
/public/ais.js
|
||||
/build
|
||||
|
||||
6
web/Caddyfile
Normal file
6
web/Caddyfile
Normal file
@@ -0,0 +1,6 @@
|
||||
:8001 {
|
||||
route {
|
||||
reverse_proxy /api/* 127.0.0.1:8888
|
||||
reverse_proxy * 127.0.0.1:1234
|
||||
}
|
||||
}
|
||||
5979
web/package-lock.json
generated
Normal file
5979
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
web/package.json
Normal file
24
web/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "esbuild src/index.jsx --bundle --minify --outfile=public/ais.js --loader:.webp=dataurl --analyze",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@mui/icons-material": "^5.10.16",
|
||||
"@mui/material": "^5.10.17",
|
||||
"esbuild": "^0.16.4",
|
||||
"parcel": "^2.8.1",
|
||||
"process": "^0.11.10",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.4.5"
|
||||
}
|
||||
}
|
||||
BIN
web/public/images/customer.webp
Normal file
BIN
web/public/images/customer.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
web/public/images/location.webp
Normal file
BIN
web/public/images/location.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
web/public/images/meeting.webp
Normal file
BIN
web/public/images/meeting.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
web/public/images/purchased.webp
Normal file
BIN
web/public/images/purchased.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
web/public/images/supplier.webp
Normal file
BIN
web/public/images/supplier.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
web/public/images/supplierGoods.webp
Normal file
BIN
web/public/images/supplierGoods.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
13
web/public/index.html
Normal file
13
web/public/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
<h1 style="text-align: center;">Loading Sam's AIS...</h1>
|
||||
</div>
|
||||
<script type="module" src="/ais.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
54
web/src/common.js
Normal file
54
web/src/common.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const API_ENDPOINT = "/api";
|
||||
|
||||
const get = async (url) => {
|
||||
const resp = await fetch(`${API_ENDPOINT}${url}`);
|
||||
const json = await resp.json();
|
||||
return json;
|
||||
};
|
||||
|
||||
const post = async (url, data) => {
|
||||
return await _post(url, data, "POST");
|
||||
};
|
||||
|
||||
const del = async (url, data) => {
|
||||
return await _post(url, data, "DELETE");
|
||||
};
|
||||
|
||||
const put = async (url, data) => {
|
||||
return await _post(url, data, "PUT");
|
||||
};
|
||||
|
||||
const _post = async (url, data, method) => {
|
||||
const resp = await fetch(`${API_ENDPOINT}${url}`, {
|
||||
method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const json = await resp.json();
|
||||
if (json.errors) {
|
||||
alert(json.errors);
|
||||
throw json.errors;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
const fixZero = (n) => {
|
||||
if (n < 10) return `0${n}`;
|
||||
return n;
|
||||
};
|
||||
|
||||
const time = (s) => {
|
||||
const t = new Date(s);
|
||||
const year = t.getFullYear();
|
||||
const month = fixZero(t.getMonth())
|
||||
const day = fixZero(t.getDay());
|
||||
const hours = fixZero(t.getHours());
|
||||
const minutes = fixZero(t.getMinutes());
|
||||
const second = fixZero(t.getSeconds());
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${second}`;
|
||||
};
|
||||
|
||||
export { get, post, del, put, time };
|
||||
38
web/src/components/AutoTable.jsx
Normal file
38
web/src/components/AutoTable.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
TableContainer,
|
||||
Paper,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableBody,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
|
||||
const AutoTable = ({ data }) => {
|
||||
const columns = Object.keys(data[0] || []);
|
||||
return (
|
||||
<TableContainer component={Paper} sx={{ m: 1 }}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col}>{col}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{data.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{columns.map((col) => (
|
||||
<TableCell key={col}>{row[col]}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoTable;
|
||||
22
web/src/components/ImageCard.jsx
Normal file
22
web/src/components/ImageCard.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
const ImageCard = ({ title, description, image, onClick }) => (
|
||||
<Card sx={{ m: 1 }}>
|
||||
<CardActionArea onClick={onClick}>
|
||||
<CardMedia component="img" height="239" image={image} alt="Card Image" />
|
||||
<CardContent>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
<Typography>{description}</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default ImageCard;
|
||||
36
web/src/components/LoginButton.jsx
Normal file
36
web/src/components/LoginButton.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import userContext from "../context/userContext";
|
||||
import { get } from "../common";
|
||||
|
||||
const LoginButton = () => {
|
||||
const { user, setUser } = React.useContext(userContext);
|
||||
const navigator = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (user.role === 1) {
|
||||
navigator("/customer");
|
||||
} else if (user.role === 2) {
|
||||
navigator("/supplier");
|
||||
} else {
|
||||
navigator('/login');
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
get("/login")
|
||||
.then((resp) => {
|
||||
setUser(resp);
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Button variant="contained" color="secondary" onClick={handleClick}>
|
||||
{user.username || "Login"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginButton;
|
||||
31
web/src/components/LogoutCard.jsx
Normal file
31
web/src/components/LogoutCard.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
import { Card, CardActionArea, CardContent, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
import { post } from "../common";
|
||||
import userContext from "../context/userContext";
|
||||
|
||||
const LogoutCard = () => {
|
||||
const nevigator = useNavigate();
|
||||
const { setUser } = React.useContext(userContext);
|
||||
|
||||
const handelLogout = async () => {
|
||||
await post("/logout", {});
|
||||
setUser({});
|
||||
nevigator("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<Card sx={{ m: 1 }}>
|
||||
<CardActionArea onClick={handelLogout}>
|
||||
<LogoutIcon />
|
||||
<CardContent>
|
||||
<Typography variant="h5">Logout</Typography>
|
||||
<Typography>Clear session</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogoutCard;
|
||||
23
web/src/components/SelectRole.jsx
Normal file
23
web/src/components/SelectRole.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
import { InputLabel, FormControl, Select, MenuItem } from "@mui/material";
|
||||
|
||||
const SelectRole = ({ inputRole, setInputRole }) => {
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="demo-simple-select-label">Role</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
value={inputRole}
|
||||
label="Role"
|
||||
onChange={(event) => setInputRole(event.target.value)}
|
||||
>
|
||||
<MenuItem value={1}>Customer</MenuItem>
|
||||
<MenuItem value={2}>Supplier</MenuItem>
|
||||
<MenuItem value={3}>Admin</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectRole;
|
||||
3
web/src/context/userContext.js
Normal file
3
web/src/context/userContext.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import {createContext} from 'react';
|
||||
const userContext = createContext({});
|
||||
export default userContext;
|
||||
12
web/src/index.html
Normal file
12
web/src/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root">
|
||||
</div>
|
||||
<script type="module" src="./index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
44
web/src/index.jsx
Normal file
44
web/src/index.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { createHashRouter as Router, RouterProvider } from "react-router-dom";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import HomePage from "./pages/HomePage";
|
||||
import RegisterPage from "./pages/RegisterPage";
|
||||
import CustomerPage from './pages/CustomerPage';
|
||||
import CustomerMarketPage from './pages/CustomerMarketPage';
|
||||
import userContext from "./context/userContext";
|
||||
import CustomerMarketDeatilPage from "./pages/CustomerMarketDetailPage";
|
||||
import CustomerPurchasedPage from "./pages/CustomerPurchasedPage";
|
||||
import SupplierPage from './pages/SupplierPage';
|
||||
import SupplierGoods from './pages/SupplierGoods';
|
||||
import SupplierAddGoods from './pages/SupplierAddGoods';
|
||||
|
||||
const router = Router([
|
||||
{ path: "/", element: <HomePage /> },
|
||||
{ path: "/ping", element: <div>pong</div> },
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{ path: "/register", element: <RegisterPage /> },
|
||||
{ path: "/customer", element: <CustomerPage /> },
|
||||
{ path: "/customer/market", element: <CustomerMarketPage /> },
|
||||
{ path: "/customer/market/:marketid", element: <CustomerMarketDeatilPage /> },
|
||||
{ path: "/customer/purchased", element: <CustomerPurchasedPage /> },
|
||||
{ path: "/supplier", element: <SupplierPage /> },
|
||||
{ path: "/supplier/goods", element: <SupplierGoods /> },
|
||||
{ path: "/supplier/goods/new", element: <SupplierAddGoods /> },
|
||||
]);
|
||||
|
||||
const App = () => {
|
||||
const [user, setUser] = React.useState({});
|
||||
return (
|
||||
<userContext.Provider value={{ user, setUser }}>
|
||||
<RouterProvider router={router}></RouterProvider>
|
||||
</userContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
35
web/src/layouts/BasicLayout.jsx
Normal file
35
web/src/layouts/BasicLayout.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { AppBar, Box, CssBaseline, Toolbar, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import LoginButton from "../components/LoginButton";
|
||||
|
||||
const BasicLayout = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<header>
|
||||
<AppBar position="relative">
|
||||
<Toolbar>
|
||||
<Typography variant="h6" sx={{ flexGrow: 1 }}>
|
||||
<Link style={{ textDecoration: "none", color: "unset" }} to="/">
|
||||
<img
|
||||
style={{
|
||||
height: "1.39rem",
|
||||
}}
|
||||
src="data:image/svg+xml;charset=utf-8,%3Csvg width='214' height='37' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M196.157.298l16.449 16.624c.75.758.75 1.987.001 2.745l-16.445 16.63-2.84-2.87a1.987 1.987 0 010-2.79l12.204-12.341-12.1-12.229a2.142 2.142 0 010-3.007l2.731-2.762zm-5.293 2.75l-2.719-2.75-16.362 16.546a2.075 2.075 0 000 2.911l16.367 16.542 2.716-2.747c.83-.84.83-2.2 0-3.038L178.781 18.3l12.084-12.22a2.161 2.161 0 00-.001-3.033zm-30.713 17.22c0 3.678-2.362 6.457-5.807 6.457-3.407 0-5.769-2.78-5.769-6.38 0-3.64 2.362-6.496 5.769-6.496 3.445 0 5.807 2.778 5.807 6.418zm-11.653 5.791c1.317 2.857 3.833 4.344 6.814 4.344 4.994 0 8.981-3.874 8.981-10.097 0-6.262-3.987-10.136-8.981-10.136-2.981 0-5.497 1.487-6.814 4.344V2.656l-3.871.783V30.09h3.871v-4.03zm-32.861-2.387c0 4.736 2.207 6.849 6.195 6.692.348 0 .735-.039 1.084-.117v-3.835h-1.007c-1.393 0-2.4-.861-2.4-2.74V2.656l-3.872.783v20.233zM69.951 4.847h4.18l-2.438 7.71H68.48l1.471-7.71zM51.521 30.05h3.87V19.523c0-3.209 1.975-5.713 4.801-5.713 2.594 0 3.872 1.878 3.872 4.343v11.898h3.87V18.193c0-4.97-2.98-8.062-7.045-8.062-2.594 0-4.762 1.135-6.388 3.718-1.161-2.349-3.29-3.718-5.884-3.718-2.478 0-4.607 1.37-5.768 3.835v-3.483h-3.872V30.05h3.872V19.523c0-3.209 1.974-5.713 4.8-5.713 2.594 0 3.871 1.878 3.871 4.343v11.898zM0 28.29l1.742-3.562c1.278.94 3.562 1.996 5.304 1.996 2.168 0 3.02-1.056 3.02-2.192.038-1.33-1.046-1.839-3.33-2.582-3.832-1.174-6-2.544-6-6.145 0-3.287 2.477-5.635 6.968-5.635 2.323 0 4.452.743 6.156 1.957l-1.743 3.248c-1.006-.626-2.71-1.566-4.607-1.566-1.664 0-2.67.666-2.67 1.957 0 1.057.735 1.761 3.174 2.505 4.142 1.252 6.194 2.818 6.194 6.105 0 3.718-2.981 5.988-7.124 5.988-2.826 0-5.535-.94-7.084-2.074zm25.628-14.44c-3.445 0-5.807 2.778-5.807 6.457 0 3.64 2.362 6.418 5.807 6.418 3.407 0 5.769-2.857 5.769-6.496 0-3.601-2.362-6.38-5.769-6.38zm5.844 12.21c-1.316 2.857-3.832 4.344-6.813 4.344-4.994 0-8.982-3.875-8.982-10.136 0-6.223 3.988-10.098 8.982-10.098 2.98 0 5.497 1.488 6.813 4.345v-4.031h3.872v19.568h-3.872V26.06zm43.01-1.332L72.74 28.29c1.549 1.135 4.259 2.074 7.085 2.074 4.142 0 7.123-2.27 7.123-5.988 0-3.287-2.052-4.853-6.194-6.105-2.439-.744-3.175-1.448-3.175-2.505 0-1.291 1.007-1.957 2.672-1.957 1.897 0 3.6.94 4.607 1.566l1.742-3.248a10.59 10.59 0 00-6.156-1.957c-4.49 0-6.968 2.348-6.968 5.635 0 3.6 2.168 4.97 6 6.145 2.284.743 3.368 1.252 3.33 2.582 0 1.136-.852 2.192-3.02 2.192-1.742 0-4.026-1.056-5.304-1.996zm21.991-4.46c0-6.223 4.259-10.098 10.182-10.098 2.787 0 5.265.9 7.162 2.818l-2.362 2.935c-1.471-1.487-3.213-2.074-4.762-2.074-3.561 0-6.078 2.152-6.078 6.38 0 4.226 2.517 6.456 6.078 6.456 1.549 0 3.291-.626 4.801-2.191l2.361 2.896c-1.897 1.917-4.336 2.974-7.2 2.974-5.885 0-10.182-3.874-10.182-10.097zm28.077 1.721V10.483h3.871v11.82c0 2.582 1.626 4.343 4.336 4.343 2.71 0 4.336-1.76 4.336-4.344V10.483h3.871V21.99c0 5.362-3.291 8.375-8.207 8.375-4.917 0-8.207-3.013-8.207-8.375z' fill='%23fff'/%3E%3C/svg%3E"
|
||||
/>
|
||||
</Link>
|
||||
</Typography>
|
||||
<LoginButton />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</header>
|
||||
<main>
|
||||
<Box sx={{ m: 3 }}>{children}</Box>
|
||||
</main>
|
||||
<footer></footer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicLayout;
|
||||
13
web/src/layouts/SmallPaperLayout.jsx
Normal file
13
web/src/layouts/SmallPaperLayout.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import BasicLayout from "./BasicLayout";
|
||||
import { Paper, Container } from "@mui/material";
|
||||
|
||||
const SmallPaperLayout = ({ children }) => (
|
||||
<BasicLayout>
|
||||
<Container maxWidth="sm">
|
||||
<Paper sx={{ p: 3}}>{children}</Paper>
|
||||
</Container>
|
||||
</BasicLayout>
|
||||
);
|
||||
|
||||
export default SmallPaperLayout;
|
||||
56
web/src/pages/CustomerMarketDetailPage.jsx
Normal file
56
web/src/pages/CustomerMarketDetailPage.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import userContext from "../context/userContext";
|
||||
import { useParams } from "react-router-dom";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { get, post } from "../common";
|
||||
|
||||
const CustomerMarketDeatilPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
const [data, setData] = React.useState([]);
|
||||
const params = useParams();
|
||||
|
||||
const buyFunc = async (goodsid) => {
|
||||
await post(`/customer/market/${params.marketid}/${goodsid}`, {});
|
||||
refresh();
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
get(`/customer/market/${params.marketid}`).then((resp) => {
|
||||
let goods = resp.goods.map((g) => {
|
||||
delete g.supplier_id;
|
||||
delete g.market_id;
|
||||
g.action = (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => {
|
||||
buyFunc(g.id);
|
||||
}}
|
||||
>
|
||||
Buy!
|
||||
</Button>
|
||||
);
|
||||
return g;
|
||||
});
|
||||
setData(goods);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">
|
||||
Hi {user.username}, you are in market {params.marketid}
|
||||
</Typography>
|
||||
<hr />
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerMarketDeatilPage;
|
||||
51
web/src/pages/CustomerMarketPage.jsx
Normal file
51
web/src/pages/CustomerMarketPage.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { TextField, Typography, Box, Button } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { Link } from "react-router-dom";
|
||||
import { get } from "../common";
|
||||
|
||||
const CustomerMarketPage = () => {
|
||||
const [inputPoint, setInputPoint] = React.useState("");
|
||||
const [data, setData] = React.useState([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
get("/customer/market").then((resp) => {
|
||||
const d = resp.markets.map((m) => {
|
||||
m.action = <Link to={`${m.id}`}>Enter</Link>;
|
||||
return m;
|
||||
});
|
||||
setData(d);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSearch = () => {
|
||||
get(`/customer/market/distance?point=${inputPoint}`).then((resp) => {
|
||||
const d = resp.markets.map((m) => {
|
||||
m.action = <Link to={`${m.id}`}>Enter</Link>;
|
||||
return m;
|
||||
});
|
||||
setData(d);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Find your neaset market</Typography>
|
||||
<hr />
|
||||
<Box style={{ display: "flex" }}>
|
||||
<TextField
|
||||
value={inputPoint}
|
||||
onChange={(event) => setInputPoint(event.target.value)}
|
||||
label="Location"
|
||||
/>
|
||||
<Button variant="contained" color="secondary" onClick={handleSearch}>
|
||||
Search
|
||||
</Button>
|
||||
</Box>
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerMarketPage;
|
||||
40
web/src/pages/CustomerPage.jsx
Normal file
40
web/src/pages/CustomerPage.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import userContext from "../context/userContext";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import ImageCard from "../components/ImageCard";
|
||||
import LogoutCard from "../components/LogoutCard";
|
||||
|
||||
// images
|
||||
import locationImage from "../../public/images/location.webp";
|
||||
import purchasedImage from "../../public/images/purchased.webp";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
const CustomerPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Hello, customer {user.username}</Typography>
|
||||
<hr />
|
||||
<Box style={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Link to="/customer/market">
|
||||
<ImageCard
|
||||
title="Nearby Market"
|
||||
description="Find your neaest market"
|
||||
image={locationImage}
|
||||
/>
|
||||
</Link>
|
||||
<Link to="/customer/purchased">
|
||||
<ImageCard
|
||||
title="Purchased"
|
||||
description="See what you have purchased"
|
||||
image={purchasedImage}
|
||||
/>
|
||||
</Link>
|
||||
<LogoutCard />
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerPage;
|
||||
75
web/src/pages/CustomerPurchasedPage.jsx
Normal file
75
web/src/pages/CustomerPurchasedPage.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import userContext from "../context/userContext";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { get, del, time } from "../common";
|
||||
|
||||
const CustomerPurchasedPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
const [data, setData] = React.useState([]);
|
||||
|
||||
const handleDelete = (id) => {
|
||||
del(`/customer/purchase/${id}`).then(() => {
|
||||
refresh();
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
get(`/customer/purchase`).then((resp) => {
|
||||
// flatten
|
||||
const result = resp.map((r) => {
|
||||
const { id, quantity, purchased_time } = r;
|
||||
const pt = time(purchased_time);
|
||||
const { name, price } = r.goods;
|
||||
const deleteButton = (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
handleDelete(id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
return {
|
||||
id,
|
||||
quantity,
|
||||
pt,
|
||||
name,
|
||||
price,
|
||||
action: deleteButton,
|
||||
};
|
||||
});
|
||||
setData(result);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">
|
||||
Hi {user.username}, here is what you have purchased
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={() => {
|
||||
get(`/customer/purchase/report`).then((data) => {
|
||||
alert(`$The sum of all your perchase is ${data.sum}`);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Summary Report
|
||||
</Button>
|
||||
<hr />
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomerPurchasedPage;
|
||||
57
web/src/pages/HomePage.jsx
Normal file
57
web/src/pages/HomePage.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import ImageCard from "../components/ImageCard";
|
||||
import userContext from "../context/userContext";
|
||||
|
||||
// images
|
||||
import CustomerImage from "../../public/images/customer.webp";
|
||||
import SupplierImage from "../../public/images/supplier.webp";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const HomePage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
const navigator = useNavigate();
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Box sx={{ m: 3 }}>
|
||||
<Typography variant="h4">{"Welcome to sam's club"}</Typography>
|
||||
<Typography>Select a role to continue</Typography>
|
||||
<hr />
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<ImageCard
|
||||
title="Customer module"
|
||||
description="Find a neaest market and purchase goods"
|
||||
image={CustomerImage}
|
||||
onClick={() => {
|
||||
if (user.role !== 1) {
|
||||
alert("You are not allow to use this module");
|
||||
} else {
|
||||
navigator("/customer");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ImageCard
|
||||
title="Supplier"
|
||||
description="Manage the supply chain system and goods."
|
||||
image={SupplierImage}
|
||||
onClick={() => {
|
||||
if (user.role !== 2) {
|
||||
alert("You are not allow to use this module");
|
||||
} else {
|
||||
navigator("/supplier");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
80
web/src/pages/LoginPage.jsx
Normal file
80
web/src/pages/LoginPage.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Button, Stack, TextField, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import SmallPaperLayout from "../layouts/SmallPaperLayout";
|
||||
import SelectRole from "../components/SelectRole";
|
||||
import userContext from "../context/userContext";
|
||||
import { post } from "../common";
|
||||
|
||||
const LoginPage = () => {
|
||||
const { setUser } = React.useContext(userContext);
|
||||
const [inputUsername, setInputUsername] = React.useState("");
|
||||
const [inputPassword, setInputPassword] = React.useState("");
|
||||
const [inputRole, setInputRole] = React.useState(0);
|
||||
|
||||
const validateInput = () => {
|
||||
if (!inputUsername) {
|
||||
alert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
if (inputUsername.length >= 30) {
|
||||
alert("Username too long");
|
||||
return;
|
||||
}
|
||||
if (!inputPassword) {
|
||||
alert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
if (!inputRole) {
|
||||
alert("Role can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// do login
|
||||
loginFunc();
|
||||
};
|
||||
|
||||
const navigator = useNavigate();
|
||||
|
||||
const loginFunc = async () => {
|
||||
const url = '/login';
|
||||
|
||||
const resp = await post(url, {
|
||||
username: inputUsername,
|
||||
password: inputPassword,
|
||||
});
|
||||
setUser(resp)
|
||||
|
||||
const nextURL = inputRole === 1 ? "/customer" : "/supplier";
|
||||
navigator(nextURL);
|
||||
};
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h4" sx={{ pb: 2 }}>
|
||||
Login to Sam AIS
|
||||
</Typography>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="User name"
|
||||
value={inputUsername}
|
||||
onChange={(event) => setInputUsername(event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
value={inputPassword}
|
||||
onChange={(event) => setInputPassword(event.target.value)}
|
||||
type="password"
|
||||
/>
|
||||
<SelectRole inputRole={inputRole} setInputRole={setInputRole} />
|
||||
<Button variant="contained" color="success" onClick={validateInput}>
|
||||
Login
|
||||
</Button>
|
||||
<Typography>
|
||||
Click <Link to="/register">here</Link> to register a new account
|
||||
</Typography>
|
||||
</Stack>
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
16
web/src/pages/ProfilePage.jsx
Normal file
16
web/src/pages/ProfilePage.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from "react";
|
||||
import SmallPaperLayout from '../layouts/SmallPaperLayout';
|
||||
import userContext from "../context/userContext";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
|
||||
const ProfilePages = () => {
|
||||
const { user, setUser } = React.useContext(userContext);
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h3">Hello, {user.username}</Typography>
|
||||
<hr />
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePages;
|
||||
122
web/src/pages/RegisterPage.jsx
Normal file
122
web/src/pages/RegisterPage.jsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import SmallPaperLayout from "../layouts/SmallPaperLayout";
|
||||
import SelectRole from "../components/SelectRole";
|
||||
import { post } from "../common";
|
||||
|
||||
const RegisterPage = () => {
|
||||
const [inputUsername, setInputUsername] = React.useState("");
|
||||
const [inputPassword, setInputPassword] = React.useState("");
|
||||
const [repeatInputPassword, setRepeatInputPassword] = React.useState("");
|
||||
const [inputRole, setInputRole] = React.useState(0);
|
||||
|
||||
const validateInput = () => {
|
||||
if (!inputUsername) {
|
||||
alert("Username can't be empty");
|
||||
return;
|
||||
}
|
||||
if (inputUsername.length >= 30) {
|
||||
alert("Username too long");
|
||||
return;
|
||||
}
|
||||
if (!inputPassword) {
|
||||
alert("Password can't be empty");
|
||||
return;
|
||||
}
|
||||
if (inputPassword !== repeatInputPassword) {
|
||||
alert("Password not matched!");
|
||||
return;
|
||||
}
|
||||
if (!inputRole) {
|
||||
alert("Role can't be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
// do register
|
||||
registerFunc();
|
||||
};
|
||||
|
||||
const navigator = useNavigate();
|
||||
|
||||
const registerFunc = () => {
|
||||
const url = "/register";
|
||||
post(url, {
|
||||
username: inputUsername,
|
||||
password: inputPassword,
|
||||
role: inputRole,
|
||||
}).then(() => {
|
||||
alert("Successfully registered, please go to login page");
|
||||
navigator("/login");
|
||||
});
|
||||
};
|
||||
|
||||
const validatePassword = (p) => {
|
||||
if (p.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (p.length < 8) {
|
||||
return "password should be at least 8 characters";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h4" sx={{ pb: 2 }}>
|
||||
Register
|
||||
</Typography>
|
||||
<Stack spacing={3}>
|
||||
<TextField
|
||||
label="User name"
|
||||
value={inputUsername}
|
||||
onChange={(event) => setInputUsername(event.target.value)}
|
||||
error={inputUsername.length >= 30}
|
||||
helperText={inputUsername.length >= 30 && "User name too long !"}
|
||||
/>
|
||||
<TextField
|
||||
label="Password"
|
||||
value={inputPassword}
|
||||
onChange={(event) => setInputPassword(event.target.value)}
|
||||
type="password"
|
||||
error={validatePassword(inputPassword)}
|
||||
helperText={validatePassword(inputPassword)}
|
||||
/>
|
||||
<TextField
|
||||
label="Repeat password"
|
||||
value={repeatInputPassword}
|
||||
onChange={(event) => setRepeatInputPassword(event.target.value)}
|
||||
type="password"
|
||||
error={inputPassword !== repeatInputPassword}
|
||||
helperText={
|
||||
inputPassword !== repeatInputPassword && "Password not matched"
|
||||
}
|
||||
/>
|
||||
<SelectRole inputRole={inputRole} setInputRole={setInputRole} />
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={<Checkbox />}
|
||||
label={
|
||||
<Typography>
|
||||
I agree the{" "}
|
||||
<Link to="/eula">end-user license agreement (EULA)</Link>
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button variant="contained" color="warning" onClick={validateInput}>
|
||||
Register
|
||||
</Button>
|
||||
</Stack>
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterPage;
|
||||
67
web/src/pages/SupplierAddGoods.jsx
Normal file
67
web/src/pages/SupplierAddGoods.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Button, Stack, TextField, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import SmallPaperLayout from "../layouts/SmallPaperLayout";
|
||||
import { post } from "../common";
|
||||
|
||||
const SupplierAddGoods = () => {
|
||||
const [inputName, setInputName] = React.useState("");
|
||||
const [inputDescription, setInputDescription] = React.useState("");
|
||||
const [inputMarketId, setInputMarketId] = React.useState(0);
|
||||
const [inputQuantity, setInputQuantity] = React.useState(0);
|
||||
const [inputPrice, setInputPrice] = React.useState(0);
|
||||
return (
|
||||
<SmallPaperLayout>
|
||||
<Typography variant="h4">Add New Goods</Typography>
|
||||
<hr />
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
value={inputName}
|
||||
onChange={(event) => setInputName(event.target.value)}
|
||||
label="Product name"
|
||||
/>
|
||||
<TextField
|
||||
value={inputDescription}
|
||||
onChange={(event) => setInputDescription(event.target.value)}
|
||||
label="Product description"
|
||||
/>
|
||||
<TextField
|
||||
value={inputMarketId}
|
||||
onChange={(event) => setInputMarketId(event.target.value)}
|
||||
label="Market ID"
|
||||
type="number"
|
||||
/>
|
||||
<TextField
|
||||
value={inputQuantity}
|
||||
onChange={(event) => setInputQuantity(event.target.value)}
|
||||
label="Product quantity"
|
||||
type="number"
|
||||
/>
|
||||
<TextField
|
||||
value={inputPrice}
|
||||
onChange={(event) => setInputPrice(event.target.value)}
|
||||
label="Product price"
|
||||
type="number"
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="warning"
|
||||
onClick={() => {
|
||||
post(`/supplier/goods`, {
|
||||
name: inputName,
|
||||
description: inputDescription,
|
||||
market_id: parseInt(inputMarketId),
|
||||
quantity: parseInt(inputQuantity),
|
||||
price: inputPrice,
|
||||
}).then(() => {
|
||||
alert("Successfully add new goods");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</Stack>
|
||||
</SmallPaperLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierAddGoods;
|
||||
40
web/src/pages/SupplierGoods.jsx
Normal file
40
web/src/pages/SupplierGoods.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import { get } from "../common";
|
||||
import AutoTable from "../components/AutoTable";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const SupplierGoods = () => {
|
||||
const [data, setData] = React.useState([]);
|
||||
const refresh = () => {
|
||||
get("/supplier/goods").then((resp) => {
|
||||
// delete unused data
|
||||
setData(
|
||||
resp.map((r) => {
|
||||
delete r.supplier_id;
|
||||
return r;
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Goods management</Typography>
|
||||
<Typography>The list of your goods</Typography>
|
||||
<Link to="/supplier/goods/new">
|
||||
<Button variant="contained" color="info">
|
||||
New Goods
|
||||
</Button>
|
||||
</Link>
|
||||
<hr />
|
||||
<AutoTable data={data} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierGoods;
|
||||
36
web/src/pages/SupplierPage.jsx
Normal file
36
web/src/pages/SupplierPage.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import ImageCard from "../components/ImageCard";
|
||||
import BasicLayout from "../layouts/BasicLayout";
|
||||
import supplierGoodsImage from "../../public/images/supplierGoods.webp";
|
||||
import meetingImage from "../../public/images/meeting.webp";
|
||||
import userContext from "../context/userContext";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const SupplierPage = () => {
|
||||
const { user } = React.useContext(userContext);
|
||||
return (
|
||||
<BasicLayout>
|
||||
<Typography variant="h4">Welcome, supplier {user.username}</Typography>
|
||||
<hr />
|
||||
<Box style={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Link to="/supplier/goods">
|
||||
<ImageCard
|
||||
title="Goods Management"
|
||||
description="Create, View, Update, and Delete your goods"
|
||||
image={supplierGoodsImage}
|
||||
/>
|
||||
</Link>
|
||||
<Link to="/supplier/report">
|
||||
<ImageCard
|
||||
title="Sales Report"
|
||||
description="Generate your fanaical report"
|
||||
image={meetingImage}
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupplierPage;
|
||||
Reference in New Issue
Block a user