diff --git a/src/App.js b/src/App.js index 79d22e61d175190fabf0f5e644228560da115184..eb90662679b003941ada3ceeefe7bde28f0e3e64 100644 --- a/src/App.js +++ b/src/App.js @@ -17,6 +17,7 @@ import automated_transactions from "./resources/AutomatedTransactions"; import events from './resources/Events'; import members from './resources/Members'; import movements from "./resources/Movements"; +import participations from './resources/Participations'; import people from './resources/People'; import personal_accounts from "./resources/PersonalAccounts"; import personal_transactions from "./resources/PersonalTransactions"; @@ -58,6 +59,7 @@ export default class App extends Component { <Resource name="personal_accounts" {...personal_accounts} /> <Resource name="personal_transactions" {...personal_transactions} /> <Resource name="events" {...events} /> + <Resource name="participations" {...participations} /> </Admin> ); } diff --git a/src/components/PaymentInput.js b/src/components/PaymentInput.js index 0c0917d39b2230343ea0eba8968f25271d556d0d..51aad16c1343ced2dc1e98a3879db02c5de43463 100644 --- a/src/components/PaymentInput.js +++ b/src/components/PaymentInput.js @@ -3,15 +3,15 @@ import React from "react"; import { FormDataConsumer, SelectInput, useTranslate } from 'react-admin'; import PersonalAccountSelector from "../components/PersonalAccountSelector"; -const PaymentInput = ({ price }) => { +const PaymentInput = ({ price, optional = false }) => { const translate = useTranslate(); return (<> <MuiTextField value={Number(price).toLocaleString('fr-FR', { currency: 'EUR', currencyDisplay: 'symbol', style: 'currency' })} disabled variant="filled" type="text" label={translate('inputs.multiproductcount.price')} /> - <SelectInput source="payment" label="Moyen de paiement" allowEmpty={false} choices={[ + <SelectInput source="payment" label="Moyen de paiement" allowEmpty={optional} choices={[ { id: 'cash', name: 'Liquide (Caisse)' }, { id: 'card', name: 'Carte Bancaire' }, { id: 'account', name: 'Compte personel' }, - ]} initialValue='cash' /> + ]} initialValue={optional ? null : 'cash'} /> <FormDataConsumer> {({ formData, ...rest }) => formData.payment === 'account' && <PersonalAccountSelector source="token" label="Compte" /> diff --git a/src/layout/Menu.js b/src/layout/Menu.js index 1c059a54800c06f09e8be2670ddb52bdd8bb9427..ac0c7da5f46b7a7044c6ac4df244beb999a0ec50 100644 --- a/src/layout/Menu.js +++ b/src/layout/Menu.js @@ -21,6 +21,7 @@ import ImportExportIcon from '@material-ui/icons/ImportExport'; import LocalCafeIcon from '@material-ui/icons/LocalCafe'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; import MoneyIcon from '@material-ui/icons/Money'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; import SwapHorizIcon from '@material-ui/icons/SwapHoriz'; import * as React from 'react'; @@ -105,6 +106,7 @@ const Menu = ({ onMenuClick, logout }) => { <Item to="/purchases/create" permissions="purchases.create" primaryText={translate('menu.left.buy')} leftIcon={<ShoppingCartIcon />} /> <Item to="/accounts_counts/create" permissions="accounts_counts.create" primaryText={translate('menu.left.count_money')} leftIcon={<MoneyIcon />} /> <Item to="/products_counts/create" permissions="products_counts.create" primaryText={translate('menu.left.count_stocks')} leftIcon={<BarChartIcon />} /> + <Item to="/participations/create" permissions="participations.create" primaryText={translate('menu.left.add_participant')} leftIcon={<EventIcon />} /> <Accordeon open={false} title={translate('menu.left.humans')} permissions={["people.show", "members.show", "users.show"]}> <Item to="/people" permissions="people.show" primaryText={translate('menu.left.people')} leftIcon={<EmojiPeopleIcon />} /> <Item to="/members" permissions="members.show" primaryText={translate('menu.left.members')} leftIcon={<GroupIcon />} /> @@ -128,8 +130,9 @@ const Menu = ({ onMenuClick, logout }) => { <Item to="/purchases" permissions="purchases.show" primaryText={translate('menu.left.purchases')} leftIcon={<ShoppingCartIcon />} /> <Item to="/transferts" permissions="transferts.show" primaryText={translate('menu.left.transferts')} leftIcon={<ImportExportIcon />} /> </Accordeon> - <Accordeon open={false} title={translate('menu.left.events')} permissions={["events.show"]}> + <Accordeon open={false} title={translate('menu.left.events')} permissions={["events.show", "participations.show"]}> <Item to="/events" permissions="events.show" primaryText={translate('menu.left.events')} leftIcon={<EventIcon />} /> + <Item to="/participations" permissions="participations.show" primaryText={translate('menu.left.participations')} leftIcon={<PersonAddIcon />} /> </Accordeon> <Accordeon open={false} title={translate('menu.left.archives')} permissions={["archived_members.show"]}> <Item to="/archived_members" permissions="archived_members.show" primaryText={translate('menu.left.archived_members')} leftIcon={<GroupIcon />} /> @@ -139,5 +142,4 @@ const Menu = ({ onMenuClick, logout }) => { </> ); }; - export { Menu }; diff --git a/src/providers/I18nProvider.js b/src/providers/I18nProvider.js index aed52bf903e7b10dca09863e556cdff16132a004..f7bbeb9c3265276319b3505a0a54b230e99e51b1 100644 --- a/src/providers/I18nProvider.js +++ b/src/providers/I18nProvider.js @@ -84,6 +84,7 @@ const messages = { buy: "Acheter", count_money: "Compter les comptes", count_stocks: "Compter les stocks", + add_participant: "Ajouter un participant", members: "Membres", stocks: "Stocks", products: "Produits", @@ -113,7 +114,8 @@ const messages = { personal_transactions: "Transactions Personnelles", personal_refills: "Recharge Compte Perso.", refill: "Recharger", - events: "Évènements" + events: "Évènements", + participations: "Participants" } }, resources: { @@ -391,6 +393,17 @@ const messages = { password_changed_at: 'MDP changé le' } }, + participations: { + name: 'Participation |||| Participations', + fields: { + id: "#", + created_at: 'Créé le', + updated_at: 'Modifié le', + event_id: "Évènement", + person_id: "Personne", + transaction_id: "Transaction" + } + }, profile: { name: 'Profile', fields: { diff --git a/src/resources/Participations.js b/src/resources/Participations.js new file mode 100644 index 0000000000000000000000000000000000000000..12882b4c808e9cc0c43fb31a157eeff97e844efb --- /dev/null +++ b/src/resources/Participations.js @@ -0,0 +1,301 @@ +import { capitalize, FormControl, FormControlLabel, InputLabel, MenuItem, Select, Switch, TextField as MuiTextField, useMediaQuery } from '@material-ui/core'; +import CheckIcon from '@material-ui/icons/Check'; +import CloseIcon from '@material-ui/icons/Close'; +import React, { useEffect, useState } from "react"; +import { AutocompleteInput, BooleanField, Create, Datagrid, DateField, Edit, EditButton, Error, FormDataConsumer, FormTab, FunctionField, Labeled, List, Loading, ReferenceField, ReferenceInput, ShowButton, SimpleList, SimpleShowLayout, TabbedForm, TextField, useInput, useNotify, useQuery, useRefresh, useTranslate } from 'react-admin'; +import { ShowDialog } from '../components/DialogForm'; +import PaymentInput from "../components/PaymentInput"; + +const ParticipationFilters = [ + <ReferenceInput source="event_id" reference="events" filterToQuery={searchText => ({ name: searchText })}> + <AutocompleteInput optionText="name" /> + </ReferenceInput>, + <ReferenceInput source="person_id" reference="people" filterToQuery={searchText => ({ fullname: searchText, has_account: true })}> + <AutocompleteInput optionText="fullname" /> + </ReferenceInput> +]; + +const EventDataField = (props) => { + const { data, loading, error } = useQuery({ + type: 'getOne', + resource: 'events', + payload: { id: props.record.event_id } + }); + + if (loading) return <Loading />; + if (error) return <Error />; + if (!data) return null; + + return data.data.map((val, i) => { + if (val.type === "bool") { + return (<Labeled key={i} label={capitalize(val.name)}> + <BooleanField source={"data." + val.name} record={props.record} /> + </Labeled>); + } else { + return (<Labeled key={i} label={capitalize(val.name)}> + <TextField source={"data." + val.name} label={val.name} record={props.record} /> + </Labeled>); + } + }); +}; + +const MultiButton = (props) => { + if (props?.record?.transaction_id === null || props?.record?.transaction_id === undefined) { + return <EditButton {...props} />; + } else { + return <ShowButton {...props} />; + } +} + +const Participation = (props) => { + const isDesktop = useMediaQuery(theme => theme.breakpoints.up('md')); + return ( + <> + <List {...props} filters={ParticipationFilters} bulkActionButtons={false}> + {isDesktop ? ( + <Datagrid> + <TextField source="id" /> + <ReferenceField source="event_id" reference="events" link="show"> + <TextField source="name" /> + </ReferenceField> + <ReferenceField source="person_id" reference="people" link="show" > + <FunctionField render={r => r.firstname + " " + r.lastname} /> + </ReferenceField> + <ReferenceField source="transaction_id" reference="transactions" link="show"> + <TextField source="name" /> + </ReferenceField> + <MultiButton /> + </Datagrid> + ) : ( + <SimpleList + secondaryText={record => + <ReferenceField record={record} source="event_id" reference="events" link={false}> + <TextField source="name" /> + </ReferenceField>} + primaryText={record => + <ReferenceField record={record} source="person_id" reference="people" link={false} > + <FunctionField render={r => r.firstname + " " + r.lastname} /> + </ReferenceField>} + tertiaryText={record => (record.transaction_id === null ? <CloseIcon /> : <CheckIcon />)} + linkType="show" + /> + )} + </List> + <ShowDialog> + <SimpleShowLayout> + <TextField source="id" /> + <ReferenceField source="event_id" reference="events" link="show"> + <TextField source="name" /> + </ReferenceField> + <ReferenceField source="person_id" reference="people" link="show" > + <FunctionField render={r => r.firstname + " " + r.lastname} /> + </ReferenceField> + <ReferenceField source="transaction_id" reference="transactions" link="show"> + <TextField source="name" /> + </ReferenceField> + <EventDataField /> + <DateField source="created_at" /> + <DateField source="updated_at" /> + </SimpleShowLayout> + </ShowDialog> + </> + ); +}; + +const eventPrice = (event, person, data) => { + const member = person.is_member; + let price = member ? event.price_member : event.price; + + for (let dat of event.data) { + if (dat.type === "boolean") { + if (data[dat.name] === true) { + price += member ? dat.price_member : dat.price; + } + } else if (dat.type === "select") { + for (let val of dat.values) { + if (data[dat.name] === val.name) { + price += member ? val.price_member : val.price; + break; + } + } + } + } + + return price; +} + +const EventDataInput = ({ eventId, personId, priceChanged, ...props }) => { + // TODO: Calculate and report price + const { + input: { name, onChange, initialValue }, + meta: { touched, error }, + isRequired + } = useInput(props); + console.log(initialValue); + + const [value, setValue] = useState({}); + + const { data: event, loading: loading1, error: error1 } = useQuery({ + type: 'getOne', + resource: 'events', + payload: { id: eventId } + }); + + const { data: person, loading: loading2, error: error2 } = useQuery({ + type: 'getOne', + resource: 'people', + payload: { id: personId } + }); + + const onChangeWrapper = (name) => { + return (e) => { + let val = { ...value }; + val[name] = e.target.type === 'checkbox' ? e.target.checked : e.target.value; + setValue(val); + onChange(val); + } + } + + useEffect(() => { + if (priceChanged !== undefined && event !== undefined && person !== undefined && value !== undefined) + priceChanged(eventPrice(event, person, value)); + }, [event, person, value]); + + useEffect(() => { + let val = {}; + + event?.data?.map((v) => { + if (v.type === "boolean") { + val[v.name] = false; + } else if (v.type === "select") { + val[v.name] = v.values[0]?.name; + } else { + val[v.name] = ""; + } + }); + + setValue(val); + }, [event]); + + if (loading1 || loading2) return <Loading />; + if (error1 || error2) return <Error />; + if ((!event) || (!person)) return null; + + return event.data.map((structure, index) => { + if (structure.type === "boolean") { + + return <FormControlLabel key={index} margin="normal" + control={<Switch checked={value[structure.name] ?? false} onChange={onChangeWrapper(structure.name)} name={structure.name} />} + label={capitalize(structure.name)} + />; + } else if (structure.type === "select") { + return <FormControl key={index} variant="filled" margin="normal"> + <InputLabel id={"data-" + index}>{capitalize(structure.name)}</InputLabel> + <Select + labelId={"data-" + index} + id={"data-select-" + index} + value={value[structure.name] ?? ""} + onChange={onChangeWrapper(structure.name)} + > + {structure.values.map((item, i) => { + return <MenuItem value={item.name} key={i}>{capitalize(item.name)}</MenuItem> + })} + </Select> + </FormControl>; + } else { + return <MuiTextField key={index} name={structure.name} label={capitalize(structure.name)} onChange={onChangeWrapper(structure.name)} value={value[structure.name] ?? ""} variant="filled" margin="normal" size="small" />; + } + }); +}; + +const EditParticipation = props => { + const translate = useTranslate(); + const refresh = useRefresh(); + const notify = useNotify(); + + const [price, setPrice] = useState(0); + + return <> + <Edit {...props} onSuccess={() => { + notify('ra.notification.created', 'info', { smart_count: 1 }); + refresh(); + }}> + <TabbedForm syncWithLocation={false}> + <FormTab label="Produits"> + <ReferenceField source="event_id" reference="events" link="show"> + <TextField source="name" /> + </ReferenceField> + <ReferenceField source="person_id" reference="people" link="show" > + <FunctionField render={r => r.firstname + " " + r.lastname} /> + </ReferenceField> + <ReferenceField source="transaction_id" reference="transactions" link="show"> + <TextField source="name" /> + </ReferenceField> + <FormDataConsumer> + {({ formData, ...rest }) => { + if (formData.event_id === undefined || formData.person_id === undefined || (formData.transaction_id !== null && formData.transaction_id !== undefined)) + return null; + return <EventDataInput priceChanged={setPrice} source="data" key={formData.event_id + " " + formData.person_id} eventId={formData.event_id} personId={formData.person_id} />; + }} + </FormDataConsumer> + <MuiTextField value={Number(price).toLocaleString('fr-FR', { currency: 'EUR', currencyDisplay: 'symbol', style: 'currency' })} disabled variant="filled" type="text" label={translate('inputs.multiproductcount.price')} /> + </FormTab> + <FormTab label="Paiement"> + <FormDataConsumer> + {({ formData, ...rest }) => { + if (formData.transaction_id !== null && formData.transaction_id !== undefined) + return null; + return <PaymentInput price={price} optional />; + }} + </FormDataConsumer> + </FormTab> + </TabbedForm> + </Edit> + </> +}; + +const AddParticipation = props => { + const refresh = useRefresh(); + const notify = useNotify(); + const translate = useTranslate(); + + const [price, setPrice] = useState(0); + + return <> + <Create {...props} onSuccess={() => { + notify('ra.notification.created', 'info', { smart_count: 1 }); + refresh(); + }}> + <TabbedForm syncWithLocation={false}> + <FormTab label="Produits"> + <ReferenceInput source="event_id" reference="events" filterToQuery={searchText => ({ name: searchText })}> + <AutocompleteInput optionText="name" /> + </ReferenceInput> + <ReferenceInput source="person_id" reference="people" filterToQuery={searchText => ({ fullname: searchText })}> + <AutocompleteInput optionText="fullname" /> + </ReferenceInput> + <FormDataConsumer> + {({ formData, ...rest }) => { + if (formData.event_id === undefined || formData.person_id === undefined) + return null; + return <EventDataInput priceChanged={setPrice} source="data" key={formData.event_id + " " + formData.person_id} eventId={formData.event_id} personId={formData.person_id} />; + }} + </FormDataConsumer> + <MuiTextField value={Number(price).toLocaleString('fr-FR', { currency: 'EUR', currencyDisplay: 'symbol', style: 'currency' })} disabled variant="filled" type="text" label={translate('inputs.multiproductcount.price')} /> + </FormTab> + <FormTab label="Paiement"> + <PaymentInput price={price} optional /> + </FormTab> + </TabbedForm> + </Create> + </> +}; + +const participations = { + list: Participation, + create: AddParticipation, + edit: EditParticipation, + show: Participation +}; + +export default participations;