diff --git a/src/App.js b/src/App.js index 679b601437272008e38a21662097852f2c155325..8bb058954d85b71d178b57fc57eecf56b308908f 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,8 @@ import Dashboard from './Dashboard'; import Purchase from './purchase/Purchase'; import Topup from './Topup'; import ShowTransaction from './ShowTransaction'; +import ShowOrder from './order/ShowOrder'; +import CreateOrder from './order/CreateOrder'; import AuthContext from './AuthContext'; import SignIn from './SignIn'; import Head from './Head'; @@ -31,6 +33,10 @@ function Panel(props) { <Route path="/topup"> <Topup /> </Route> + <Route path="/order/create"> + <CreateOrder /> + </Route> + <Route path="/order/:id" component={ShowOrder} /> <Route path="/"> <Dashboard /> </Route> diff --git a/src/Dashboard.js b/src/Dashboard.js index 6198dbb555ea057a837f028613b26c6ed7989b5d..42377cb220df8aeab17f353865968fbe4262dd7a 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -2,6 +2,7 @@ import React from 'react'; import { Container, Row, Col, Button } from 'react-bootstrap'; import AuthContext from './AuthContext'; import Fetcher from './Fetcher'; +import OrderList from './order/OrderList'; import TransactionList from './TransactionList'; import { printMoney } from './util'; @@ -18,10 +19,10 @@ class Dashboard extends React.Component { render() { return ( - <Fetcher - url={"/api/member/me"} - onFetch={member => this.setState({ balance: member.balance, name: member.name})} > - <Container> + <Container> + <Fetcher + url={"/api/member/me"} + onFetch={member => this.setState({ balance: member.balance, name: member.name})} > <Row> <Col xs> <div className="text-right"> @@ -34,9 +35,10 @@ class Dashboard extends React.Component { <Button variant="success" href="/purchase">Comprar</Button> </Col> </Row> + <OrderList /> <TransactionList /> - </Container> - </Fetcher> + </Fetcher> + </Container> ); } } diff --git a/src/Head.js b/src/Head.js index 5241de98a056d85f34a60233a74e6c7d61b3ff21..b47ff38ff425020db86440c2ca5d8cbcd843105f 100644 --- a/src/Head.js +++ b/src/Head.js @@ -27,6 +27,7 @@ function Head(props) { <Nav className="mr-auto" activeKey={location.pathname}> <Nav.Link href="/products">Productos</Nav.Link> <Nav.Link href="/purchase">Comprar</Nav.Link> + <Nav.Link href="/order/create">Abrir pedido</Nav.Link> </Nav> {adminNav} <Form inline> diff --git a/src/Topup.js b/src/Topup.js index 0139ab3d83bdcd90704676396ecae518d934a40b..b427600515157efaa505282b743b1a93c8ca93ce 100644 --- a/src/Topup.js +++ b/src/Topup.js @@ -140,7 +140,7 @@ class Topup extends React.Component { </Form.Group> <Form.Group as={Row}> <Col sm={{ span: 10, offset: 2 }}> - <Button type="submit">Envia</Button> + <Button type="submit">Recarga</Button> </Col> </Form.Group> </Form> diff --git a/src/TransactionList.js b/src/TransactionList.js index 2c5f4251fe2583b0a78dd5227eb7982ac2cd3f99..74d1002a0ef45d848d19a5fe083dccf0d505f5ae 100644 --- a/src/TransactionList.js +++ b/src/TransactionList.js @@ -3,6 +3,7 @@ import { Redirect } from 'react-router-dom'; import BootstrapTable from 'react-bootstrap-table-next'; import { FaShoppingBasket, FaMoneyBillAlt } from 'react-icons/fa'; import { GiPayMoney, GiReceiveMoney } from 'react-icons/gi'; +import { HiClipboardCopy, HiClipboardList } from 'react-icons/hi'; import Fetcher from './Fetcher'; import { printMoney, printDate } from './util'; @@ -17,6 +18,10 @@ const columns = [ return <GiReceiveMoney />; } return <GiPayMoney />; + case "order": + return <HiClipboardList />; + case "refund": + return <HiClipboardCopy />; default: return <FaMoneyBillAlt />; }; diff --git a/src/order/CreateOrder.js b/src/order/CreateOrder.js new file mode 100644 index 0000000000000000000000000000000000000000..740021e62b4dc1a6383a359fdabb8720965d4c77 --- /dev/null +++ b/src/order/CreateOrder.js @@ -0,0 +1,126 @@ +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import { Container, Form, Row, Col, Button, Alert, Spinner } from 'react-bootstrap'; +import ProductPicker from '../ProductPicker'; +import AuthContext from '../AuthContext'; + +function date2string(date) { + return date.toISOString().split("T")[0]; +} + +function daysAfterNow(days) { + let date = new Date(); + date.setDate(date.getDate() + days); + return date; +} + +class CreateOrder extends React.Component { + static contextType = AuthContext; + + constructor(props) { + super(props); + this.state = { + products: [], + name: "", + description: "", + deadline: daysAfterNow(3), + orderId: null, + isLoading: false, + error: null + }; + } + + submit(e) { + e.preventDefault(); + + this.setState({isLoading: true, error: null, noMoney: false}); + const products = this.state.products.map(p => { + return { code: p.code }; + }); + const body = JSON.stringify({ + name: this.state.name, + description: this.state.description, + deadline: this.state.deadline, + products + }); + fetch("/api/order", {headers: {'x-authentication': this.context.token}, method: "POST", body}) + .then(response => { + if (!response.ok) { + throw new Error(response.status.toString()+' '+response.statusText); + } + return response.json(); + }) + .then(o => this.setState({orderId: o.ID, isLoading: false})) + .catch(error => this.setState({isLoading: false, error: error.message})); + } + + render() { + if (this.state.isLoading) { + return <Spinner animation="border" />; + } + + let alert; + if (this.state.error != null) { + alert = ( + <Alert variant="danger"> + Ha ocurrido un error creando el pedido: {this.state.error} + </Alert> + ); + } + if (this.state.orderId !== null) { + return <Redirect to={"/order/"+this.state.orderId} />; + } + + return ( + <Container> + {alert} + <Form onSubmit={e => this.submit(e)}> + <Form.Group as={Row}> + <Form.Label as="legend" column sm={3}>Nombre</Form.Label> + <Col sm={9}> + <Form.Control + placeholder="nombre del pedido" + value={this.state.name} + onChange={e => this.setState({name: e.target.value})} + /> + </Col> + </Form.Group> + <Form.Group as={Row}> + <Form.Label as="legend" column sm={3}>Descripción</Form.Label> + <Col sm={9}> + <Form.Control + as='textarea' + value={this.state.description} + onChange={e => this.setState({description: e.target.value})} + /> + </Col> + </Form.Group> + <Form.Group as={Row}> + <Form.Label as="legend" column sm={3}>Fecha lÃmite</Form.Label> + <Col sm={9}> + <Form.Control + type='date' + value={date2string(this.state.deadline)} + onChange={e => this.setState({deadline: Date(e.target.value)})} + min={date2string(daysAfterNow(1))} + /> + </Col> + </Form.Group> + <ProductPicker + picks={this.state.products} + setPicks={products => this.setState({ products })} + /> + <Form.Group as={Row}> + <Col> + <div className="text-right"> + <Button type="submit">Abrir pedido</Button> + </div> + </Col> + </Form.Group> + </Form> + </Container> + ); + } +} + +export default CreateOrder; diff --git a/src/order/OrderList.js b/src/order/OrderList.js new file mode 100644 index 0000000000000000000000000000000000000000..b6eaadee5a85d5a9bf2379c68b397bf5b104d425 --- /dev/null +++ b/src/order/OrderList.js @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import Fetcher from '../Fetcher'; +import { Container, Card, Row, Col, Button } from 'react-bootstrap'; + +function OrderList() { + const [ orders, setOrders ] = useState([]); + const order_list = orders.map(o => + <Card as={Col} sm={4}> + <Card.Body> + <Card.Title>{o.name}</Card.Title> + <Card.Text>{o.description}</Card.Text> + <Button href={"/order/"+o.ID}>Realizar pedido</Button> + </Card.Body> + </Card> + ); + return ( + <Container> + <Fetcher url={"/api/order/active"} onFetch={setOrders} > + <Row> + {order_list} + </Row> + </Fetcher> + </Container> + ); +} + +export default OrderList; diff --git a/src/order/PurchaseOrder.js b/src/order/PurchaseOrder.js new file mode 100644 index 0000000000000000000000000000000000000000..ea3ef20a5158781332c6fc83303500b46e144a30 --- /dev/null +++ b/src/order/PurchaseOrder.js @@ -0,0 +1,129 @@ +import React from 'react'; +import { Alert, Spinner, Form, InputGroup, Button, Col, Row } from 'react-bootstrap'; +import AuthContext from '../AuthContext'; +import { printMoney } from '../util'; + +class PurchaseOrder extends React.Component { + static contextType = AuthContext; + + constructor(props) { + super(props); + let order = props.order.products; + order = order.map(p => { + p.amount = 0; + p.purchased = 0; + return p; + }); + props.order.transactions.forEach(t => { + t.order_purchase.forEach(purchase => { + const i = order.findIndex(p => p.code === purchase.product_code); + if (i) { + order[i].purchased += purchase.amount; + } + }); + }); + this.state = { + order: order, + total: 0, + isLoading: false, + noMoney: false, + error: null + }; + } + + send(e) { + e.preventDefault(); + + this.setState({isLoading: true, error: null, noMoney: false}); + const purchase = this.state.order.map(p => { + return { + product_code: p.code, + amount: parseInt(p.amount) + }; + }); + const body = JSON.stringify({ + order: this.props.order.ID, + purchase + }); + console.log(body); + fetch("/api/order/purchase", {headers: {'x-authentication': this.context.token}, method: "POST", body}) + .then(response => { + if (response.status === 400) { + this.setState({isLoading: false, noMoney: true}); + } else if (!response.ok) { + this.setState({isLoading: false, error: response.status.toString()+' '+response.statusText}) + } else { + this.props.onSend(this.state.order, this.state.total); + } + }) + } + + setAmount(index, amount) { + let order = this.state.order; + order[index].amount = amount; + + const add = (acc, p) => acc + (p.price*p.amount); + const total = order.reduce(add, 0); + + this.setState({ order, total }); + } + + render() { + if (this.state.isLoading) { + return <Spinner animation="border" />; + } + + let alert; + if (this.state.noMoney) { + alert = ( + <Alert variant="warning"> + No tienes suficiente dinero para realizar este pedido. + </Alert> + ); + } else if (this.state.error != null) { + alert = ( + <Alert variant="danger"> + Ha ocurrido un error enviando el pedido: {this.state.error} + </Alert> + ); + } + + const formEntries = this.state.order.map((p, i) => + <Form.Group key={p.code} as={Row}> + <Form.Label column className="text-right"> + {p.name} ({p.code}): + </Form.Label> + <Col> + <InputGroup> + <Form.Control + type="number" min="0" + placeholder="cantidad" + value={p.amount} + onChange={e => this.setAmount(i, e.target.value)} + /> + <InputGroup.Append> + <InputGroup.Text>{printMoney(p.price)}€</InputGroup.Text> + </InputGroup.Append> + </InputGroup> + </Col> + </Form.Group> + ); + + return ( + <Form onSubmit={e => this.send(e)}> + {alert} + {formEntries} + <Form.Group as={Row}> + <Col className="text-right"> + <Button type="submit">Realizar pedido</Button> + </Col> + <Col> + <h3>Total: {printMoney(this.state.total)}€</h3> + </Col> + </Form.Group> + </Form> + ); + } +} + +export default PurchaseOrder; diff --git a/src/order/ShowOrder.js b/src/order/ShowOrder.js new file mode 100644 index 0000000000000000000000000000000000000000..723c3bb83c98b1b259ee02ab226c92b8dcb1794a --- /dev/null +++ b/src/order/ShowOrder.js @@ -0,0 +1,130 @@ +import React from 'react'; +import Fetcher from '../Fetcher'; +import { Container, Row, Col, Badge } from 'react-bootstrap'; +import PurchaseOrder from './PurchaseOrder'; +import { printDate } from '../util'; +import AuthContext from '../AuthContext'; +import { printMoney } from '../util'; + +function ShowOrderTransaction(props) { + const list = props.transaction.order_purchase.map(o => + <li key={o.product.code}> + {o.product.name} ({o.product.code}): {o.amount} + </li> + ); + return ( + <div> + <h3>Mi pedido</h3> + <ul> + {list} + </ul> + <p>Total: {printMoney(-props.transaction.total)} €</p> + </div> + ); +} + +function ShowOrderResults(props) { + let products = props.order.products.map(p => { + p.amount = 0; + return p; + }); + const transactions = props.order.transactions.map(t => { + const list = t.order_purchase.map(purchase => { + const i = products.findIndex(p => p.code === purchase.product_code); + if (!i) { + return null; + } + products[i].amount += purchase.amount; + if (purchase.amount) { + const key = t.member.num.toString() + "-" + purchase.product_code.toString(); + return <li key={key}>{products[i].name} {purchase.amount}</li>; + } + return null; + }); + return ( + <li key={t.member.num}> + {t.member.name} ({t.member.num}): + <ul> + {list} + </ul> + </li> + ); + }); + + const product_list = products.map(p => <li key={p.code}>{p.name}: {p.amount}</li>); + return ( + <div> + <h3>Productos pedidos</h3> + <ul>{product_list}</ul> + + <h3>Pedidos</h3> + <ul>{transactions}</ul> + </div> + ); +} + +class ShowOrder extends React.Component { + static contextType = AuthContext; + + constructor(props) { + super(props); + this.state = { + order: { + products: [], + transactions: [] + }, + transaction: null + }; + } + + onSend(purchase, total) { + const order_purchase = purchase.map(p => { + p.product = p; + return p; + }); + const transaction = { order_purchase, total: -total }; + this.setState({ transaction }); + } + + showTransaction() { + if (this.state.transaction) { + return <ShowOrderTransaction order={this.state.order} transaction={this.state.transaction} />; + } + if (this.state.order.active) { + return <PurchaseOrder order={this.state.order} onSend={(p, t) => this.onSend(p, t)}/> + } + } + + setData(data) { + this.setState({ order: data.order, transaction: data.transaction }); + } + + render() { + let expired; + if (!this.state.order.active) { + expired = <Badge variant="info">finalizado</Badge> + } + + const { id } = this.props.match.params; + return ( + <Container> + <Fetcher url={"/api/order/"+id} onFetch={data => this.setData(data)} > + <Row> + <Col> + <h1>{this.state.order.name}</h1> + </Col> + <Col> + <p className="text-right">Fecha limite: {printDate(this.state.order.deadline)}{expired}</p> + </Col> + </Row> + <p>{this.state.order.description}</p> + + {this.showTransaction()} + <ShowOrderResults order={this.state.order} /> + </Fetcher> + </Container> + ); + } +} + +export default ShowOrder;