From 078d6f78d39fdc615bcf205c99290157d760a60f Mon Sep 17 00:00:00 2001 From: meskio <meskio@sindominio.net> Date: Fri, 25 Sep 2020 19:52:14 +0200 Subject: [PATCH] Add purchases --- package-lock.json | 20 +++++ package.json | 2 + src/App.js | 8 ++ src/AuthContext.js | 6 +- src/Dashboard.js | 16 +++- src/Fetcher.js | 3 + src/SignIn.js | 2 +- src/index.js | 1 + src/purchase/Purchase.js | 155 +++++++++++++++++++++++++++++++++++ src/purchase/PurchaseForm.js | 91 ++++++++++++++++++++ src/purchase/ShowPurchase.js | 53 ++++++++++++ 11 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 src/purchase/Purchase.js create mode 100644 src/purchase/PurchaseForm.js create mode 100644 src/purchase/ShowPurchase.js diff --git a/package-lock.json b/package-lock.json index 07db138..f4dae88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10469,6 +10469,21 @@ "warning": "^4.0.3" } }, + "react-bootstrap-table-next": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/react-bootstrap-table-next/-/react-bootstrap-table-next-4.0.3.tgz", + "integrity": "sha512-uKxC73qUdUfusRf2uzDfMiF9LvTG5vuhTZa0lbAgHWSLLLaKTsI0iHf1e4+c7gP71q8dFsp7StvkP65SxC1JRg==", + "requires": { + "classnames": "^2.2.5", + "react-transition-group": "^4.2.0", + "underscore": "1.9.1" + } + }, + "react-bootstrap-table2-editor": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-bootstrap-table2-editor/-/react-bootstrap-table2-editor-1.4.0.tgz", + "integrity": "sha512-18yDCwsVt3b5Fwy0jidNDAbUA6vC7k9JjQVmykazWSw8G115+mmZnhe9/7RO7jAu8X7lhmobwlNwECzwPu1nDg==" + }, "react-dev-utils": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz", @@ -12756,6 +12771,11 @@ "react-lifecycles-compat": "^3.0.4" } }, + "underscore": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", + "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/package.json b/package.json index 9b39a3a..d4d957b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "bootstrap": "^4.5.2", "react": "^16.13.1", "react-bootstrap": "^1.3.0", + "react-bootstrap-table-next": "^4.0.3", + "react-bootstrap-table2-editor": "^1.4.0", "react-dom": "^16.13.1", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3" diff --git a/src/App.js b/src/App.js index 44d69df..e88ea82 100644 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,8 @@ import { BrowserRouter, Switch, Route } from 'react-router-dom'; import MemberList from './member'; import ProductList from './ProductList'; import Dashboard from './Dashboard'; +import Purchase from './purchase/Purchase'; +import ShowPurchase from './purchase/ShowPurchase'; import AuthContext from './AuthContext'; import SignIn from './SignIn'; import Head from './Head'; @@ -19,6 +21,12 @@ function Panel(props) { <Route path="/products"> <ProductList /> </Route> + <Route path="/purchase/:id"> + <ShowPurchase /> + </Route> + <Route path="/purchase"> + <Purchase /> + </Route> <Route path="/"> <Dashboard /> </Route> diff --git a/src/AuthContext.js b/src/AuthContext.js index 622eb7d..74c76a2 100644 --- a/src/AuthContext.js +++ b/src/AuthContext.js @@ -1,5 +1,9 @@ import { createContext } from 'react'; -export const AuthContext = createContext({}); +export const AuthContext = createContext({ + num: 0, + role: "", + token: null, +}); export default AuthContext; diff --git a/src/Dashboard.js b/src/Dashboard.js index 0752448..eeba0a7 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Container } from 'react-bootstrap'; +import { Container, Row, Col, Button } from 'react-bootstrap'; import AuthContext from './AuthContext'; import Fetcher from './Fetcher'; import printMoney from './util'; @@ -21,8 +21,18 @@ class Dashboard extends React.Component { url={"/api/member/"+this.context.num.toString()} onFetch={member => this.setState({ balance: member.balance, name: member.name})} > <Container> - <h1>{this.state.name}</h1> - <p>Saldo: {printMoney(this.state.balance)}</p> + <Row> + <Col xs> + <div className="text-right"> + <h6>{this.state.name}</h6> + <h1>{printMoney(this.state.balance)}€</h1> + </div> + </Col> + <Col xs={{ offset: 0 }} md={{ offset: 1 }}> + <br /> + <Button variant="success" href="/purchase">Compra</Button> + </Col> + </Row> </Container> </Fetcher> ); diff --git a/src/Fetcher.js b/src/Fetcher.js index 22a563b..657c5b6 100644 --- a/src/Fetcher.js +++ b/src/Fetcher.js @@ -27,6 +27,9 @@ class Fetcher extends React.Component { } fetch() { + if (this.state.error) { + this.setState({ error: null }); + } fetch(this.props.url, { headers: { 'x-authentication': this.context.token } }) .then(response => { if (!response.ok) { diff --git a/src/SignIn.js b/src/SignIn.js index 05ed7f9..46e42aa 100644 --- a/src/SignIn.js +++ b/src/SignIn.js @@ -19,7 +19,7 @@ class SignIn extends React.Component { onFormSubmit(e) { e.preventDefault(); - this.setState({isLoading: true}); + this.setState({isLoading: true, error: null}); const body = JSON.stringify({ name: this.state.name, password: this.state.password}); diff --git a/src/index.js b/src/index.js index 9ed4e73..17f4c8f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import App from './App'; import * as serviceWorker from './serviceWorker'; import 'bootstrap/dist/css/bootstrap.min.css'; +import 'react-bootstrap-table-next/dist/react-bootstrap-table2.min.css'; ReactDOM.render( <React.StrictMode> diff --git a/src/purchase/Purchase.js b/src/purchase/Purchase.js new file mode 100644 index 0000000..1969d53 --- /dev/null +++ b/src/purchase/Purchase.js @@ -0,0 +1,155 @@ +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import Fetcher from '../Fetcher'; +import PurchaseForm from './PurchaseForm'; +import { Row, Col, Button, Alert, Spinner } from 'react-bootstrap'; +import BootstrapTable from 'react-bootstrap-table-next'; +import cellEditFactory from 'react-bootstrap-table2-editor'; +import AuthContext from '../AuthContext'; +import printMoney from '../util'; + +const columns = [ + {dataField: 'code', text: 'codigo', editable: false}, + {dataField: 'name', text: 'nombre', editable: false}, + {dataField: 'priceStr', text: 'precio', editable: false}, + {dataField: 'ammount', text: 'cantidad', validator: v => { + if (isNaN(v)) { + return { + valid: false, + message: "no es un numero" + }; + } + return true; + }}, + {dataField: 'del', text: '', editable: false}, +] + +class Purchase extends React.Component { + static contextType = AuthContext; + + constructor(props) { + super(props); + this.state = { + products: [], + purchase: [], + total: 0, + purchaseId: null, + isLoading: false, + badAuth: false, + error: null + }; + } + + addProduct(product) { + const code = product.code; + if (this.state.purchase.find(p => p.code === code) !== undefined) { + return false; + } + + product.priceStr = printMoney(product.price)+"€"; + product.del = <Button onClick={() => this.delProduct(code)}>-</Button>; + let purchase = this.state.purchase; + purchase.push(product); + this.setState({ purchase }); + this.updateTotal(purchase); + return true; + } + + delProduct(code) { + const i = this.state.purchase.findIndex(p => p.code === code); + let purchase = this.state.purchase; + purchase.splice(i,1); + this.setState({purchase}); + this.updateTotal(purchase); + } + + updateTotal(purchase) { + const add = (acc, p) => acc + (p.price*p.ammount); + const total = purchase.reduce(add, 0); + this.setState({total}); + } + + send() { + this.setState({isLoading: true, error: null, noMoney: false}); + const body = JSON.stringify(this.state.purchase.map(p => { + return { + product: parseInt(p.code), + ammount: parseInt(p.ammount) + }; + })); + fetch("/api/purchase", {headers: {'x-authentication': this.context.token}, method: "POST", body}) + .then(response => { + if (response.status === 400) { + var error = new Error("Not enough money"); + error.name ="not-money"; + throw error; + } else if (!response.ok) { + throw new Error(response.status.toString()+' '+response.statusText); + } + return response.json(); + }) + .then(p => { + this.setState({purchaseId: p.ID, isLoading: false}); + }) + .catch(error => { + if (error.name === "not-money") { + this.setState({isLoading: false, error: null, noMoney: true}); + } else { + this.setState({isLoading: false, error: error}) + } + }); + } + + render() { + if (this.state.isLoading) { + return <Spinner animation="border" />; + } + + let alert; + if (this.state.noMoney != null) { + alert = ( + <Alert variant="warning"> + No tienes suficiente dinero para realizar esta compra. + </Alert> + ); + } else if (this.state.error != null) { + alert = ( + <Alert variant="danger"> + Ha ocurrido un error enviando la compra: {this.state.error} + </Alert> + ); + } + if (this.state.purchaseId !== null) { + return <Redirect to={"/purchase/"+this.state.purchaseId} />; + } + + const purchase = this.state.purchase; + return ( + <Fetcher url="/api/product" onFetch={products => this.setState({ products })} > + {alert} + <BootstrapTable + keyField="code" + data={ purchase } + columns={ columns } + cellEdit={ cellEditFactory({ + mode: 'click', + afterSaveCell: () => this.updateTotal(this.state.purchase) + }) } /> + <PurchaseForm products={this.state.products} onSubmit={p => this.addProduct(p)} /> + <br /> + <Row> + <Col> + <h3>Total: {printMoney(this.state.total)}€</h3> + </Col> + <Col> + <div className="text-right"> + <Button onClick={() => this.send()}>Enviar</Button> + </div> + </Col> + </Row> + </Fetcher> + ); + } +} + +export default Purchase; diff --git a/src/purchase/PurchaseForm.js b/src/purchase/PurchaseForm.js new file mode 100644 index 0000000..c7338a2 --- /dev/null +++ b/src/purchase/PurchaseForm.js @@ -0,0 +1,91 @@ +import React from 'react'; +import { Col, Form, Button } from 'react-bootstrap'; +import printMoney from '../util'; + + +class PurchaseForm extends React.Component { + constructor(props) { + super(props); + this.state = { + codeInvalid: false, + code: "", + name: null, + price: null, + ammount: 1 + }; + } + + setCode(codeStr) { + const code = parseInt(codeStr); + const product = this.props.products.find(p => p.code === code); + const name = product === undefined? null: product.name; + const price = product === undefined? null: product.price; + + this.setState({ + code: codeStr, name, price, + codeInvalid: product === undefined + }); + } + + submit(e) { + e.preventDefault(); + if (this.state.name == null) { + return + } + const product = { + code: this.state.code, + ammount: this.state.ammount, + price: this.state.price, + name: this.state.name + }; + + if (this.props.onSubmit(product)) { + this.setState({ + code: "", + name: null, + price: null, + ammount: 1 + }); + } else { + this.setState({ codeInvalid: true }); + } + } + + render() { + // TODO: add it by name... + return ( + <Form onSubmit={e => this.submit(e)}> + <Form.Row> + <Col> + <Form.Control + placeholder="codigo" + value={this.state.code} + onChange={e => this.setCode(e.target.value)} + isInvalid={this.state.codeInvalid} + /> + </Col> + <Col> + {this.state.name} + </Col> + <Col> + {printMoney(this.state.price)+"€"} + </Col> + <Col> + <Form.Control + type="number" min="1" + placeholder="cantidad" + value={this.state.ammount} + onChange={e => this.setState({ammount: e.target.value})} + /> + </Col> + <Col> + <Button type="submit">+</Button> + </Col> + </Form.Row> + </Form> + ); + } + +} + +export default PurchaseForm; diff --git a/src/purchase/ShowPurchase.js b/src/purchase/ShowPurchase.js new file mode 100644 index 0000000..1002c33 --- /dev/null +++ b/src/purchase/ShowPurchase.js @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Table, Row, Col } from 'react-bootstrap'; +import Fetcher from '../Fetcher'; +import printMoney from '../util'; + +function ShowPurchase() { + const { id } = useParams(); + const [purchase, setPurchase] = useState(0); + + console.log(purchase) + let entries; + if (purchase.products) { + entries = purchase.products.map(p => { + return ( + <tr key={p.product}> + <td>{p.product}</td> + <td>{p.Product.name}</td> + <td>{printMoney(p.price)}€</td> + <td>{p.ammount}</td> + </tr> + ) + }); + } + + return ( + <Fetcher url={"/api/purchase/"+id} onFetch={setPurchase} > + <Table striped bordered hover> + <thead> + <tr> + <th>Codigo</th> + <th>Nombre</th> + <th>Precio</th> + <th>Cantidad</th> + </tr> + </thead> + <tbody> + {entries} + </tbody> + </Table> + <Row> + <Col> + <h3>Total: {printMoney(purchase.total)}€</h3> + </Col> + <Col> + <p className="text-right">{new Date(purchase.date).toLocaleDateString()}</p> + </Col> + </Row> + </Fetcher> + ); +} + +export default ShowPurchase; -- GitLab