From c2aee14c0e3cc1fc7f4c0b0227892dd3e298c2c3 Mon Sep 17 00:00:00 2001 From: meskio <meskio@sindominio.net> Date: Wed, 23 Dec 2020 17:24:29 +0100 Subject: [PATCH] Clean up member admin management Add support for order permitions --- src/Fetcher.js | 13 +-- src/Head.js | 8 +- src/MemberAdder.js | 144 ----------------------------- src/MemberList.js | 150 ------------------------------- src/Panel.js | 11 ++- src/ProductList.js | 2 +- src/Topup.js | 7 +- src/member/MemberAdder.js | 37 ++++++++ src/member/MemberEditer.js | 121 +++++++++++++++++++++++++ src/member/MemberForm.js | 79 ++++++++++++++++ src/member/MemberList.js | 52 +++++++++++ src/{ => member}/MemberPicker.js | 7 +- src/purchase/Purchase.js | 2 +- src/util.js | 13 ++- 14 files changed, 334 insertions(+), 312 deletions(-) delete mode 100644 src/MemberAdder.js delete mode 100644 src/MemberList.js create mode 100644 src/member/MemberAdder.js create mode 100644 src/member/MemberEditer.js create mode 100644 src/member/MemberForm.js create mode 100644 src/member/MemberList.js rename src/{ => member}/MemberPicker.js (90%) diff --git a/src/Fetcher.js b/src/Fetcher.js index 555363d..87e3383 100644 --- a/src/Fetcher.js +++ b/src/Fetcher.js @@ -9,18 +9,19 @@ class Fetcher extends React.Component { constructor(props) { super(props); this.state = { - isLoading: false, + isLoading: true, error: null, }; } componentDidMount() { - this.setState({ isLoading: true }); this.fetch(); - this.timerID = setInterval( - () => this.fetch(), - 10000 // every 10 seconds - ); + if (!this.props.oneShot) { + this.timerID = setInterval( + () => this.fetch(), + 10000 // every 10 seconds + ); + } } compomentWillUnmount() { diff --git a/src/Head.js b/src/Head.js index b58f1d9..8c40792 100644 --- a/src/Head.js +++ b/src/Head.js @@ -50,9 +50,11 @@ function Head(props) { <LinkContainer to="/purchase"> <Nav.Link>Comprar</Nav.Link> </LinkContainer> - <LinkContainer to="/order/create"> - <Nav.Link>Abrir pedido</Nav.Link> - </LinkContainer> + {(auth.role === "order" || auth.role === "admin") && ( + <LinkContainer to="/order/create"> + <Nav.Link>Abrir pedido</Nav.Link> + </LinkContainer> + )} </Nav> <Nav className="ml-auto" activeKey={location.pathname}> diff --git a/src/MemberAdder.js b/src/MemberAdder.js deleted file mode 100644 index fc2c041..0000000 --- a/src/MemberAdder.js +++ /dev/null @@ -1,144 +0,0 @@ -import React from "react"; -import { Redirect } from "react-router-dom"; -import { Form, Button, InputGroup } from "react-bootstrap"; -import Sender from "./Sender"; - -class MemberAdder extends React.Component { - constructor(props) { - super(props); - this.state = { - num: null, - login: null, - name: null, - email: null, - phone: null, - password: null, - euros: 0, - cents: 0, - admin: false, - redirect: false, - }; - } - - render() { - if (this.state.redirect) { - return <Redirect to="/members" />; - } - - const invalid = - !this.state.num || !this.state.login || !this.state.password; - - const body = () => { - return { - num: parseInt(this.state.num), - login: this.state.login, - name: this.state.name, - email: this.state.email, - phone: this.state.phone, - password: this.state.password, - balance: parseInt(this.state.euros) * 100 + parseInt(this.state.cents), - role: this.state.admin ? "admin" : "member", - }; - }; - - return ( - <Sender - url="/api/member" - body={body} - onSuccess={() => this.setState({ redirect: true })} - > - <Form.Group> - <Form.Label>Numero de socia:</Form.Label> - <Form.Control - type="number" - placeholder="numero" - value={this.state.num} - onChange={(e) => this.setState({ num: e.target.value })} - isInvalid={!this.state.num} - /> - </Form.Group> - <Form.Group> - <Form.Label>Nombre de acceso:</Form.Label> - <Form.Control - placeholder="login" - value={this.state.login} - onChange={(e) => this.setState({ login: e.target.value })} - isInvalid={!this.state.login} - /> - </Form.Group> - <Form.Group> - <Form.Label>Nombre:</Form.Label> - <Form.Control - placeholder="nombre" - value={this.state.name} - onChange={(e) => this.setState({ name: e.target.value })} - /> - </Form.Group> - <Form.Group> - <Form.Label>Email:</Form.Label> - <Form.Control - type="email" - placeholder="email" - value={this.state.email} - onChange={(e) => this.setState({ email: e.target.value })} - /> - </Form.Group> - <Form.Group> - <Form.Label>Telefono:</Form.Label> - <Form.Control - placeholder="telefono" - value={this.state.phone} - onChange={(e) => this.setState({ phone: e.target.value })} - /> - </Form.Group> - <Form.Group> - <Form.Label>Contraseña:</Form.Label> - <Form.Control - type="password" - placeholder="contraseña" - value={this.state.password} - onChange={(e) => this.setState({ password: e.target.value })} - isInvalid={!this.state.password} - /> - </Form.Group> - <Form.Group> - <Form.Check - type="switch" - id="admin" - label="Es administradora" - checked={this.state.admin} - onChange={(e) => this.setState({ admin: e.target.checked })} - /> - </Form.Group> - <Form.Group> - <Form.Label>Saldo inicial:</Form.Label> - <InputGroup> - <Form.Control - placeholder="euros" - value={this.state.euros} - onChange={(e) => this.setState({ euros: e.target.value })} - /> - <InputGroup.Append> - <InputGroup.Text>.</InputGroup.Text> - </InputGroup.Append> - <Form.Control - placeholder="centimos" - value={this.state.cents} - onChange={(e) => this.setState({ cents: e.target.value })} - min="0" - max="99" - /> - <InputGroup.Append> - <InputGroup.Text>€</InputGroup.Text> - </InputGroup.Append> - </InputGroup> - </Form.Group> - <Button type="submit" disabled={invalid}> - Crear usuaria - </Button> - </Sender> - ); - } -} - -export default MemberAdder; diff --git a/src/MemberList.js b/src/MemberList.js deleted file mode 100644 index be1be7c..0000000 --- a/src/MemberList.js +++ /dev/null @@ -1,150 +0,0 @@ -import React from "react"; -import { Table, Alert, Form, Button } from "react-bootstrap"; -import Fetcher from "./Fetcher"; -import EditableCell from "./EditableCell"; -import AuthContext from "./AuthContext"; -import { printMoney, url } from "./util"; - -class MemberList extends React.Component { - static contextType = AuthContext; - - constructor(props) { - super(props); - this.state = { - members: [], - error: null, - }; - } - - update(row, key, value) { - this.setState({ error: null }); - - let members = this.state.members; - const num = members[row].num; - - if (key === "num") { - value = parseInt(value); - } - members[row][key] = value; - this.setState({ members }); - - let update = {}; - update[key] = value; - const body = JSON.stringify(update); - fetch(url("/api/member/" + num), { - headers: { "x-authentication": this.context.token }, - method: "PUT", - body, - }).then((response) => { - if (!response.ok) { - this.setState({ - error: response.status.toString() + " " + response.statusText, - }); - } - }); - } - - setAdmin(row, checked) { - const role = checked ? "admin" : "member"; - this.update(row, "role", role); - } - - delMember(num) { - let members = this.state.members; - const index = members.findIndex((m) => m.num === num); - members.splice(index, 1); - this.setState({ members }); - - fetch(url("/api/member/" + num), { - headers: { "x-authentication": this.context.token }, - method: "DELETE", - }).then((response) => { - if (!response.ok) { - this.setState({ - error: response.status.toString() + " " + response.statusText, - }); - } - }); - } - - render() { - let alert = null; - if (this.state.error !== null) { - alert = ( - <Alert variant="danger"> - Ha ocurrido un error enviando cambios: {this.state.error} - </Alert> - ); - } - - const entries = this.state.members.map((member, row) => { - return ( - <tr key={member.num}> - <EditableCell - onChange={(v) => this.update(row, "num", v)} - value={member.num} - /> - <EditableCell - onChange={(v) => this.update(row, "login", v)} - value={member.login} - /> - <EditableCell - onChange={(v) => this.update(row, "name", v)} - value={member.name} - /> - <EditableCell - onChange={(v) => this.update(row, "email", v)} - value={member.email} - /> - <EditableCell - onChange={(v) => this.update(row, "phone", v)} - value={member.phone} - /> - <td>{printMoney(member.balance)}€</td> - <td sm={1}> - <Form> - <Form.Check - type="switch" - id={"admin-" + member.num} - label="" - checked={member.role === "admin"} - onChange={(e) => this.setAdmin(row, e.target.checked)} - /> - </Form> - </td> - <td sm={1}> - <Button variant="danger" onClick={() => this.delMember(member.num)}> - - - </Button> - </td> - </tr> - ); - }); - - return ( - <Fetcher - url="/api/member" - onFetch={(members) => this.setState({ members })} - > - {alert} - <Table striped bordered hover> - <thead> - <tr> - <th></th> - <th>Login</th> - <th>Nombre</th> - <th>Email</th> - <th>Telefono</th> - <th>Saldo</th> - <th sm={1}>Admin</th> - <th sm={1}></th> - </tr> - </thead> - <tbody>{entries}</tbody> - </Table> - </Fetcher> - ); - } -} - -export default MemberList; diff --git a/src/Panel.js b/src/Panel.js index 2527c72..37fa783 100644 --- a/src/Panel.js +++ b/src/Panel.js @@ -1,8 +1,9 @@ import React from "react"; import { BrowserRouter, Switch, Route } from "react-router-dom"; import { Container, Row } from "react-bootstrap"; -import MemberAdder from "./MemberAdder"; -import MemberList from "./MemberList"; +import MemberAdder from "./member/MemberAdder"; +import MemberEditer from "./member/MemberEditer"; +import MemberList from "./member/MemberList"; import ProductList from "./ProductList"; import Dashboard from "./Dashboard"; import OwnPassword from "./OwnPassword"; @@ -34,6 +35,9 @@ function LogedPanel(props) { <Route path="/members/add"> <MemberAdder /> </Route> + <Route path="/member/:num"> + <MemberEditer /> + </Route> <Route path="/members/purchase"> <Purchase member /> </Route> @@ -52,6 +56,9 @@ function LogedPanel(props) { <Route path="/purchase"> <Purchase /> </Route> + <Route path="/topup/:num"> + <Topup /> + </Route> <Route path="/topup"> <Topup /> </Route> diff --git a/src/ProductList.js b/src/ProductList.js index 2636fbf..d8aeeb0 100644 --- a/src/ProductList.js +++ b/src/ProductList.js @@ -143,7 +143,7 @@ class ProductList extends React.Component { onFetch={(products) => this.setState({ products })} > {alert} - <Table striped bordered hover> + <Table striped bordered hover responsive> <thead> <tr> <th>codigo</th> diff --git a/src/Topup.js b/src/Topup.js index 2351e92..6614acd 100644 --- a/src/Topup.js +++ b/src/Topup.js @@ -1,10 +1,11 @@ import React, { useState } from "react"; -import { Redirect } from "react-router-dom"; -import MemberPicker from "./MemberPicker"; +import { useParams, Redirect } from "react-router-dom"; +import MemberPicker from "./member/MemberPicker"; import { Form, Col, Row, Button, InputGroup } from "react-bootstrap"; import Sender from "./Sender"; function Topup() { + const { num } = useParams(); const [member, setMember] = useState(null); const [amount, setAmount] = useState(0); const [comment, setComment] = useState(""); @@ -27,7 +28,7 @@ function Topup() { return ( <Sender url="/api/topup" body={body} onSuccess={setTransaction}> - <MemberPicker member={member} onChange={setMember} /> + <MemberPicker member={member} onChange={setMember} num={parseInt(num)} /> <Form.Group as={Row}> <Form.Label as="legend" column sm={2}> Recarga diff --git a/src/member/MemberAdder.js b/src/member/MemberAdder.js new file mode 100644 index 0000000..5644715 --- /dev/null +++ b/src/member/MemberAdder.js @@ -0,0 +1,37 @@ +import React, { useState } from "react"; +import { Button, Alert } from "react-bootstrap"; +import MemberForm from "./MemberForm"; +import Sender from "../Sender"; + +function MemberAdder() { + const [member, setMember] = useState({ + num: "", + name: "", + email: "", + phone: "", + role: "member", + }); + const [success, setSuccess] = useState(false); + + if (success) { + return ( + <Alert variant="success"> + {member.num} ya es parte del garbanzo, recibirá un email con un enlace + para poner su contraseña y nombre de acceso. + </Alert> + ); + } + + const invalid = !member.num || !member.name || !member.email; + + return ( + <Sender url="/api/member" body={member} onSuccess={() => setSuccess(true)}> + <MemberForm value={member} onChange={setMember} /> + <Button type="submit" disabled={invalid}> + Crear socia + </Button> + </Sender> + ); +} + +export default MemberAdder; diff --git a/src/member/MemberEditer.js b/src/member/MemberEditer.js new file mode 100644 index 0000000..d0b325a --- /dev/null +++ b/src/member/MemberEditer.js @@ -0,0 +1,121 @@ +import React, { useState, useContext } from "react"; +import { Redirect, useParams } from "react-router-dom"; +import { LinkContainer } from "react-router-bootstrap"; +import { Form, Col, Spinner, Alert, Button, Modal } from "react-bootstrap"; +import MemberForm from "./MemberForm"; +import Sender from "../Sender"; +import Fetcher from "../Fetcher"; +import AuthContext from "../AuthContext"; +import { url } from "../util"; + +function MemberEditer() { + const { num } = useParams(); + const auth = useContext(AuthContext); + const [member, setMember] = useState({ + num: "", + name: "", + email: "", + phone: "", + role: "member", + }); + const [error, setError] = useState(""); + const [showDelete, setShowDelete] = useState(false); + const [loading, setLoading] = useState(false); + const [redirect, setRedirect] = useState(false); + + const setMembers = (members) => { + const mem = members.find((m) => m.num === parseInt(num)); + if (!mem) { + setError("Numero de socia invalido"); + } else { + setMember({ + num: mem.num, + name: mem.name, + email: mem.email, + phone: mem.phone, + role: mem.role, + }); + } + }; + + const handleClose = () => setShowDelete(false); + + const delMember = () => { + setLoading(true); + fetch(url("/api/member/" + num), { + headers: { "x-authentication": auth.token }, + method: "DELETE", + }).then((response) => { + if (!response.ok) { + setError( + "No pudo eliminar la socia: " + + response.status.toString() + + " " + + response.statusText + ); + } else { + setRedirect(true); + } + }); + }; + + if (redirect) { + return <Redirect to="/members" />; + } + + if (loading) { + return <Spinner animation="border" />; + } + + if (error) { + return <Alert variant="danger">{error}</Alert>; + } + + const invalid = !member.num || !member.name || !member.email; + + return ( + <Fetcher url="/api/member" onFetch={setMembers} oneShot> + <Sender + url={"/api/member/" + num} + method="PUT" + body={member} + onSuccess={() => setRedirect(true)} + > + <MemberForm value={member} onChange={setMember} /> + <Form.Row> + <Button type="submit" disabled={invalid}> + Guardar + </Button> + + <LinkContainer to="/members"> + <Button variant="secondary">Cancelar</Button> + </LinkContainer> + <Col className="text-right"> + <Button variant="danger" onClick={() => setShowDelete(true)}> + Eliminar + </Button> + </Col> + </Form.Row> + + <Modal show={showDelete} onHide={handleClose}> + <Modal.Header closeButton> + <Modal.Title>Confirmar la elminicacion</Modal.Title> + </Modal.Header> + <Modal.Body> + ¿Borrar la cuenta permanentemente la cuenta de {member.name}? + </Modal.Body> + <Modal.Footer> + <Button variant="secondary" onClick={handleClose}> + Cancelar + </Button> + <Button variant="danger" onClick={delMember}> + Eliminar + </Button> + </Modal.Footer> + </Modal> + </Sender> + </Fetcher> + ); +} + +export default MemberEditer; diff --git a/src/member/MemberForm.js b/src/member/MemberForm.js new file mode 100644 index 0000000..0ac1a77 --- /dev/null +++ b/src/member/MemberForm.js @@ -0,0 +1,79 @@ +import React from "react"; +import { Form } from "react-bootstrap"; +import { printRole } from "../util"; + +function toRole(roleStr) { + switch (roleStr) { + case "admin": + return "admin"; + case "pedidos": + return "order"; + default: + return "member"; + } +} + +function MemberForm(props) { + const member = props.value; + + const set = (key, value) => { + let m = { ...member }; + m[key] = value; + props.onChange(m); + }; + + return ( + <div> + <Form.Group> + <Form.Label>Numero de socia:</Form.Label> + <Form.Control + type="number" + placeholder="numero" + value={member.num} + onChange={(e) => set("num", parseInt(e.target.value))} + isInvalid={!member.num} + /> + </Form.Group> + <Form.Group> + <Form.Label>Nombre:</Form.Label> + <Form.Control + placeholder="nombre" + value={member.name} + onChange={(e) => set("name", e.target.value)} + /> + </Form.Group> + <Form.Group> + <Form.Label>Email:</Form.Label> + <Form.Control + type="email" + placeholder="email" + value={member.email} + onChange={(e) => set("email", e.target.value)} + isInvalid={!member.email} + /> + </Form.Group> + <Form.Group> + <Form.Label>Telefono:</Form.Label> + <Form.Control + placeholder="telefono" + value={member.phone} + onChange={(e) => set("phone", e.target.value)} + /> + </Form.Group> + <Form.Group> + <Form.Label>Rol:</Form.Label> + <Form.Control + as="select" + value={printRole(member.role)} + onChange={(e) => set("role", toRole(e.target.value))} + > + <option>socia</option> + <option>pedidos</option> + <option>admin</option> + </Form.Control> + </Form.Group> + </div> + ); +} + +export default MemberForm; diff --git a/src/member/MemberList.js b/src/member/MemberList.js new file mode 100644 index 0000000..eaf0803 --- /dev/null +++ b/src/member/MemberList.js @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { Table } from "react-bootstrap"; +import { LinkContainer } from "react-router-bootstrap"; +import { GiPayMoney } from "react-icons/gi"; +import Fetcher from "../Fetcher"; +import { printMoney, printRole } from "../util"; + +function MemberList() { + const [members, setMembers] = useState([]); + + const entries = members.map((member) => { + return ( + <LinkContainer to={"/member/" + member.num}> + <tr key={member.num}> + <td>{member.num}</td> + <td>{member.name}</td> + <td>{member.login}</td> + <td>{member.email}</td> + <td>{member.phone}</td> + <td>{printRole(member.role)}</td> + <td>{printMoney(member.balance)}€</td> + <td md={1}> + <LinkContainer to={"/topup/" + member.num}> + <GiPayMoney /> + </LinkContainer> + </td> + </tr> + </LinkContainer> + ); + }); + + return ( + <Fetcher url="/api/member" onFetch={setMembers}> + <Table striped bordered hover responsive> + <thead> + <tr> + <th></th> + <th>Nombre</th> + <th>Login</th> + <th>Email</th> + <th>Telefono</th> + <th>Rol</th> + <th>Saldo</th> + </tr> + </thead> + <tbody>{entries}</tbody> + </Table> + </Fetcher> + ); +} + +export default MemberList; diff --git a/src/MemberPicker.js b/src/member/MemberPicker.js similarity index 90% rename from src/MemberPicker.js rename to src/member/MemberPicker.js index c5ddb50..2bba414 100644 --- a/src/MemberPicker.js +++ b/src/member/MemberPicker.js @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { Form, Col } from "react-bootstrap"; import { Typeahead } from "react-bootstrap-typeahead"; -import Fetcher from "./Fetcher"; +import Fetcher from "../Fetcher"; function MemberPicker(props) { const [members, setStateMembers] = useState([]); @@ -13,6 +13,11 @@ function MemberPicker(props) { return m; }); setStateMembers(newMembers); + + if (!props.member && props.num) { + const member = newMembers.find((m) => m.num === props.num); + props.onChange(member); + } }; return ( diff --git a/src/purchase/Purchase.js b/src/purchase/Purchase.js index 3527985..be07694 100644 --- a/src/purchase/Purchase.js +++ b/src/purchase/Purchase.js @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Redirect } from "react-router-dom"; import { Row, Col, Button, Alert } from "react-bootstrap"; import ProductPicker from "../ProductPicker"; -import MemberPicker from "../MemberPicker"; +import MemberPicker from "../member/MemberPicker"; import Sender from "../Sender"; import { printMoney } from "../util"; diff --git a/src/util.js b/src/util.js index 2e19010..0268523 100644 --- a/src/util.js +++ b/src/util.js @@ -6,6 +6,17 @@ function printDate(date) { return new Date(date).toLocaleDateString(); } +function printRole(role) { + switch (role) { + case "admin": + return "admin"; + case "order": + return "pedidos"; + default: + return "socia"; + } +} + function url(path) { let api = process.env.REACT_APP_API; if (!api) { @@ -14,4 +25,4 @@ function url(path) { return api + path; } -export { printMoney, printDate, url }; +export { printMoney, printDate, printRole, url }; -- GitLab