️ This Gitlab will be shut down at 2021-12-31 23:59:59.
Students and staff can migrate to gitlab.utwente.nl.
SNT members can migrate to gitlab.snt.utwente.nl.
Contact bestuur@snt.utwente.nl for more information.

Migrate your projects today!
Export your project using the webinterface or use a script.

Commit 2d0aca2c authored by TJHeeringa's avatar TJHeeringa

Rewrote DataField and Memberships in MembersDetail

parent e2c52603
......@@ -77,6 +77,7 @@ class DateValidator extends ValidatorComponent {
requiredError,
helperText,
validatorListener,
containerProps,
...rest
} = this.props;
const { isValid } = this.state;
......
......@@ -33,8 +33,6 @@ export const MembershipForm = ({ association, membership, handleMembershipChange
const alerthandler = useAlertHandler();
const API = useAPI();
// const MembertypesAPI = useMemberTypes();
// const membertypes = MembertypesAPI.collection;
const { data: membertypes } = useGetMemberTypesByAssociationQuery(association.slug);
useAddValidationRule(ValidatorForm, isAfterDate.name, isAfterDate.validate(membership.date_joined));
......@@ -64,7 +62,7 @@ export const MembershipForm = ({ association, membership, handleMembershipChange
"collection it will not be recomputed."
: "Only the board can change this.",
[association.is_board]);
const newMembertypeHelperText = useMemo(()=>membership.date_left === null
const newMembertypeHelperText = useMemo(()=>membership.date_left
? "What membertype do you want to be next year?"
: "Disabled because you have ended your membership",
[association.date_left]);
......
......@@ -7,6 +7,7 @@ import {ValidatorForm} from "react-material-ui-form-validator";
import {useAlertHandler} from "../../Contexts/AlertHandler";
import {useAPI} from "../../Contexts/API";
import {useDataFields} from "../../Contexts/Members";
import {useGetDataFieldsByAssociationQuery} from "../../Store/services/dataFields";
import SpecificDataField from "../Fields/SpecificDataField";
import Wrapper from "../Fields/Wrapper";
......@@ -17,8 +18,7 @@ export const SpecificDataForm = ({ specificData: propSpecificData, association,
const [specificDataChanges, setSpecificDataChanges] = useState({});
const dataFieldsApi = useDataFields();
const dataFields = dataFieldsApi.collection;
const { data: dataFields } = useGetDataFieldsByAssociationQuery(association.slug);
// We need to save the specific data in a state to be able to use it in a form.
// The number of fields is not always the same. We could create a state for each.
......@@ -40,7 +40,7 @@ export const SpecificDataForm = ({ specificData: propSpecificData, association,
,[propSpecificData]);
const specificDataWithChanges = useMemo(()=>
Object.fromEntries(dataFields.map((dataField)=>([dataField.url, {...specificData[dataField.url], ...specificDataChanges[dataField.url]}])))
Object.fromEntries(dataFields?.map((dataField)=>([dataField.url, {...specificData[dataField.url], ...specificDataChanges[dataField.url]}])) || [])
,[dataFields, specificData, specificDataChanges]);
const handleSubmit = () => {
......
......@@ -21,12 +21,12 @@ const MembershipInfo = ({ association, membership }) => {
if (!loading) {
if (membership.new_type !== null) {
data = {...data,
"Current Membertype:": membertypes?.find(membertype=>membertype.url === membership.type).type || "",
"Upcoming Membertype:": membertypes?.find(membertype=>membertype.url === membership.new_type).type || ""
"Current Membertype:": membertypes?.find(membertype=>membertype.url === membership.type)?.type || "",
"Upcoming Membertype:": membertypes?.find(membertype=>membertype.url === membership.new_type)?.type || ""
};
} else {
data = {...data,
"Membertype:": membertypes?.find(membertype=>membertype.url === membership.type).type || "",
"Membertype:": membertypes?.find(membertype=>membertype.url === membership.type)?.type || "",
};
}
}
......
......@@ -2,11 +2,12 @@ import PropTypes from "prop-types";
import React, {useMemo} from "react";
import {useDataFields} from "../../Contexts/Members";
import {useGetDataFieldsByAssociationQuery} from "../../Store/services/dataFields";
import Info from "./Info";
const SpecificData = ({ specific_data }) => {
const { collection: dataFields } = useDataFields();
const dataFieldToTypeDict = Object.fromEntries(dataFields.map(dataField=>[dataField.name, dataField.type]));
const SpecificData = ({ specific_data, association }) => {
const { data: dataFields } = useGetDataFieldsByAssociationQuery(association.slug);
const dataFieldToTypeDict = Object.fromEntries(dataFields?.map(dataField=>[dataField.name, dataField.type]) || []);
const data = useMemo(()=>
Object.fromEntries(specific_data.map(sp=>[sp.name, dataFieldToTypeDict[sp.name] === "Boolean"
......@@ -24,7 +25,8 @@ const SpecificData = ({ specific_data }) => {
};
SpecificData.propTypes = {
specific_data: PropTypes.array.isRequired
specific_data: PropTypes.array.isRequired,
association: PropTypes.object.isRequired
};
export default SpecificData;
\ No newline at end of file
......@@ -4,28 +4,27 @@ import React, {useState} from "react";
import { useLocation } from "react-router-dom";
import {useAPI} from "../../../Contexts/API";
import {useMemberships} from "../../../Store/services/user";
import AssociationsNavDropdown from "./Dropdown";
import AssociationsNavTabs from "./NavTabs";
const Header = ({ size }) => {
const [imageWidths, setImageWidths] = useState([]);
const location = useLocation();
const API = useAPI();
const association_memberships = API.sunmember ? API.sunmember.association_memberships : [];
const { pathname } = useLocation();
const association_memberships = useMemberships();
const url = "/protected";
const calculateImageWidths = () => {
const membership_count = association_memberships.length;
const image_count = imageWidths.length;
const associations_without_logo_count = membership_count - image_count;
return sum(imageWidths)+membership_count*30+associations_without_logo_count*100;
};
// const calculateImageWidths = () => {
// const membership_count = association_memberships.length;
// const image_count = imageWidths.length;
// const associations_without_logo_count = membership_count - image_count;
// return sum(imageWidths)+membership_count*30+associations_without_logo_count*100;
// };
const calculateTotalNavBar = () => {
//the total sum of all the margins and icons
// return calculateImageWidths()+65;
return (association_memberships.length+2)*160+100;
return ((association_memberships?.length || 0) + 2)*160+100;
};
const onImgLoad = ({target:img}, id) => {
......@@ -42,15 +41,15 @@ const Header = ({ size }) => {
{ size.width > calculateTotalNavBar()
? (
<AssociationsNavTabs
association_memberships={association_memberships}
association_memberships={association_memberships || []}
url={url}
path={location.pathname}
path={pathname}
onImgLoad={onImgLoad}
/>)
: (
<AssociationsNavDropdown
association_memberships={association_memberships}
path={location.pathname}
association_memberships={association_memberships || []}
path={pathname}
url={url}
/>)
}
......
......@@ -9,6 +9,7 @@ import React, {useMemo} from "react";
import {useLocation} from "react-router-dom";
import {useAPI} from "../../../Contexts/API";
import {useMemberships} from "../../../Store/services/user";
import Drawer from "../../Lists/Drawer/Drawer";
......@@ -87,7 +88,9 @@ const AssociationMemberDrawer = (props) => {
// an empty sidebar.
const url_segments = url.split("/");
const slug = url_segments[url_segments.length-2];
const currentMembership = API.sunmember && API.sunmember.association_memberships.find(membership=>membership.association.slug===slug).current;
const memberships = useMemberships();
const currentMembership = memberships?.find(membership=>membership.association.slug===slug)?.current;
if (!currentMembership) {
return null;
}
......
......@@ -236,7 +236,7 @@ const filterMessages = {
// const BooleanFormatter = ({ value }) => <Chip label={value === "True" ? "Yes" : "No"} />;
const BooleanFormatter = ({ value }) => value === "True" ? "Yes" : "No";
const CurrencyFormatter = ({ value }) => value.toLocaleString("en-GB", { style: "currency", currency: "EUR" });
const CurrencyFormatter = ({ value }) => value?.toLocaleString("en-GB", { style: "currency", currency: "EUR" }) || "";
const DateFormatter = ({ value }) => value ? moment(value).format("L") : "";
......
import Button from "@material-ui/core/Button";
import MenuItem from "@material-ui/core/MenuItem";
import CalendarToday from "@material-ui/icons/CalendarToday";
import DirectionsWalkIcon from "@material-ui/icons/DirectionsWalk";
import TransferWithinAStationIcon from "@material-ui/icons/TransferWithinAStation";
import PropTypes from "prop-types";
import React, {useMemo} from "react";
import {ValidatorForm} from "react-material-ui-form-validator";
import {DateFieldV2, DateFieldV3} from "../../Components/Fields/DateField";
import IconHolder from "../../Components/Fields/IconHolder";
import SelectField from "../../Components/Fields/SelectField";
import Wrapper from "../../Components/Fields/Wrapper";
import useAddValidationRule from "../../Components/Hooks/useAddValidationRule";
import isAfterDate from "../../Components/ValidatorRules/isAfterDate";
import {useGetMemberTypesByAssociationQuery} from "../../Store/services/memberTypes";
const MembershipForm = ({ association, membership, handleMembershipChange, onSubmit, onCancel }) => {
const { data: membertypes } = useGetMemberTypesByAssociationQuery(association.slug);
useAddValidationRule(ValidatorForm, isAfterDate.name, isAfterDate.validate(membership.date_joined));
const dateLeftHelperText = association.is_board && "You cannot change this to a value in the past to prevent " +
"inconstistencies. If you really want to have this at a date in the past contact the helpdesk.";
const currentMembertypeHelperText = useMemo(()=>association.is_board
? "Be careful when changing this. If you have added the membership fee for this member already in a debt " +
"collection it will not be recomputed."
: "Only the board can change this.",
[association.is_board]);
const newMembertypeHelperText = useMemo(()=>membership.date_left
? "Disabled because you have ended your membership"
: "What membertype do you want to be next year?",
[association.date_left]);
const chooseableMemberTypes = useMemo(()=>association.is_board
? membertypes
: membertypes.filter(membertype=>!membertype.only_chooseable_by_board),
[association.is_board, membertypes]);
console.log(newMembertypeHelperText, association.date_left);
return (
<ValidatorForm
onSubmit={onSubmit}
onError={errors => console.log(errors)}
>
<Wrapper>
<IconHolder Icon={CalendarToday}/>
<DateFieldV2
name={"date_joined"}
label={"Date joined"}
value={membership.date_joined}
onChange={date=>handleMembershipChange("date_joined", date)}
disabled
/>
</Wrapper>
<Wrapper>
<IconHolder Icon={CalendarToday}/>
<DateFieldV3
label={"Date left"}
name={"date_left"}
value={membership.date_left}
onChange={date=>handleMembershipChange("date_left", date)}
disabled={!association.is_board}
helperText={dateLeftHelperText}
validators={[isAfterDate.name]}
errorMessages={["Must be a later date than the date the member joined"]}
/>
</Wrapper>
<Wrapper>
<IconHolder Icon={DirectionsWalkIcon}/>
<SelectField
name={"Current Membertype"}
value={membership.type || ""}
onChange={event=>handleMembershipChange("type", event.target.value)}
disabled={!association.is_board}
helperText={currentMembertypeHelperText}
>
{ chooseableMemberTypes && chooseableMemberTypes.map((membertype, m)=>(
<MenuItem key={m} value={membertype.url}>{ membertype.type }</MenuItem>
)) }
</SelectField>
</Wrapper>
<Wrapper>
<IconHolder Icon={TransferWithinAStationIcon}/>
<SelectField
name={"New Membertype"}
value={membership.new_type || ""}
onChange={event=>handleMembershipChange("new_type", event.target.value)}
disabled={membership.date_left !== null}
helperText={newMembertypeHelperText}
>
<MenuItem value={""}> - </MenuItem>
{ chooseableMemberTypes && chooseableMemberTypes.map((membertype, m)=>(
<MenuItem key={m} value={membertype.url}>{ membertype.type }</MenuItem>
)) }
</SelectField>
</Wrapper>
{ /*<Wrapper>*/ }
{ /* <IconHolder Icon={VisibilityIcon}/>*/ }
{ /* <SelectField*/ }
{ /* name={"Data after end of membership"}*/ }
{ /* value={membership.visible_after_date_left}*/ }
{ /* onChange={event=>handleMembershipChange("visible_after_date_left", event.target.value)}*/ }
{ /* helperText={"What should happen to your data after your membership has ended?"}*/ }
{ /* >*/ }
{ /* <MenuItem value={true}>visible</MenuItem>*/ }
{ /* <MenuItem value={false}>gone</MenuItem>*/ }
{ /* </SelectField>*/ }
{ /*</Wrapper>*/ }
<Wrapper>
<div/>
<div>
<Button type={"submit"} variant={"contained"} color={"primary"}>Save</Button>
&nbsp;
<Button variant={"contained"} onClick={onCancel}>Cancel</Button>
</div>
</Wrapper>
</ValidatorForm>
);
};
MembershipForm.propTypes = {
membership: PropTypes.object.isRequired,
association: PropTypes.object.isRequired,
handleMembershipChange: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired
};
export default MembershipForm;
\ No newline at end of file
import moment from "moment/moment";
import PropTypes from "prop-types";
import React, {useMemo} from "react";
import Info from "../../Components/Info/Info";
import {useGetMemberTypesByAssociationQuery} from "../../Store/services/memberTypes";
const MembershipInfo = ({ association, membership }) => {
const { data: membertypes, loading } = useGetMemberTypesByAssociationQuery(association.slug);
const morphMembershipToData = (membership) => {
let data = {
"Date Joined:": moment(membership.date_joined).format("L")
};
if (membership.date_left) {
const date_left_or_leaving_text = moment(membership.date_left) <= moment() ? "Date Left:" : "Date Leaving";
data[date_left_or_leaving_text] = moment(membership.date_left).format("L");
}
data["Status"] = membership.status;
if (!loading) {
if (membership.new_type) {
data = {...data,
"Current Membertype:": membertypes?.find(membertype=>membertype.url === membership.type)?.type || "",
"Upcoming Membertype:": membertypes?.find(membertype=>membertype.url === membership.new_type)?.type || ""
};
} else {
data = {...data,
"Membertype:": membertypes?.find(membertype=>membertype.url === membership.type)?.type || "",
};
}
}
return data;
};
const data = useMemo(() => morphMembershipToData(membership), [membership, membertypes, loading]);
return (
<Info
headerless={true}
data={data}
/>
);
};
MembershipInfo.propTypes = {
membership: PropTypes.object.isRequired
};
export default MembershipInfo;
\ No newline at end of file
import * as moment from "moment";
import PropTypes from "prop-types";
import React from "react";
import withExtraProps from "../../Components/HOC/withExtraProps";
import withPropMap from "../../Components/HOC/withPropMap";
import {usePatchMembershipMutation} from "../../Store/services/members";
import {usePatchMyMembershipMutation} from "../../Store/services/user";
import InfoForm from "../InfoForm";
import MembershipForm from "./MembershipForm";
import MembershipInfo from "./MembershipInfo";
const formObjectToMembershipFormPropsMap = ({ formObject, handleFormObjectChange, ...rest_props}) =>
({ membership: formObject, handleMembershipChange: handleFormObjectChange, ...rest_props});
const formObjectToMembershipInfoPropsMap = ({ info, ...rest_props}) =>
({ membership: info, ...rest_props});
const membershipWithDatesToApi = (membership) => ({
...membership,
profile: membership.profile.url,
association: membership.association.url,
date_joined: membership.date_joined && moment(membership.date_joined).format("YYYY-MM-DD"),
date_left: membership.date_left && moment(membership.date_left).format("YYYY-MM-DD"),
});
const MembershipInfoForm = ({ membership, showInfoFormStateButton, initialInfoOrFormState, MoreActionButtonGroup, postSubmit, postCancel, is_you, association }) => {
const [ patchMembership ] = usePatchMembershipMutation();
const [ patchMyMembership ] = usePatchMyMembershipMutation();
const fieldAndValueToStateChanges = (field, value) => ({ [field]: value });
const onSubmit = (membership) => is_you
? patchMyMembership(membershipWithDatesToApi(membership)).then(()=>postSubmit(membership))
: patchMembership(membershipWithDatesToApi(membership)).then(()=>postSubmit(membership));
const MembershipInfoComponent = withExtraProps(withPropMap(MembershipInfo, formObjectToMembershipInfoPropsMap), {
association: association
});
const MembershipFormComponent = withExtraProps(withPropMap(MembershipForm, formObjectToMembershipFormPropsMap), {
is_you: is_you,
association: association
});
return (
<InfoForm
onSubmit={onSubmit}
onCancel={postCancel}
title={"Membership"}
infoFormObject={membership}
InfoComponent={MembershipInfoComponent}
FormComponent={MembershipFormComponent}
fieldAndValueToStateChanges={fieldAndValueToStateChanges}
showInfoFormStateButton={showInfoFormStateButton}
initialInfoOrFormState={initialInfoOrFormState}
MoreActionButtonGroup={MoreActionButtonGroup}
/>
);
};
const dummyFunction = () => {};
MembershipInfoForm.propTypes = {
membership: PropTypes.object.isRequired,
showInfoFormStateButton: PropTypes.bool.isRequired,
initialInfoOrFormState: PropTypes.string.isRequired,
is_you: PropTypes.bool.isRequired,
association: PropTypes.object.isRequired,
MoreActionButtonGroup: PropTypes.elementType,
enableDelete: PropTypes.bool,
postSubmit: PropTypes.func,
postDelete: PropTypes.func,
postCancel: PropTypes.func
};
MembershipInfoForm.defaultProps = {
enableDelete: false,
MoreActionButtonGroup: dummyFunction(),
postSubmit: dummyFunction,
postDelete: dummyFunction,
postCancel: dummyFunction,
};
export default MembershipInfoForm;
\ No newline at end of file
import Button from "@material-ui/core/Button";
import { isEmpty } from "lodash";
import PropTypes from "prop-types";
import React from "react";
import {ValidatorForm} from "react-material-ui-form-validator";
import SpecificDataField from "../../Components/Fields/SpecificDataField";
import Wrapper from "../../Components/Fields/Wrapper";
import {useGetDataFieldsByAssociationQuery} from "../../Store/services/dataFields";
export const SpecificDataForm = ({ specificData, handleSpecificDataChange, association, onSubmit, onCancel}) => {
const { data: dataFields } = useGetDataFieldsByAssociationQuery(association.slug);
const dataFieldUrlToSpecificDataValue = (url) => specificData?.find(data=> data.data_field === url)?.value || "";
// const handleSubmit = () => {
// dataFields.forEach(dataField=>{
// if (!isEmpty(specificDataChanges[dataField.url])) {
// if (specificDataWithChanges[dataField.url].url) {
// API.callv4({
// url: specificDataWithChanges[dataField.url].url,
// method: "PATCH",
// object: specificDataChanges[dataField.url],
// on_succes: (data) => {
// onUpdate(data);
// alerthandler.success("Saved data field");
// },
// on_failure: (err) => {
// alerthandler.failure("Saving of data field went wrong");
// console.error(err);
// },
// });
// } else {
// API.callv4({
// url: "/association_data",
// method: "POST",
// object: {...specificDataChanges[dataField.url], association: association.url, data_field: dataField.url, membership: membership.url},
// on_succes: (data) => {
// onUpdate(data);
// alerthandler.success("Saved data field");
// },
// on_failure: (err) => {
// alerthandler.failure("Saving of data field went wrong");
// console.error(err);
// },
// });
// }
// }
// });
// };
return (
<ValidatorForm
onSubmit={onSubmit}
onError={errors => console.log(errors)}
>
{ dataFields && dataFields.map((dataField, d)=>(
<Wrapper key={d}>
<SpecificDataField
field={dataField}
helperText={dataField.helper_text}
value={dataFieldUrlToSpecificDataValue(dataField.url)}
onChange={handleSpecificDataChange}
disabled={(dataField.board_only || (dataField.mandatory && dataFieldUrlToSpecificDataValue(dataField.url) === "True")) && !association.is_board}
/>
</Wrapper>
)) }
<Wrapper>
<div/>
<div>
<Button variant={"contained"} color={"primary"} type={"submit"}>
Save
</Button>
&nbsp;
<Button variant={"contained"} onClick={onCancel}>
Cancel
</Button>
</div>
</Wrapper>
</ValidatorForm>
);
};
SpecificDataForm.propTypes = {
specificData: PropTypes.array.isRequired,
association: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
handleSpecificDataChange: PropTypes.func.isRequired
};
export default SpecificDataForm;
\ No newline at end of file
import PropTypes from "prop-types";
import React, {useMemo} from "react";
import Info from "../../Components/Info/Info";
import {useGetDataFieldsByAssociationQuery} from "../../Store/services/dataFields";
const SpecificData = ({ specificData, association }) => {
const { data: dataFields } = useGetDataFieldsByAssociationQuery(association.slug);
const dataFieldToTypeDict = Object.fromEntries(dataFields?.map(dataField=>[dataField.name, dataField.type]) || []);
const data = useMemo(()=>
Object.fromEntries(specificData.map(sp=>[sp.name, dataFieldToTypeDict[sp.name] === "Boolean"
? (sp.value === "True" ? "Yes" : "No")
: sp.value