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>
+          &nbsp;
+          <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