From 7d0f5ee73a8cd7089762a5eb4d3a035369df818b Mon Sep 17 00:00:00 2001 From: meskio <meskio@sindominio.net> Date: Mon, 1 Mar 2021 17:07:16 +0100 Subject: [PATCH] Inventary UI --- api/db/inventary.go | 2 +- src/Head.js | 29 ++++++++-- src/Panel.js | 16 ++++++ src/ProductPicker.js | 19 ++++--- src/inventary/CreateInventary.js | 92 ++++++++++++++++++++++++++++++++ src/inventary/CreateSupplier.js | 43 +++++++++++++++ src/inventary/InventaryList.js | 70 ++++++++++++++++++++++++ src/inventary/ShowInventary.js | 46 ++++++++++++++++ src/util.js | 16 +++++- 9 files changed, 322 insertions(+), 11 deletions(-) create mode 100644 src/inventary/CreateInventary.js create mode 100644 src/inventary/CreateSupplier.js create mode 100644 src/inventary/InventaryList.js create mode 100644 src/inventary/ShowInventary.js diff --git a/api/db/inventary.go b/api/db/inventary.go index 9841dab..20ec20b 100644 --- a/api/db/inventary.go +++ b/api/db/inventary.go @@ -17,7 +17,7 @@ type Inventary struct { MemberNum int `json:"-" gorm:"column:member"` Member *Member `json:"member,omitempty" gorm:"foreignKey:MemberNum;references:Num"` Date time.Time `json:"date"` - SupplierID *uint `json:"-"` + SupplierID *uint `json:"supplier_id"` Supplier *Supplier `json:"supplier"` Comment string `json:"comment"` Products []InventaryProduct `json:"products"` diff --git a/src/Head.js b/src/Head.js index c05aba4..cc55b70 100644 --- a/src/Head.js +++ b/src/Head.js @@ -31,6 +31,31 @@ function Head(props) { </NavDropdown> ); } + + let productNav = ( + <LinkContainer to="/products"> + <Nav.Link>Productos</Nav.Link> + </LinkContainer> + ); + if (auth.role === "admin") { + productNav = ( + <NavDropdown title="Productos" id="admin"> + <LinkContainer to="/products"> + <NavDropdown.Item>Productos</NavDropdown.Item> + </LinkContainer> + <LinkContainer to="/inventary"> + <NavDropdown.Item>Inventario</NavDropdown.Item> + </LinkContainer> + <LinkContainer to="/inventary/add"> + <NavDropdown.Item>Entrada</NavDropdown.Item> + </LinkContainer> + <LinkContainer to="/supplier/add"> + <NavDropdown.Item>Añade Proveedor</NavDropdown.Item> + </LinkContainer> + </NavDropdown> + ); + } + return ( <Navbar bg="light" expand="sm"> <LinkContainer to="/"> @@ -46,10 +71,8 @@ function Head(props) { </LinkContainer> <Navbar.Toggle aria-controls="basic-navbar-nav" /> <Navbar.Collapse id="basic-navbar-nav"> + {productNav} <Nav className="mr-auto" activeKey={location.pathname}> - <LinkContainer to="/products"> - <Nav.Link>Productos</Nav.Link> - </LinkContainer> <LinkContainer to="/purchase"> <Nav.Link>Comprar</Nav.Link> </LinkContainer> diff --git a/src/Panel.js b/src/Panel.js index 8dc728d..abd5ea8 100644 --- a/src/Panel.js +++ b/src/Panel.js @@ -5,6 +5,10 @@ import MemberAdder from "./member/MemberAdder"; import MemberEditer from "./member/MemberEditer"; import MemberList from "./member/MemberList"; import ProductList from "./ProductList"; +import CreateInventary from "./inventary/CreateInventary"; +import InventaryList from "./inventary/InventaryList"; +import ShowInventary from "./inventary/ShowInventary"; +import CreateSupplier from "./inventary/CreateSupplier"; import Dashboard from "./Dashboard"; import OwnPassword from "./OwnPassword"; import Purchase from "./purchase/Purchase"; @@ -49,6 +53,18 @@ function LogedPanel(props) { <Route path="/products"> <ProductList /> </Route> + <Route path="/inventary/add"> + <CreateInventary /> + </Route> + <Route path="/inventary/:id"> + <ShowInventary /> + </Route> + <Route path="/inventary"> + <InventaryList /> + </Route> + <Route path="/supplier/add"> + <CreateSupplier /> + </Route> <Route path="/transaction/:id"> <ShowTransaction /> </Route> diff --git a/src/ProductPicker.js b/src/ProductPicker.js index 917dfbf..bf57421 100644 --- a/src/ProductPicker.js +++ b/src/ProductPicker.js @@ -24,6 +24,8 @@ class ProductPicker extends React.Component { let picks = this.props.picks; picks[index].amount = parseInt(amount); this.props.setPicks(picks); + // trick it to redraw on each change + this.setState({ code: this.state.code }); } setPrice(index, price) { @@ -48,12 +50,17 @@ class ProductPicker extends React.Component { return; } + let amount = 0; + if (this.props.amount) { + amount = 1; + } picks.push({ code: product.code, name: product.name, + origPrice: product.price, price: product.price, stock: product.stock, - amount: 1, + amount, }); this.props.setPicks(picks); this.setState({ code: "" }); @@ -86,12 +93,12 @@ class ProductPicker extends React.Component { printMoney(p.price) + "€" )} </Col> - {this.props.amount && ( + {(this.props.amount || this.props.stock) && ( <Col> <Form.Control type="number" - min="1" - max={p.stock} + min={this.props.amount ? "1" : Number.MIN_SAFE_INTEGER} + max={this.props.amount ? p.stock : Number.MAX_SAFE_INTEGER} placeholder="cantidad" value={p.amount} onChange={(e) => this.setAmount(i, e.target.value)} @@ -119,7 +126,7 @@ class ProductPicker extends React.Component { <Col> <h6>Precio</h6> </Col> - {this.props.amount && ( + {(this.props.amount || this.props.stock) && ( <Col> <h6>Cantidad</h6> </Col> @@ -146,7 +153,7 @@ class ProductPicker extends React.Component { /> </Col> <Col xs={false} sm></Col> - {this.props.amount && <Col xs={false} sm></Col>} + {(this.props.amount || this.props.stock) && <Col xs={false} sm></Col>} <Col xs={false} sm={1}></Col> </Form.Group> </Fetcher> diff --git a/src/inventary/CreateInventary.js b/src/inventary/CreateInventary.js new file mode 100644 index 0000000..bfc15c5 --- /dev/null +++ b/src/inventary/CreateInventary.js @@ -0,0 +1,92 @@ +import React, { useState } from "react"; +import { Redirect } from "react-router-dom"; +import { Form, Row, Col, Button } from "react-bootstrap"; +import ProductPicker from "../ProductPicker"; +import Fetcher from "../Fetcher"; +import Sender from "../Sender"; + +function supplierNameToId(suppliers, name) { + const found = suppliers.find((s) => s.name === name); + if (found) { + return found.ID; + } + return null; +} + +function CreateInventary() { + const [suppliers, setSuppliers] = useState([]); + const [comment, setComment] = useState(""); + const [supplier, setSupplier] = useState(null); + const [picks, setPicks] = useState([]); + const [redirect, setRedirect] = useState(null); + + if (redirect !== null) { + return <Redirect to={"/inventary/" + redirect.ID} />; + } + + const body = () => { + return { + comment, + supplier_id: supplierNameToId(suppliers, supplier), + products: picks.map((p) => { + return { + code: p.code, + price: p.price !== p.origPrice ? p.price : null, + stock: p.amount ? p.amount : null, + }; + }), + }; + }; + + const disabled = !picks; + + const supplierOptions = [<option key=""></option>].concat( + suppliers.map((s) => <option key={s.ID}>{s.name}</option>) + ); + + // TODO: debug it, it doesn't send correctly + // it doesnt load picks when they change... + return ( + <Sender url="/api/inventary" body={body} onSuccess={setRedirect}> + <Form.Group as={Row}> + <Form.Label as="legend" column sm={3}> + Proveedor + </Form.Label> + <Col sm={9}> + <Fetcher url="/api/supplier" onFetch={setSuppliers}> + <Form.Control + as="select" + onChange={(e) => setSupplier(e.target.value)} + > + {supplierOptions} + </Form.Control> + </Fetcher> + </Col> + </Form.Group> + <Form.Group as={Row}> + <Form.Label as="legend" column sm={3}> + Comentario + </Form.Label> + <Col sm={9}> + <Form.Control + placeholder="..." + value={comment} + onChange={(e) => setComment(e.target.value)} + /> + </Col> + </Form.Group> + <ProductPicker picks={picks} setPicks={setPicks} stock price /> + <Form.Group as={Row}> + <Col> + <div className="text-right"> + <Button type="submit" disabled={disabled}> + Añadir entrada + </Button> + </div> + </Col> + </Form.Group> + </Sender> + ); +} + +export default CreateInventary; diff --git a/src/inventary/CreateSupplier.js b/src/inventary/CreateSupplier.js new file mode 100644 index 0000000..037003e --- /dev/null +++ b/src/inventary/CreateSupplier.js @@ -0,0 +1,43 @@ +import React, { useState } from "react"; +import { Redirect } from "react-router-dom"; +import { Form, Row, Col, Button } from "react-bootstrap"; +import Sender from "../Sender"; + +function CreateSupplier() { + const [name, setName] = useState(""); + const [redirect, setRedirect] = useState(null); + + if (redirect !== null) { + return <Redirect to="/inventary/" />; + } + + const body = { name }; + + return ( + <Sender url="/api/supplier" body={body} onSuccess={setRedirect}> + <Form.Group as={Row}> + <Form.Label as="legend" column sm={3}> + Nombre + </Form.Label> + <Col sm={9}> + <Form.Control + placeholder="nombre" + value={name} + onChange={(e) => setName(e.target.value)} + /> + </Col> + </Form.Group> + <Form.Group as={Row}> + <Col> + <div className="text-right"> + <Button type="submit" disabled={!name}> + Añadir proveedor + </Button> + </div> + </Col> + </Form.Group> + </Sender> + ); +} + +export default CreateSupplier; diff --git a/src/inventary/InventaryList.js b/src/inventary/InventaryList.js new file mode 100644 index 0000000..23b51ec --- /dev/null +++ b/src/inventary/InventaryList.js @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import { Table, OverlayTrigger, Popover } from "react-bootstrap"; +import { LinkContainer } from "react-router-bootstrap"; +import Fetcher from "../Fetcher"; +import { printDate, printMoney, printInventaryID } from "../util"; + +function supplier(entry) { + if (entry.supplier) { + return entry.supplier.name; + } + return ""; +} + +function inventaryOverlay(entry) { + const content = entry.products.map((p) => { + const stock = p.stock ? p.stock : ""; + const price = p.price !== null ? printMoney(p.price) : ""; + + return ( + <div key={"I" + entry.ID + "-" + p.ID}> + {p.product.name + ": " + stock + " " + price} + <br /> + </div> + ); + }); + + return ( + <Popover> + <Popover.Title>{supplier(entry)}</Popover.Title> + <Popover.Content>{content}</Popover.Content> + </Popover> + ); +} + +function InventaryList() { + const [inventary, setInventary] = useState([]); + + const entries = inventary.map((i) => ( + <OverlayTrigger key={i.ID} overlay={inventaryOverlay(i)}> + <LinkContainer to={"/inventary/" + i.ID}> + <tr> + <td>{printInventaryID(i)}</td> + <td>{printDate(i.date)}</td> + <td>{i.member.name}</td> + <td>{supplier(i)}</td> + <td>{i.comment}</td> + </tr> + </LinkContainer> + </OverlayTrigger> + )); + + return ( + <Fetcher url="/api/inventary" onFetch={setInventary}> + <Table className="text-center" responsive> + <thead> + <tr> + <th>ID</th> + <th>Fecha</th> + <th>Por</th> + <th>Proveedor</th> + <th>Comentario</th> + </tr> + </thead> + <tbody>{entries}</tbody> + </Table> + </Fetcher> + ); +} + +export default InventaryList; diff --git a/src/inventary/ShowInventary.js b/src/inventary/ShowInventary.js new file mode 100644 index 0000000..0e650f0 --- /dev/null +++ b/src/inventary/ShowInventary.js @@ -0,0 +1,46 @@ +import React, { useState } from "react"; +import { useParams } from "react-router-dom"; +import { Row, Col, Table } from "react-bootstrap"; +import Fetcher from "../Fetcher"; +import { printMoney, printDate, printInventaryID } from "../util"; + +function ShowInventary() { + const { id } = useParams(); + const [entry, setEntry] = useState({ ID: 0, products: [] }); + + const products = entry.products.map((p) => ( + <tr> + <td>{p.product.name}</td> + <td>{p.stock}</td> + <td>{p.price !== null ? printMoney(p.price) : ""}</td> + </tr> + )); + + return ( + <Fetcher url={"/api/inventary/" + id} onFetch={setEntry}> + <Row> + <Col>{entry.supplier && <h3>{entry.supplier.name}</h3>}</Col> + <Col> + <p className="text-right"> + {printDate(entry.date)} {printInventaryID(entry)} + <br /> + {entry.member && entry.member.name} + </p> + </Col> + </Row> + <p>{entry.comment}</p> + <Table striped bordered hover responsive> + <thead> + <tr> + <th>producto</th> + <th>stock</th> + <th>precio</th> + </tr> + </thead> + <tbody>{products}</tbody> + </Table> + </Fetcher> + ); +} + +export default ShowInventary; diff --git a/src/util.js b/src/util.js index fa03340..f191089 100644 --- a/src/util.js +++ b/src/util.js @@ -23,7 +23,19 @@ function printRole(role) { } function printTransactionID(transaction) { - return "T-" + transaction.ID.toString().padStart(5, "0"); + return printID("T", transaction); +} + +function printInventaryID(inventary) { + return printID("I", inventary); +} + +function printSupplierID(supplier) { + return printID("S", supplier); +} + +function printID(pre, item) { + return pre + "-" + item.ID.toString().padStart(5, "0"); } function url(path) { @@ -57,4 +69,6 @@ export { time2string, daysAfterNow, printTransactionID, + printInventaryID, + printSupplierID, }; -- GitLab