diff --git a/api/db/member.go b/api/db/member.go index 78e6a046b775cbdf6205115b995aa90bc7c68a87..cbce9549248680f01ede90afe284412064ce3afc 100644 --- a/api/db/member.go +++ b/api/db/member.go @@ -128,6 +128,7 @@ func (d DB) Login(login, password string) (member Member, err error) { cleanedLogin := cleanLogin(login) err = d.db.Where("email = ?", cleanedLogin). First(&member).Error + if err != nil { err = d.db.Where("login = ?", cleanedLogin). First(&member).Error diff --git a/api/db/transaction.go b/api/db/transaction.go index bf597571d6a5ece10fd4f93bb6ed0324bce289ca..d4d56b23565fb393b9a6735d9b642af403d704ae 100644 --- a/api/db/transaction.go +++ b/api/db/transaction.go @@ -43,9 +43,36 @@ type Purchase struct { Amount int `json:"amount"` } -func (d *DB) ListTransactions() (transactions []Transaction, err error) { - err = d.transactionQuery(). - Order("date desc"). +func (d *DB) ListTransactions(query map[string][]string) (transactions []Transaction, err error) { + const dateLayout = "2006-01-02" + tx := d.transactionQuery() + for k, v := range query { + switch k { + case "start-date": + var date time.Time + date, err = time.Parse(dateLayout, v[0]) + if err != nil { + return + } + tx = tx.Where("date >= ?", date) + case "end-date": + var date time.Time + date, err = time.Parse(dateLayout, v[0]) + if err != nil { + return + } + tx = tx.Where("date <= ?", date) + case "member": + tx = tx.Where("member_num in ?", v) + case "proxy": + tx = tx.Where("proxy_num in ?", v) + case "type": + tx = tx.Where("type in ?", v) + default: + log.Printf("Unexpected transaction query: %s %v", k, v) + } + } + err = tx.Order("date desc"). Find(&transactions).Error return } diff --git a/api/transaction.go b/api/transaction.go index bd5d2c461aa80e10c13e10406e823f67e5069206..c70c31ce475392c2257f2cfc3a601f9d23d1edc4 100644 --- a/api/transaction.go +++ b/api/transaction.go @@ -12,7 +12,14 @@ import ( ) func (a *api) ListTransactions(w http.ResponseWriter, req *http.Request) { - transactions, err := a.db.ListTransactions() + err := req.ParseForm() + if err != nil { + log.Printf("Can't parse the query: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + transactions, err := a.db.ListTransactions(req.Form) if err != nil { log.Printf("Can't list transactions: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/src/Dashboard.js b/src/Dashboard.js index ff4bf76a1bc0b68578275f39128955d363531f13..ac17ef6ac66dab91f000ab7c94be544f4a99f957 100644 --- a/src/Dashboard.js +++ b/src/Dashboard.js @@ -4,7 +4,7 @@ import { LinkContainer } from "react-router-bootstrap"; import AuthContext from "./AuthContext"; import Fetcher from "./Fetcher"; import OrderList from "./order/OrderList"; -import TransactionList from "./TransactionList"; +import MyTransactions from "./transaction/MyTransactions"; import { printMoney } from "./util"; class Dashboard extends React.Component { @@ -41,7 +41,7 @@ class Dashboard extends React.Component { </Col> </Row> <OrderList /> - <TransactionList /> + <MyTransactions /> </Fetcher> ); } diff --git a/src/Head.js b/src/Head.js index 8c40792d6d773f691902ed92753061a887da52a6..c05aba48445a142d5d4eddad9f9e00d01e1b8ed1 100644 --- a/src/Head.js +++ b/src/Head.js @@ -25,6 +25,9 @@ function Head(props) { <LinkContainer to="/members/add"> <NavDropdown.Item>Nueva socia</NavDropdown.Item> </LinkContainer> + <LinkContainer to="/transaction"> + <NavDropdown.Item>Transacciones</NavDropdown.Item> + </LinkContainer> </NavDropdown> ); } diff --git a/src/Panel.js b/src/Panel.js index b1aaaea5e4cd5f639c929e9cfd62cfce7d481193..8dc728d11612d19ffc058d5ff6c10ca0f8834ace 100644 --- a/src/Panel.js +++ b/src/Panel.js @@ -9,7 +9,8 @@ import Dashboard from "./Dashboard"; import OwnPassword from "./OwnPassword"; import Purchase from "./purchase/Purchase"; import Topup from "./Topup"; -import ShowTransaction from "./ShowTransaction"; +import ShowTransaction from "./transaction/ShowTransaction"; +import TransactionList from "./transaction/TransactionList"; import ShowOrder from "./order/ShowOrder"; import CreateOrder from "./order/CreateOrder"; import EditOrder from "./order/EditOrder"; @@ -51,6 +52,9 @@ function LogedPanel(props) { <Route path="/transaction/:id"> <ShowTransaction /> </Route> + <Route path="/transaction"> + <TransactionList /> + </Route> <Route path="/password"> <OwnPassword /> </Route> diff --git a/src/TransactionList.js b/src/TransactionList.js deleted file mode 100644 index 45aa3cbbf46184ca6f9b9dbbb797f28841d4ce27..0000000000000000000000000000000000000000 --- a/src/TransactionList.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState } from "react"; -import { Table, OverlayTrigger, Popover } from "react-bootstrap"; -import { LinkContainer } from "react-router-bootstrap"; -import { FaShoppingBasket, FaMoneyBillAlt } from "react-icons/fa"; -import { GiPayMoney, GiReceiveMoney } from "react-icons/gi"; -import { HiClipboardCopy, HiClipboardList } from "react-icons/hi"; -import Fetcher from "./Fetcher"; -import { printMoney, printDate } from "./util"; - -function icon(transaction) { - switch (transaction.type) { - case "purchase": - return <FaShoppingBasket />; - case "topup": - if (transaction.total < 0) { - return <GiReceiveMoney />; - } - return <GiPayMoney />; - case "order": - return <HiClipboardList />; - case "refund": - return <HiClipboardCopy />; - default: - return <FaMoneyBillAlt />; - } -} - -function transactionOverlay(transaction) { - let title; - let content; - switch (transaction.type) { - case "purchase": - title = "compra"; - content = transaction.purchase.map((p) => p.product.name).join(",") + "."; - break; - case "topup": - if (transaction.total < 0) { - title = "devolución"; - } else { - title = "recarga"; - } - content = transaction.topup.comment; - break; - case "order": - title = "pedido de " + transaction.order.name; - content = transaction.order_purchase.map((p) => { - return ( - <div key={"O" + transaction.ID + "-" + p.order_product.ID}> - {p.order_product.product.name + ": " + p.amount} - <br /> - </div> - ); - }); - break; - case "refund": - title = "devolución de " + transaction.refund.name; - break; - default: - title = "transacción"; - } - return ( - <Popover> - <Popover.Title>{title}</Popover.Title> - {content && <Popover.Content>{content}</Popover.Content>} - </Popover> - ); -} - -function TransactionList() { - const [transactions, setTransactions] = useState([]); - - // TODO: useEffect to disable fetcher... - - const entries = transactions.map((transaction) => { - const colorClass = transaction.total < 0 ? "table-danger" : "table-success"; - return ( - <OverlayTrigger - overlay={transactionOverlay(transaction)} - key={transaction.ID} - > - <LinkContainer to={"/transaction/" + transaction.ID}> - <tr className={colorClass}> - <td>{icon(transaction)}</td> - <td>{printDate(transaction.date)}</td> - <td>{printMoney(transaction.total) + " €"}</td> - </tr> - </LinkContainer> - </OverlayTrigger> - ); - }); - - return ( - <Fetcher url="/api/transaction/mine" onFetch={setTransactions}> - <Table className="text-center"> - <thead> - <tr> - <th></th> - <th>Fecha</th> - <th>Cantidad</th> - </tr> - </thead> - <tbody>{entries}</tbody> - </Table> - </Fetcher> - ); -} - -export default TransactionList; diff --git a/src/member/MemberPicker.js b/src/member/MemberPicker.js index 6b714ae9e783432ee8005ecc3933088e16834a21..c45bff2112c2fe8455456834994dda11d1b60305 100644 --- a/src/member/MemberPicker.js +++ b/src/member/MemberPicker.js @@ -23,12 +23,14 @@ function MemberPicker(props) { } }; + const text = props.text ? props.text : "Socia"; + return ( <Fetcher url="/api/member" onFetch={setMembers}> <Form.Row> <Col sm={4}> <br /> - <h4 className="text-right">Socia:</h4> + <h4 className="text-right">{text}:</h4> </Col> <Form.Group as={Col} sm={2}> <Form.Label>Num</Form.Label> diff --git a/src/order/OrderEditor.js b/src/order/OrderEditor.js index 3484dde9942c5aeae085b5151e86c43348868827..f4c5e08ae9d931b43b993a36f621be85bac8e844 100644 --- a/src/order/OrderEditor.js +++ b/src/order/OrderEditor.js @@ -2,16 +2,7 @@ import React, { useState } from "react"; import { Form, Row, Col } from "react-bootstrap"; import ProductPicker from "../ProductPicker"; import Fetcher from "../Fetcher"; - -function date2string(date) { - return date.toISOString().split("T")[0]; -} - -function daysAfterNow(days) { - let date = new Date(); - date.setDate(date.getDate() + days); - return date; -} +import { daysAfterNow, date2string } from "../util"; function order2picks(order) { return order.products.map((p) => { diff --git a/src/transaction/MyTransactions.js b/src/transaction/MyTransactions.js new file mode 100644 index 0000000000000000000000000000000000000000..1b0494496388e00b12e1d55fdcaf3134d809c41b --- /dev/null +++ b/src/transaction/MyTransactions.js @@ -0,0 +1,37 @@ +import React, { useState } from "react"; +import { Table } from "react-bootstrap"; +import icon from "./icon"; +import TransactionTr from "./TransactionTr"; +import Fetcher from "../Fetcher"; +import { printMoney, printDate } from "../util"; + +function MyTransactions() { + const [transactions, setTransactions] = useState([]); + + const entries = transactions.map((transaction) => { + return ( + <TransactionTr transaction={transaction}> + <td>{icon(transaction)}</td> + <td>{printDate(transaction.date)}</td> + <td>{printMoney(transaction.total) + " €"}</td> + </TransactionTr> + ); + }); + + return ( + <Fetcher url="/api/transaction/mine" onFetch={setTransactions}> + <Table className="text-center"> + <thead> + <tr> + <th></th> + <th>Fecha</th> + <th>Cantidad</th> + </tr> + </thead> + <tbody>{entries}</tbody> + </Table> + </Fetcher> + ); +} + +export default MyTransactions; diff --git a/src/ShowTransaction.js b/src/transaction/ShowTransaction.js similarity index 81% rename from src/ShowTransaction.js rename to src/transaction/ShowTransaction.js index 22dcebe241bbe632422268d0c060e580b0bd923a..569df30d12e7bbc74565f2b6027e04376468c74b 100644 --- a/src/ShowTransaction.js +++ b/src/transaction/ShowTransaction.js @@ -1,9 +1,9 @@ import React, { useState } from "react"; import { useParams, Redirect } from "react-router-dom"; import { Row, Col } from "react-bootstrap"; -import Fetcher from "./Fetcher"; -import ShowPurchase from "./purchase/ShowPurchase"; -import { printMoney, printDate } from "./util"; +import Fetcher from "../Fetcher"; +import ShowPurchase from "../purchase/ShowPurchase"; +import { printMoney, printDate } from "../util"; function ShowTransaction() { const { id } = useParams(); @@ -31,10 +31,11 @@ function ShowTransaction() { <Row> <Col> <h3>Total: {printMoney(transaction.total)}€</h3> + {transaction.member && <p>{transaction.member.name}</p>} </Col> <Col> <p className="text-right"> - {printDate(transaction.date)} + {printDate(transaction.date)} {"T-" + transaction.ID} {transaction.proxy && ( <span> <br /> diff --git a/src/transaction/TransactionList.js b/src/transaction/TransactionList.js new file mode 100644 index 0000000000000000000000000000000000000000..83c458501d8288270384c2a88bb23dc8faf20b19 --- /dev/null +++ b/src/transaction/TransactionList.js @@ -0,0 +1,112 @@ +import React, { useState } from "react"; +import { Table, Form, Row, Col } from "react-bootstrap"; +import icon from "./icon"; +import TransactionTr from "./TransactionTr"; +import MemberPicker from "../member/MemberPicker"; +import Fetcher from "../Fetcher"; +import { printMoney, printDate, daysAfterNow, date2string } from "../util"; + +const engType = { + compra: "purchase", + recarga: "topup", + pedido: "order", + devolucion: "refund", +}; + +function TransactionList() { + const [transactions, setTransactions] = useState([]); + const [startDate, setStartDate] = useState(date2string(daysAfterNow(-30))); + const [endDate, setEndDate] = useState(date2string(new Date())); + const [member, setMember] = useState(null); + const [proxy, setProxy] = useState(null); + const [types, setTypes] = useState([]); + + let query = "start-date=" + startDate + "&end-date=" + endDate; + if (member) { + query += "&member=" + member.num; + } + if (proxy) { + query += "&proxy=" + proxy.num; + } + query += types.map((t) => "&type=" + engType[t]); + + const onTypeChange = (e) => { + const newTypes = Array.from(e.target.selectedOptions, (o) => o.value); + setTypes(newTypes); + }; + + const entries = transactions.map((transaction) => { + return ( + <TransactionTr transaction={transaction}> + <td>{icon(transaction)}</td> + <td>{"T-" + transaction.ID}</td> + <td>{printDate(transaction.date)}</td> + <td>{transaction.member.name}</td> + <td>{transaction.proxy ? transaction.proxy.name : ""}</td> + <td>{printMoney(transaction.total) + " €"}</td> + </TransactionTr> + ); + }); + + return ( + <div> + <Form> + <Row> + <Form.Group as={Col}> + <Form.Label>Desde:</Form.Label> + <Form.Control + type="date" + value={startDate} + onChange={(e) => setStartDate(e.target.value)} + max={endDate} + /> + </Form.Group> + <Form.Group as={Col}> + <Form.Label>Hasta:</Form.Label> + <Form.Control + type="date" + value={endDate} + onChange={(e) => setEndDate(e.target.value)} + min={startDate} + max={Date.now()} + /> + </Form.Group> + <Form.Group as={Col}> + <Form.Label>Typo:</Form.Label> + <Form.Control + as="select" + value={types} + onChange={onTypeChange} + multiple + > + <option>compra</option> + <option>recarga</option> + <option>pedido</option> + <option>devolucion</option> + </Form.Control> + </Form.Group> + </Row> + <MemberPicker member={member} onChange={setMember} /> + <MemberPicker member={proxy} onChange={setProxy} text="Por" /> + </Form> + <br /> + <Fetcher url={"/api/transaction?" + query} onFetch={setTransactions}> + <Table className="text-center"> + <thead> + <tr> + <th></th> + <th>ID</th> + <th>Fecha</th> + <th>Socia</th> + <th>Por</th> + <th>Cantidad</th> + </tr> + </thead> + <tbody>{entries}</tbody> + </Table> + </Fetcher> + </div> + ); +} + +export default TransactionList; diff --git a/src/transaction/TransactionTr.js b/src/transaction/TransactionTr.js new file mode 100644 index 0000000000000000000000000000000000000000..8cffa391240eed26ff06ab9bb302c680df9657d6 --- /dev/null +++ b/src/transaction/TransactionTr.js @@ -0,0 +1,61 @@ +import React from "react"; +import { OverlayTrigger, Popover } from "react-bootstrap"; +import { LinkContainer } from "react-router-bootstrap"; + +function transactionOverlay(transaction) { + let title; + let content; + switch (transaction.type) { + case "purchase": + title = "compra"; + content = transaction.purchase.map((p) => p.product.name).join(",") + "."; + break; + case "topup": + if (transaction.total < 0) { + title = "devolución"; + } else { + title = "recarga"; + } + content = transaction.topup.comment; + break; + case "order": + title = "pedido de " + transaction.order.name; + content = transaction.order_purchase.map((p) => { + return ( + <div key={"O" + transaction.ID + "-" + p.order_product.ID}> + {p.order_product.product.name + ": " + p.amount} + <br /> + </div> + ); + }); + break; + case "refund": + title = "devolución de " + transaction.refund.name; + break; + default: + title = "transacción"; + } + return ( + <Popover> + <Popover.Title>{title}</Popover.Title> + {content && <Popover.Content>{content}</Popover.Content>} + </Popover> + ); +} + +function TransactionTr(props) { + const colorClass = + props.transaction.total < 0 ? "table-danger" : "table-success"; + return ( + <OverlayTrigger + overlay={transactionOverlay(props.transaction)} + key={props.transaction.ID} + > + <LinkContainer to={"/transaction/" + props.transaction.ID}> + <tr className={colorClass}>{props.children}</tr> + </LinkContainer> + </OverlayTrigger> + ); +} + +export default TransactionTr; diff --git a/src/transaction/icon.js b/src/transaction/icon.js new file mode 100644 index 0000000000000000000000000000000000000000..580c31dd2e29f57e59fffd8db6f4bedb1a5754f7 --- /dev/null +++ b/src/transaction/icon.js @@ -0,0 +1,24 @@ +import React from "react"; +import { FaShoppingBasket, FaMoneyBillAlt } from "react-icons/fa"; +import { GiPayMoney, GiReceiveMoney } from "react-icons/gi"; +import { HiClipboardCopy, HiClipboardList } from "react-icons/hi"; + +function icon(transaction) { + switch (transaction.type) { + case "purchase": + return <FaShoppingBasket />; + case "topup": + if (transaction.total < 0) { + return <GiReceiveMoney />; + } + return <GiPayMoney />; + case "order": + return <HiClipboardList />; + case "refund": + return <HiClipboardCopy />; + default: + return <FaMoneyBillAlt />; + } +} + +export default icon; diff --git a/src/util.js b/src/util.js index 02685239a293c87348e9c9fe858e4659cdcdd04c..98e361beef610bf507a249898de49ca898b60d46 100644 --- a/src/util.js +++ b/src/util.js @@ -25,4 +25,14 @@ function url(path) { return api + path; } -export { printMoney, printDate, printRole, url }; +function daysAfterNow(days) { + let date = new Date(); + date.setDate(date.getDate() + days); + return date; +} + +function date2string(date) { + return date.toISOString().split("T")[0]; +} + +export { printMoney, printDate, printRole, url, daysAfterNow, date2string };