diff --git a/api/api.go b/api/api.go index d0c23184dcc49065edcfbd0d2c44caf1fb839aac..4b597098e410b609ba46fb0ca2e378849473739b 100644 --- a/api/api.go +++ b/api/api.go @@ -67,8 +67,12 @@ func Init(dbPath string, signKey string, mail *Mail, r *mux.Router) error { r.HandleFunc("/order/{id:[0-9]+}", a.authNum(a.GetOrder)).Methods("GET") r.HandleFunc("/order/{id:[0-9]+}", a.authNumRole(a.UpdateOrder)).Methods("PUT") r.HandleFunc("/order/{id:[0-9]+}", a.authNumRole(a.DeleteOrder)).Methods("DELETE") + r.HandleFunc("/order/{id:[0-9]+}/arrive", a.authNumRole(a.ArrivedOrder)).Methods("PUT") + r.HandleFunc("/order/{id:[0-9]+}/collected", a.authNum(a.CollectOrder)).Methods("PUT") r.HandleFunc("/order/active", a.auth(a.ListActiveOrders)).Methods("GET") r.HandleFunc("/order/picks", a.authOrderNum(a.ListOrderPicks)).Methods("GET") + r.HandleFunc("/order/unarrived", a.authNum(a.ListOrderUnarrived)).Methods("GET") + r.HandleFunc("/order/collectable", a.authNum(a.ListOrderCollectable)).Methods("GET") r.HandleFunc("/order/{id:[0-9]+}/purchase", a.authNum(a.AddOrderPurchase)).Methods("POST") return nil } diff --git a/api/db/order.go b/api/db/order.go index 252856c1c7be56050ea1750d418803a1f1b4bab3..99a7f73c1c61f67f90f20cbb6307958ff97b7328 100644 --- a/api/db/order.go +++ b/api/db/order.go @@ -9,14 +9,19 @@ import ( "gorm.io/gorm/clause" ) +const ( + updateOrderDuration = 5 * 24 * time.Hour +) + type Order struct { gorm.Model - Name string `json:"name"` - Description string `json:"description"` - MemberNum int `json:"member_num" gorm:"column:member"` - Member *Member `json:"member,omitempty" gorm:"foreignKey:MemberNum;references:Num"` - Deadline time.Time `json:"deadline"` - Active bool `json:"active" gorm:"index"` + Name string `json:"name"` + Description string `json:"description"` + MemberNum int `json:"member_num" gorm:"column:member"` + Member *Member `json:"member,omitempty" gorm:"foreignKey:MemberNum;references:Num"` + Deadline time.Time `json:"deadline"` + Active bool `json:"active" gorm:"index"` + Arrived *time.Time `json:"arrived,omitempty"` Products []OrderProduct `json:"products"` Transactions []Transaction `json:"transactions" gorm:"foreignKey:OrderID"` @@ -60,6 +65,29 @@ func (d *DB) ListOrderPicks(num int) (orders []Order, err error) { return } +func (d *DB) ListOrderUnarrived(memberNum int) (orders []Order, err error) { + err = d.db.Preload(clause.Associations). + Preload("Transactions.OrderPurchase"). + Where("member = ?", memberNum). + Where("deadline >= ?", time.Now().Add(-updateOrderDuration)). + Where("active is false"). + Where("arrived is null"). + Find(&orders).Error + return +} + +func (d *DB) ListOrderCollectable(memberNum int) (transactions []Transaction, err error) { + err = d.db.Preload("OrderPurchase.OrderProduct.Product"). + Preload("Order"). + Joins("left join orders on transactions.order_id = orders.id"). + Where("transactions.member = ?", memberNum). + Where("collected is null or collected = false"). + Where("orders.Arrived is not null"). + Find(&transactions). + Error + return +} + func (d *DB) GetOrder(memberNum int, id int) (order Order, transaction Transaction, err error) { err = d.db.Preload(clause.Associations). Preload("Products.Product"). @@ -96,7 +124,7 @@ func (d *DB) UpdateOrder(memberNum int, id int, order *Order) error { if memberNum != 0 && dbOrder.MemberNum != memberNum { return ErrorInvalidRequest } - if dbOrder.Deadline.Add(5 * 24 * time.Hour).Before(time.Now()) { + if dbOrder.Deadline.Add(updateOrderDuration).Before(time.Now()) { return ErrorInvalidRequest } for _, p := range order.Products { @@ -193,6 +221,7 @@ func updateOrderTransaction(tx *gorm.DB, id int, total int, order *Order) error return err } order.Active = true + order.Arrived = nil order.TransactionID = nil } else { totalDiff := total - transaction.Total @@ -434,3 +463,35 @@ func (d *DB) DeactivateOrders() []Order { } return deactivatedOrders } + +func (d *DB) ArrivedOrder(memberNum int, id int) error { + dbOrder, _, err := d.GetOrder(0, id) + if err != nil { + return err + } + + if memberNum != 0 && dbOrder.MemberNum != memberNum { + return ErrorInvalidRequest + } + if dbOrder.Deadline.Add(updateOrderDuration).Before(time.Now()) { + return ErrorInvalidRequest + } + if dbOrder.Active { + return ErrorInvalidRequest + } + + return d.db.Model(&Order{}).Where("id = ?", id).Update("arrived", time.Now()).Error +} + +func (d *DB) CollectOrder(memberNum int, id int) error { + dbOrder, t, err := d.GetOrder(memberNum, id) + if err != nil { + return err + } + + if dbOrder.Active || dbOrder.Arrived == nil { + return ErrorInvalidRequest + } + + return d.db.Model(&Transaction{}).Where("id = ?", t.ID).Update("collected", true).Error +} diff --git a/api/db/transaction.go b/api/db/transaction.go index e888c499d57cdd0ceb1c3283e0b233a54337d537..9bc49ed1bb7cf808a3cd071aa51d034165cdac2f 100644 --- a/api/db/transaction.go +++ b/api/db/transaction.go @@ -32,6 +32,7 @@ type Transaction struct { Order *Order `json:"order,omitempty" gorm:"constraint:OnDelete:CASCADE"` OrderID *uint `json:"-"` Refund *Order `json:"refund,omitempty" gorm:"foreignKey:TransactionID"` + Collected *bool `json:"collected,omitempty"` } type Topup struct { diff --git a/api/order.go b/api/order.go index 259aa476c89c5afdba901a22f2a99b3dbdac9e7d..d6d80ec57a9b1a90643082560c409bc2880aeead 100644 --- a/api/order.go +++ b/api/order.go @@ -73,6 +73,40 @@ func (a *api) ListOrderPicks(num int, w http.ResponseWriter, req *http.Request) } } +func (a *api) ListOrderUnarrived(num int, w http.ResponseWriter, req *http.Request) { + orders, err := a.db.ListOrderUnarrived(num) + if err != nil { + log.Printf("Can't list unarrived orders: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(orders) + if err != nil { + log.Printf("Can't encode unarrived orders: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } +} + +func (a *api) ListOrderCollectable(num int, w http.ResponseWriter, req *http.Request) { + transactions, err := a.db.ListOrderCollectable(num) + if err != nil { + log.Printf("Can't list collectable orders: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(transactions) + if err != nil { + log.Printf("Can't encode collectable orders: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } +} + func (a *api) GetOrder(num int, w http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) id, _ := strconv.Atoi(vars["id"]) @@ -230,3 +264,50 @@ func (a *api) AddOrderPurchase(num int, w http.ResponseWriter, req *http.Request w.WriteHeader(http.StatusInternalServerError) } } + +func (a *api) ArrivedOrder(num int, role string, w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + id, _ := strconv.Atoi(vars["id"]) + if role == "admin" { + num = 0 + } + + err := a.db.ArrivedOrder(num, id) + if err != nil { + if errors.Is(err, db.ErrorNotFound) { + w.WriteHeader(http.StatusNotFound) + return + } + if errors.Is(err, db.ErrorInvalidRequest) { + w.WriteHeader(http.StatusUnauthorized) + return + } + log.Printf("Can't arrive order %s: %v", vars["id"], err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +func (a *api) CollectOrder(num int, w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + id, _ := strconv.Atoi(vars["id"]) + + err := a.db.CollectOrder(num, id) + if err != nil { + if errors.Is(err, db.ErrorNotFound) { + w.WriteHeader(http.StatusNotFound) + return + } + if errors.Is(err, db.ErrorInvalidRequest) { + w.WriteHeader(http.StatusUnauthorized) + return + } + log.Printf("Can't collect order %s: %v", vars["id"], err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusAccepted) +} diff --git a/src/order/OrderCards.js b/src/order/OrderCards.js index 3e38060438435fc03fa3f5bb4e0f16ebdc967c54..64bdfb9b3e8d5818865b712e6a8cf2b55c9ff8fd 100644 --- a/src/order/OrderCards.js +++ b/src/order/OrderCards.js @@ -1,25 +1,104 @@ import React, { useState } from "react"; import { Card, Row, Col, Button } from "react-bootstrap"; import { LinkContainer } from "react-router-bootstrap"; +import Sender from "../Sender"; import Fetcher from "../Fetcher"; function OrderCards() { - const [orders, setOrders] = useState([]); - const order_list = orders.map((o) => ( - <Card key={o.ID} as={Col} sm={4}> + const [unarrived, setUnarrived] = useState([]); + const [collectable, setCollectable] = useState([]); + const [active, setActive] = useState([]); + const [refetch, setRefetch] = useState(0); + + const unarrived_list = unarrived.map((o) => ( + <Card bg="dark" text="white" key={o.ID} as={Col} sm={4}> <Card.Body> - <Card.Title>{o.name}</Card.Title> - <Card.Text>{o.description}</Card.Text> <LinkContainer to={"/order/" + o.ID}> - <Button>Realizar pedido</Button> + <div> + <Card.Title>{o.name}</Card.Title> + <Card.Text>{o.description}</Card.Text> + </div> </LinkContainer> + <Sender + url={"/api/order/" + o.ID + "/arrive"} + method="PUT" + onSuccess={() => setRefetch(refetch + 1)} + > + <Button type="submit" variant="light"> + Informar llegada + </Button> + </Sender> </Card.Body> </Card> )); + + const collectable_list = collectable.map((t) => { + const purchase_list = t.order_purchase.map((o) => { + if (o.amount === 0) { + return ""; + } + return ( + <div key={o.order_product.code}> + {o.order_product.product.name}: {o.amount} + <br /> + </div> + ); + }); + return ( + <Card bg="secondary" text="white" key={"T-" + t.ID} as={Col} sm={4}> + <Card.Body> + <LinkContainer to={"/order/" + t.order.ID}> + <div> + <Card.Title>{t.order.name}</Card.Title> + <Card.Text>{purchase_list}</Card.Text> + </div> + </LinkContainer> + <Sender + url={"/api/order/" + t.order.ID + "/collected"} + method="PUT" + onSuccess={() => setRefetch(refetch + 1)} + > + <Button type="submit" variant="light"> + Recogido + </Button> + </Sender> + </Card.Body> + </Card> + ); + }); + + const active_list = active.map((o) => ( + <LinkContainer to={"/order/" + o.ID}> + <Card bg="info" text="white" key={o.ID} as={Col} sm={4}> + <Card.Body> + <Card.Title>{o.name}</Card.Title> + <Card.Text>{o.description}</Card.Text> + <Button variant="light">Realizar pedido</Button> + </Card.Body> + </Card> + </LinkContainer> + )); + return ( - <Fetcher url={"/api/order/active"} onFetch={setOrders}> - <Row>{order_list}</Row> - </Fetcher> + <Row> + <Fetcher + url={"/api/order/unarrived"} + onFetch={setUnarrived} + refetch={refetch} + > + {unarrived_list} + </Fetcher> + <Fetcher + url={"/api/order/collectable"} + onFetch={setCollectable} + refetch={refetch} + > + {collectable_list} + </Fetcher> + <Fetcher url={"/api/order/active"} onFetch={setActive}> + {active_list} + </Fetcher> + </Row> ); } diff --git a/src/order/ShowOrder.js b/src/order/ShowOrder.js index 5d26ecddc84ab05d44088eaa4e2bcecdd78bb77e..2bf78a8ae80d7e042628f6356285cdfdc8b2582b 100644 --- a/src/order/ShowOrder.js +++ b/src/order/ShowOrder.js @@ -13,6 +13,7 @@ import { Modal, } from "react-bootstrap"; import PurchaseOrder from "./PurchaseOrder"; +import Sender from "../Sender"; import { printDate } from "../util"; import AuthContext from "../AuthContext"; import { printMoney, url } from "../util"; @@ -68,6 +69,7 @@ function ShowOrderResults(props) { return ( <li key={t.member.num}> {t.member.name} ({t.member.num}): + {t.collected && <Badge variant="success">Recogido</Badge>} <ul>{list}</ul> </li> ); @@ -173,29 +175,66 @@ class ShowOrder extends React.Component { const { id } = this.props.match.params; let update_button; + let collect_button; if (this.state.isLoading) { update_button = <Spinner animation="border" />; } else { let deadline_week = new Date(order.deadline); deadline_week.setDate(deadline_week.getDate() + 7); - if ( - (order.member_num === parseInt(this.context.num) || - this.context.role === "admin") && - deadline_week > Date.now() - ) { - update_button = ( - <ButtonGroup> - <LinkContainer to={"/order/edit/" + id}> - <Button variant="info">Modificar</Button> - </LinkContainer> - <Button - variant="danger" - onClick={() => this.setState({ showDelete: true })} + if (deadline_week > Date.now()) { + if (order.arrived && !this.state.transaction.collected) { + collect_button = ( + <Sender + url={"/api/order/" + id + "/collected"} + method="PUT" + onSuccess={() => + this.setState({ refetch: this.state.refetch + 1 }) + } > - Cancelar - </Button> - </ButtonGroup> - ); + <Button type="submit" variant="secondary"> + Recogido + </Button> + </Sender> + ); + } + if ( + order.member_num === parseInt(this.context.num) || + this.context.role === "admin" + ) { + let arrived_button; + if (!order.active && !order.arrived) { + arrived_button = ( + <Sender + url={"/api/order/" + id + "/arrive"} + method="PUT" + onSuccess={() => + this.setState({ refetch: this.state.refetch + 1 }) + } + > + <Button type="submit" variant="dark"> + Informar llegada + </Button> + </Sender> + ); + } + update_button = ( + <div> + <ButtonGroup> + <LinkContainer to={"/order/edit/" + id}> + <Button variant="info">Modificar</Button> + </LinkContainer> + <Button + variant="danger" + onClick={() => this.setState({ showDelete: true })} + > + Cancelar + </Button> + </ButtonGroup> + <br /> + {arrived_button} + </div> + ); + } } } @@ -223,6 +262,7 @@ class ShowOrder extends React.Component { {expired} </p> {update_button} + {collect_button} </Col> </Row> <p>{this.state.order.description}</p>