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