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