Skip to content

feat: Various sale improvements #28

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/components/common/Nav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,3 @@ export const NavListItem = React.forwardRef((props, ref) => (
export const NavMenuItem = React.forwardRef((props, ref) => (
<MenuItem component={NavLinkRef} exact {...props} />
));

47 changes: 39 additions & 8 deletions src/components/sales/ItemsTable.jsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import React from "react";
import PropTypes from "prop-types";

import { Box, TableContainer, Table, TableBody, TableRow, TableCell, TextField } from "@material-ui/core";
import {
Box, Paper, TableContainer, Table, TableBody,
TableRow, TableCell, TextField,
} from "@material-ui/core";
import { SkeletonTable } from "components/common/Skeletons";
import { isEmpty, groupData } from "utils/helpers";
import { formatPrice } from "utils/format";


export default function ItemsTable({ items, disabled, quantities, onQuantityChange, ...props }) {
// Skeleton
export default function ItemsTable({ items, itemgroups, disabled, quantities, onQuantityChange, ...props }) {
if (!items)
return <SkeletonTable nCols={3} {...props} />;

if (Object.values(items).length === 0)
return <Box textAlign="center" py={3}>Il n'y a aucun article en vente !</Box>;
if (isEmpty(items)) {
return (
<Box textAlign="center" py={3}>
Il n'y a aucun article en vente !
</Box>
);
}

// TODO Group items
return (
const getTable = (subitems) => (
<TableContainer>
<Table {...props}>
<TableBody>
{Object.values(items).map(item => (
{Object.values(subitems).map(item => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell>{formatPrice(item.price, 'Gratuit')}</TableCell>
Expand Down Expand Up @@ -48,6 +55,30 @@ export default function ItemsTable({ items, disabled, quantities, onQuantityChan
</Table>
</TableContainer>
);

const itemsByGroup = groupData(items, "group");
const oneGroup = Object.values(itemsByGroup).length === 0;
return (
<Box my={2}>
{Object.keys(itemsByGroup).sort().map(groupId => (
<div key={groupId}>
{!(oneGroup && groupId === null) && (
<Box clone mt={3} mb={2}>
<h4>
{groupId === null
? <span style={{ textDecoration: "italic" }}>Sans groupe</span>
: (itemgroups?.[groupId]?.name || "...")
}
</h4>
</Box>
)}
<Paper>
{getTable(itemsByGroup[groupId])}
</Paper>
</div>
))}
</Box>
);
}

ItemsTable.propTypes = {
Expand Down
11 changes: 8 additions & 3 deletions src/pages/admin/AdminNav.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { useLocation, matchPath } from 'react-router-dom';
import { useLocation, matchPath, generatePath } from 'react-router-dom';

import { Container, Grid, Box } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
Expand All @@ -25,7 +25,7 @@ const ADMIN_LINKS = [
resource: "sales",
title: "Statistiques",
icon: <BarChart />,
path: "/admin/sales/:id/stats",
path: "/admin/sales/:id/:view(quantities|tickets|orders|charts)",
},
{
key: "view",
Expand Down Expand Up @@ -60,6 +60,11 @@ function getMatch(location) {
function getNavData(match, resource) {
if (!match)
return {};
const routeParams = {
id: null,
view: 'quantities',
...match.params,
};
switch (match.resource) {
case 'associations':
return {
Expand All @@ -72,7 +77,7 @@ function getNavData(match, resource) {
ADMIN_LINKS.filter(link => link.resource === "sales").map(link => (
<NavIconButton
key={link.key}
to={link.path.replace(':id', match.params.id)}
to={generatePath(link.path, routeParams)}
aria-label={`${link.key}-asso`}
color={match.key === link.key ? 'primary' : 'secondary'}
>
Expand Down
56 changes: 35 additions & 21 deletions src/pages/admin/SaleDetail/QuantitiesSold.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,29 @@ import {
} from "@material-ui/core";
import { Done, Pause, ExpandLess, ExpandMore } from "@material-ui/icons";
import { Skeleton } from "@material-ui/lab";
import Stat from "components/common/Stat";

import { formatPrice } from "utils/format";
import { isEmpty } from "utils/helpers";
import { isEmpty, groupData } from "utils/helpers";


const quantitySold = (items, defaultValue = "?") => (
items ? Object.values(items).reduce((sum, item) => (
sum + (item?.quantity_sold || 0)
), 0) : defaultValue
);

const priceSold = (items, defaultValue = "?") => (
items ? Object.values(items).reduce((sum, item) => (
sum + ((item?.quantity_sold || 0) * (item?.price || 0))
), 0) : defaultValue
);

export function ItemsSold({ items, ...props }) {
if (!items)
return <div><Skeleton /><Skeleton /></div>;

if (Object.values(items).length === 0)
if (isEmpty(items))
return <Box textAlign="center" disabled>Pas d'article</Box>;

return (
Expand All @@ -34,9 +49,9 @@ export function ItemsSold({ items, ...props }) {
<TableCell>{item.name}</TableCell>
<TableCell>{formatPrice(item.price)}</TableCell>
<TableCell>
{item.quantity_sold || 0}
{item?.quantity_sold || 0}
&nbsp;/&nbsp;
{item.quantity ? item.quantity : <span>&infin;</span>}
{item?.quantity || <span>&infin;</span>}
</TableCell>
</TableRow>
))}
Expand All @@ -48,21 +63,17 @@ export function ItemsSold({ items, ...props }) {

export function GroupSold({ itemgroup, items, ...props }) {
const [open, setOpen] = React.useState(true);

const totalSold = items
? items.reduce((sum, item) => sum + item.quantity_sold, 0)
: "...";
return (
<Box clone p={2} mb={2}>
<Paper>
<Grid container alignItems="center" wrap="wrap">
<Grid item xs>
<Box clone m={0}>
<h4>{itemgroup ? itemgroup.name : "..."}</h4>
<h4>{itemgroup?.name || "..."}</h4>
</Box>
</Grid>
<Grid item>
<span>{totalSold || 0}</span>
<span>{quantitySold(items)}/{itemgroup?.quantity || <span>&infin;</span>}</span>
<IconButton size="small" onClick={() => setOpen(!open)}>
{open ? <ExpandLess /> : <ExpandMore />}
</IconButton>
Expand All @@ -78,31 +89,34 @@ export function GroupSold({ itemgroup, items, ...props }) {
);
}

export default function QuantitiesSold({ items, itemgroups, fetched, ...props }) {
export default function QuantitiesSold({ items, itemgroups, fetched, max_item_quantity, ...props }) {
if (!fetched)
return <GroupSold />;

// TODO Better empty state
if (isEmpty(items))
return <Box my={3} p={2} textAlign="center" boxShadow={3} borderRadius={5}>Aucun article</Box>;

const itemsByGroup = Object.values(items).reduce((groupMap, { group, ...item }) => {
if (group in groupMap) groupMap[group].push(item);
else groupMap[group] = [item];
return groupMap;
}, {});
if (isEmpty(items)) {
return (
<Box my={3} p={3} textAlign="center" boxShadow={3} borderRadius={5}>
Aucun article
</Box>
);
}

const itemsByGroup = groupData(items, "group");
const orphans = itemsByGroup[null];
return (
<React.Fragment>
<Box display="flex" justifyContent="space-evenly" mt={2} mb={4}>
<Stat title="Articles vendus" value={quantitySold(items)} max={max_item_quantity} />
<Stat title="Argent récolté" value={priceSold(items)} unit="€" />
</Box>
{Object.values(itemgroups).map((itemgroup) => (
<GroupSold
key={itemgroup.id}
itemgroup={itemgroup}
items={itemsByGroup[itemgroup.id] || []}
/>
))}
{orphans && orphans.length && (
{orphans?.length && (
<GroupSold itemgroup={{ name: "Sans groupe" }} items={orphans} />
)}
</React.Fragment>
Expand Down
69 changes: 35 additions & 34 deletions src/pages/admin/SaleDetail/index.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { useStoreAPIData } from "redux/hooks";
import { formatDate } from "utils/format";

import { Box, Container, Grid, Chip, Tabs, Tab } from "@material-ui/core";
import { PlayArrow, Pause, Public, Lock } from "@material-ui/icons";

import Stat from "components/common/Stat";
import Loader from "components/common/Loader";
import { Link } from "components/common/Nav";
import { CopyButton } from "components/common/Buttons";

Expand All @@ -15,26 +16,24 @@ import TicketsList from "./TicketsList";


export default function SaleDetail(props) {
const [tab, setTab] = React.useState("quantities");

const saleId = props.match.params.sale_id;
const { sale_id: saleId, view: tab } = props.match.params;
const { data: sale, fetched } = useStoreAPIData(["sales", saleId], { include: "association" }, { singleElement: true });
const items = useStoreAPIData(["sales", saleId, "items"], { page_size: 'max' });
const itemgroups = useStoreAPIData(["sales", saleId, "itemgroups"], { page_size: 'max' });
const items = useStoreAPIData(["sales", saleId, "items"], { page_size: "max", with: "quantity_sold" });
const itemgroups = useStoreAPIData(["sales", saleId, "itemgroups"], { page_size: "max" });

// TODO Better loader
if (!fetched)
return "Loading"
return <Loader fluid text="Chargement de la vente..." />;

const saleLink = window.location.href.replace("/admin/", "/");
const chipMargin = { marginBottom: 4, marginRight: 4 };
const basePath = props.location.pathname.split("/").slice(0, -1).join("/");
return (
<Container>
<Grid container spacing={2}>
<Grid item xs={12} md={3}>
<Grid container spacing={2} justify="center">
<Grid item xs="auto" md={12}>
<h4 style={{ marginTop: 0 }}>Organisé par {sale.association && sale.association.shortname}</h4>
<h4 style={{ marginTop: 0 }}>Organisé par {sale?.association?.shortname || "..."}</h4>
<div>
{sale.is_active
? <Chip style={chipMargin} label="Active" color="primary" icon={<PlayArrow />} />
Expand Down Expand Up @@ -68,44 +67,46 @@ export default function SaleDetail(props) {
<Grid item xs md={9}>
<Tabs
value={tab}
onChange={(event, newTab) => setTab(newTab)}
onChange={(event, newTab) => props.history.push(`${basePath}/${newTab}`)}
variant="fullWidth"
centered
>
<Tab value="quantities" label="Quantités vendues" />
<Tab value="tickets" label="Liste des billets" />
<Tab value="orders" label="Liste des commandes" />
<Tab value="chart" label="Graphique des ventes" />
<Tab value="charts" label="Graphiques des ventes" disabled />
</Tabs>

<Box py={2}>
{(tab === "quantities" && (
<React.Fragment>
<Box display="flex" justifyContent="space-evenly" mt={2} mb={4}>
<Stat title="Places vendues" value={480} max={1000} />
<Stat title="Argent récolté" value={1050} unit="€" />
</Box>
<Switch>
<Route exact path={`${basePath}/quantities`} render={(routeProps) => (
<QuantitiesSold
max_item_quantity={sale.max_item_quantity}
items={items.data}
itemgroups={itemgroups.data}
fetched={items.fetched && itemgroups.fetched}
{...routeProps}
/>
)} />
<Route exact path={`${basePath}/tickets`} render={(routeProps) => (
<TicketsList
saleId={saleId}
items={items.data}
itemgroups={itemgroups.data}
fetched={items.fetched}
{...routeProps}
/>
)} />
<Route exact path={`${basePath}/orders`} render={(routeProps) => (
<OrdersTable
saleId={saleId}
items={items.data}
{...routeProps}
/>
</React.Fragment>
)) || (tab === "orders" && (
<OrdersTable
saleId={saleId}
items={items.data}
/>
)) || (tab === "tickets" && (
<TicketsList
saleId={saleId}
items={items.data}
itemgroups={itemgroups.data}
fetched={items.fetched}
/>
)) || (tab === "chart" && (
<p>À venir...</p>
))}
)} />
<Route exact path={`${basePath}/charts`} render={(routeProps) => (
<p>À venir...</p>
)} />
</Switch>
</Box>
</Grid>
</Grid>
Expand Down
6 changes: 3 additions & 3 deletions src/pages/admin/SaleEditor/ItemsManager/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ function ItemsManager({ selected, ...props }) {
let supprTitle = "Êtes-vous sûr.e de vouloir supprimer ";
const editorProps = {
onChange: props.onChange,
errors: props.errors[name][id] || {},
editing: props.editing[name][id],
saving: props.saving[name][id],
errors: props.errors?.[name]?.[id] || {},
editing: props.editing?.[name]?.[id] || false,
saving: props.saving?.[name]?.[id] || false,
inDialog,
};

Expand Down
4 changes: 2 additions & 2 deletions src/pages/admin/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export default function AdminSite(props) {
<Route exact path="/admin" component={Dashboard} />
<Route exact path="/admin/assos/:asso_id" component={AssoDashboard} only="asso_manager" />
<Route exact path="/admin/sales/create" component={SaleEditor} />
<Redirect exact from="/admin/sales/:sale_id" to="/admin/sales/:sale_id/stats" />
<Redirect exact from="/admin/sales/:sale_id" to="/admin/sales/:sale_id/quantities" />
<Route exact path="/admin/sales/:sale_id/view" component={SaleView} />
<Route exact path="/admin/sales/:sale_id/edit" component={SaleEditor} />
<Route exact path="/admin/sales/:sale_id/stats" component={SaleDetail} />
<Route exact path="/admin/sales/:sale_id/:view(quantities|tickets|orders|charts)" component={SaleDetail} />
<Route component={Error404} />
</Switch>
</React.Fragment>
Expand Down
Loading