Commit 018011de authored by TJHeeringa's avatar TJHeeringa

Finished Members Current

parent 5a7e8359
......@@ -74,9 +74,9 @@ export class MemberGrid extends Component {
this.setState({selectedData: selectedData});
};
onCommitChanges = ({ added, changed, dummy }) => {
onCommitChanges = ({ added, changed, deleted }) => {
let {rows} = this.state;
console.log({added, changed, dummy});
console.log({added, changed, deleted});
rows = rows.map((row, index) => (changed[index] ? {...row, ...changed[index]} : row));
let edited_row_index = Object.keys(changed)[0];
let row = rows[edited_row_index];
......@@ -192,8 +192,6 @@ export class MemberGrid extends Component {
<TableEditRow />
<TableEditColumn
showEditCommand
// showDeleteCommand
// messages={{deleteCommand:'Archive'}}
/>
<TableHeaderRow
showSortingControls
......
......@@ -14,7 +14,7 @@ import {
GroupingPanel,
ExportPanel,
Toolbar,
DragDropProvider,
DragDropProvider, TableEditRow, TableEditColumn,
} from "@devexpress/dx-react-grid-material-ui";
import {
FilteringState, IntegratedFiltering, IntegratedPaging, IntegratedSelection,
......@@ -25,47 +25,52 @@ import {
SortingState,
DataTypeProvider,
GroupingState,
IntegratedGrouping,
IntegratedGrouping, EditingState,
} from "@devexpress/dx-react-grid";
import {Getter, Plugin} from "@devexpress/dx-react-core";
import { GridExporter } from '@devexpress/dx-react-grid-export';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import Chip from '@material-ui/core/Chip';
import Input from '@material-ui/core/Input';
import Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
import { fade } from '@material-ui/core/styles/colorManipulator';
import { withStyles } from '@material-ui/core/styles';
import PropTypes from "prop-types";
import saveAs from 'file-saver';
import { Helper } from "App/Helper";
const CurrencyFormatter = ({ value }) => (
value.toLocaleString('en-GB', { style: 'currency', currency: 'euro' })
);
// ****** Styling ******
const CurrencyTypeProvider = props => (
<DataTypeProvider
formatterComponent={CurrencyFormatter}
{...props}
/>
);
const styles = theme => ({
tableStriped: {
'& tbody tr:nth-of-type(odd)': {
backgroundColor: fade(theme.palette.primary.main, 0.15),
},
},
});
const DateFormatter = ({ value }) => (
<span>
{value.toLocaleDateString()}
</span>
const TableComponentBase = ({ classes, ...restProps }) => (
<Table.Table
{...restProps}
className={classes.tableStriped}
/>
);
const DateTypeProvider = props => (
<DataTypeProvider {...props} formatterComponent={DateFormatter} />
);
export const TableComponent = withStyles(styles, { name: 'TableComponent' })(TableComponentBase);
const ExtremeTable = (props) => {
const { headers, rows, getRowId,
defaultSorting, defaultFilters, defaultHiddenColumnNames, pageSizes,
showSelect, showSelectAll, showExporter, showGrouping, selectByRowClick,
currencyColumns,
rowSelectionEnabledFilter, selection: {selection, setSelection}} = props;
showSelect, showSelectAll, showEditing, showExporter, showGrouping, selectByRowClick, onChange,
booleanColumns, choiceColumns, choiceSelectionOptions, currencyColumns, dateColumns, numberColumns,
rowSelectionEnabledFilter, editingStateColumnExtensions, selection: {selection, setSelection}} = props;
if (setSelection === undefined){
const [selection, setSelection] = useState([]);
}
// ****** Functions ******
// ****** Functions ******
const getHiddenColumnsFilteringExtensions = hiddenColumnNames => hiddenColumnNames
.map(columnName => ({
......@@ -77,23 +82,157 @@ const ExtremeTable = (props) => {
getHiddenColumnsFilteringExtensions(hiddenColumnNames),
);
const onSave = (workbook) => {
workbook.csv.writeBuffer().then((buffer) => {
saveAs(new Blob([buffer], { type: 'application/octet-stream' }), 'DataGrid.csv');
});
};
const rowSelectionEnabled = row => rowSelectionEnabledFilter(row, selection);
// ****** States ******
// If there is no external selection state, create a local one
if (setSelection === undefined){
const [selection, setSelection] = useState([]);
}
const [pageSize, setPageSize] = useState(10);
const [currentPage, setCurrentPage] = useState(0);
const [sorting, setSorting] = useState(defaultSorting);
const [grouping, setGrouping] = useState([]);
const [filteringColumnExtensions, setFilteringColumnExtensions] = useState(getHiddenColumnsFilteringExtensions(defaultHiddenColumnNames));
const [columnOrder, setColumnOrder] = useState(headers.map(header=>header.name));
// ****** Export ******
const exporterRef = useRef(null);
const startExport = useCallback((options) => {exporterRef.current.exportGrid(options)}, [exporterRef]);
const rowSelectionEnabled = row => rowSelectionEnabledFilter(row, selection);
const onSave = (workbook) => {
workbook.csv.writeBuffer().then((buffer) => {
saveAs(new Blob([buffer], { type: 'application/octet-stream' }), 'DataGrid.csv');
});
};
// ****** Formatters ******
const BooleanFormatter = ({ value }) => <Chip label={value ? 'Yes' : 'No'} />;
const CurrencyFormatter = ({ value }) => value.toLocaleString('en-GB', { style: 'currency', currency: 'EUR' });
// const DateFormatter = ({ value }) => {
// console.log("value", value);
// return (
// <span>
// {value !== undefined ? value.toLocaleDateString() : ''}
// </span>
// );
// };
// ****** Editors ******
const BooleanEditor = ({ value, onValueChange, disabled }) => {
return (
<Select
input={<Input />}
value={value ? 'Yes' : 'No'}
onChange={event => onValueChange(event.target.value === 'Yes')}
style={{ width: '100%' }}
disabled={disabled}
>
<MenuItem value="Yes">
Yes
</MenuItem>
<MenuItem value="No">
No
</MenuItem>
</Select>
)
};
const ChoiceEditor = ({ value, onValueChange, column }) => {
let options = choiceSelectionOptions[column.name];
return (
<Select
input={<Input />}
value={value}
onChange={event => onValueChange(event.target.value)}
style={{ width: '100%' }}
>
{options.map((option,o)=>
<MenuItem key={o} value={option}>
{option}
</MenuItem>
)}
</Select>
)
};
const DateEditor = ({ value, onValueChange }) => {
return (
<Input
value={value}
// onChange={event => onValueChange(event.target.value)}
style={{ width: '100%' }}
type={'date'}
/>
)
};
const NumberEditor = ({ value, onValueChange }) => {
return (
<Input
value={value}
onChange={event => onValueChange(event.target.value)}
style={{ width: '100%' }}
type={'number'}
/>
)
};
// ****** Providers ******
const BooleanTypeProvider = props => (
<DataTypeProvider
formatterComponent={BooleanFormatter}
editorComponent={BooleanEditor}
{...props}
/>
);
const ChoiceTypeProvider = props => (
<DataTypeProvider
editorComponent={ChoiceEditor}
{...props}
/>
);
const CurrencyTypeProvider = props => (
<DataTypeProvider
formatterComponent={CurrencyFormatter}
{...props}
/>
);
const DateTypeProvider = props => (
<DataTypeProvider
editorComponent={DateEditor}
// formatterComponent={DateFormatter}
{...props}
/>
);
const NumberTypeProvider = props => (
<DataTypeProvider
editorComponent={NumberEditor}
{...props}
/>
);
const onCommitChanges = ({ added, changed, deleted }) => {
console.log({added, changed, deleted});
let updated_rows = rows.map((row, index) => (changed[index] ? {...row, ...changed[index]} : row));
let edited_row_index = Object.keys(changed)[0];
let edited_row = updated_rows[edited_row_index];
let original_row = rows[edited_row_index];
let differences = Helper.objectDifference(original_row, edited_row);
onChange(differences, original_row, edited_row, edited_row_index)
};
return (
<Paper>
......@@ -102,14 +241,32 @@ const ExtremeTable = (props) => {
columns={headers}
getRowId={getRowId}
>
<BooleanTypeProvider
for={booleanColumns}
/>
<ChoiceTypeProvider
for={choiceColumns}
/>
<CurrencyTypeProvider
for={currencyColumns}
/>
<DateTypeProvider
for={dateColumns}
/>
<NumberTypeProvider
for={numberColumns}
/>
<DragDropProvider />
<SortingState
sorting={sorting}
onSortingChange={setSorting}
/>
{showEditing &&
<EditingState
onCommitChanges={onCommitChanges}
columnExtensions={editingStateColumnExtensions}
/>
}
{showGrouping &&
<GroupingState
grouping={grouping}
......@@ -147,7 +304,17 @@ const ExtremeTable = (props) => {
{showGrouping &&
<IntegratedGrouping/>
}
<Table />
<Table
tableComponent={TableComponent}
/>
{showEditing &&
<TableEditRow/>
}
{showEditing &&
<TableEditColumn
showEditCommand
/>
}
<TableColumnReordering
order={columnOrder}
onOrderChange={setColumnOrder}
......@@ -216,6 +383,8 @@ ExtremeTable.propTypes = {
showSelect: PropTypes.bool,
/* Boolean for showing the checkbox with allows selecting of all at once */
showSelectAll: PropTypes.bool,
/* Setting to true enables the option to edit rows */
showEditing: PropTypes.bool,
/* Setting to true creates an option to export to .csv */
showExporter: PropTypes.bool,
/* Boolean for enabling the option to group options */
......@@ -233,6 +402,10 @@ ExtremeTable.propTypes = {
columnName: PropTypes.string,
direction: PropTypes.string,
})),
editingStateColumnExtensions: PropTypes.arrayOf(PropTypes.exact({
columnName: PropTypes.string,
editingEnabled: PropTypes.bool,
})),
defaultHiddenColumnNames: PropTypes.arrayOf(PropTypes.string),
/* Function that is called on the row to give the indentifier of said row; setting to unidentified gives the default of returning the index */
getRowId: PropTypes.func,
......@@ -250,14 +423,19 @@ ExtremeTable.propTypes = {
ExtremeTable.defaultProps = {
showSelect: true,
showSelectAll: true,
showEditing: false,
showExporter: false,
showGrouping: true,
selectByRowClick: false,
pageSizes: [5,10,25,50,100,500],
defaultFilters: [{ columnName: 'given_name', operation: 'startsWith', value: ''}],
defaultFilters: [],
defaultSorting: [{ columnName: 'given_name', direction: 'asc' }],
defaultHiddenColumnNames: [],
editingStateColumnExtensions: [],
booleanColumns: [],
choiceColumns: [],
currencyColumns: [],
dateColumns: [],
rowSelectionEnabledFilter: (row, selection) => true,
selection: {selection: undefined, setSelection: undefined}
};
......
import React, { Component } from 'react'
import 'react-table/react-table.css'
import { Helper } from "App/Helper";
import { AlertHandlerContext } from "../../Contexts/AlertHandler";
import ExtremeTable from "App/Components/Tables/ExtremeTable"
const MemberTable = props => {
const { rows, headers, memberTypes, editingStateColumnExtensions, choiceSelectionOptions } = props;
const extremeHeaders = headers.map(header=>{return {"name": header.name, "title": header.title} });
return (
<div className="MemberGrid">
<ExtremeTable
{...props}
rows={rows}
headers={extremeHeaders}
showExporter={true}
showEditing={true}
editingStateColumnExtensions={editingStateColumnExtensions}
choiceSelectionOptions={choiceSelectionOptions}
/>
</div>
);
};
export default MemberTable;
\ No newline at end of file
......@@ -97,10 +97,44 @@ export class Helper {
}
static isEmpty(obj) {
return Object.keys(obj).length === 0;
}
static isEmpty = (obj) => Object.keys(obj).length === 0;
// https://stackoverflow.com/questions/33232823/how-to-compare-two-objects-and-get-key-value-pairs-of-their-differences
static isObject = x => Object (x) === x;
static empty = {};
static objectLeftDifference = (left = {}, right = {}, rel = "left") =>
Object.entries (left)
.map(
([ k, v ]) =>
this.isObject (v) && this.isObject (right[k])
? [ k, this.objectLeftDifference (v, right[k], rel) ]
: right[k] !== v
? [ k, { [rel]: v } ]
: [ k, this.empty ]
)
.reduce(
(acc, [ k, v ]) =>
v === this.empty
? acc
: { ...acc, [k]: v },
this.empty
);
static objectMerge = (left = {}, right = {}) =>
Object.entries (right)
.reduce(
(acc, [ k, v ]) =>
this.isObject (v) && this.isObject(left[k])
? { ...acc, [k]: this.objectMerge(left[k], v) }
: { ...acc, [k]: v }
, left
);
static objectDifference = (original = {}, modified = {}) => this.objectMerge(
this.objectLeftDifference (original, modified, "original"),
this.objectLeftDifference (modified, original, "modified")
);
static intersection = (array_1, array_2) => {
array_1.filter(value => array_2.includes(value))
......
......@@ -238,21 +238,21 @@ class DataField extends Component {
</SelectValidator>
</div>
}
<div className={classes.wrapper}>
<GradeIcon className={classes.icon} color="action"/>
<SelectValidator
{...textEditorProps('board only')}
validators={['required']}
value={data_field.board_only}
onChange={(event) => {
this.handleDataChange('board_only', event.target.value)
}}
helperText={"This is a field of which the content only the board is going to see the content of."}
>
<MenuItem value={true}>Yes</MenuItem>
<MenuItem value={false}>No</MenuItem>
</SelectValidator>
</div>
{/*<div className={classes.wrapper}>*/}
{/*<GradeIcon className={classes.icon} color="action"/>*/}
{/*<SelectValidator*/}
{/*{...textEditorProps('board only')}*/}
{/*validators={['required']}*/}
{/*value={data_field.board_only}*/}
{/*onChange={(event) => {*/}
{/*this.handleDataChange('board_only', event.target.value)*/}
{/*}}*/}
{/*helperText={"This is a field of which the content only the board is going to see the content of."}*/}
{/*>*/}
{/*<MenuItem value={true}>Yes</MenuItem>*/}
{/*<MenuItem value={false}>No</MenuItem>*/}
{/*</SelectValidator>*/}
{/*</div>*/}
<div className={classes.wrapper}>
<EuroIcon className={classes.icon} color="action"/>
{(data_field.type === 'Choice' || data_field.type === 'Boolean') ? (
......
import React, { Component } from 'react'
import { MemberGrid } from 'App/Components/Member/MemberGrid'
import React, { Component } from 'react';
import { MemberGrid } from 'App/Components/Member/MemberGrid';
import MemberTable from 'App/Components/Tables/MemberTable';
import { Helper } from "App/Helper";
import { withStyles } from "@material-ui/core";
import {AlertHandlerContext} from "../../Contexts/AlertHandler";
const MembersCurrentStyles = theme => ({
......@@ -16,12 +18,22 @@ class MembersCurrent extends Component {
constructor(props) {
super(props);
this.state = {
data_fields: undefined,
member_types: undefined,
members_current: undefined
}
}
componentDidMount() {
Helper.api_call(
this.props.association.url + '/data_fields?limit=1000',
"GET",
undefined,
'json',
(data) => {
let data_fields = data.results;
this.setState({data_fields: data_fields})
});
Helper.api_call(
this.props.association.url + '/membertypes?limit=1000',
"GET",
......@@ -44,19 +56,44 @@ class MembersCurrent extends Component {
}
morphDataToMemberGridProfiles = () => {
return this.state.members_current.map(nested_member_profile => {
let member_profile = nested_member_profile.profile;
member_profile.type = nested_member_profile.type.type;
member_profile.date_joined = nested_member_profile.date_joined;
member_profile.urls = {'membership': nested_member_profile.url};
nested_member_profile.association_specific_data.forEach(data => {
return this.state.members_current.map(membership => {
// membership has profile, membertype and association specific data attached
// this flattens it to a single object that is no longer nested.
let member_profile = membership.profile;
member_profile.type = membership.type.type;
member_profile.membership_fee = membership.type.membership_fee;
member_profile.date_joined = membership.date_joined;
member_profile.date_left = membership.date_left;
membership.association_specific_data.forEach(data => {
member_profile[data.name] = data.value;
member_profile.urls[data.name] = data.url;
});
// add urls to find the relevant url for the PATCH/PUT when editing the profile
member_profile.urls = {
'membership': membership.url,
'specific_data': {},
'data_fields': {},
'member_types': {},
};
this.state.data_fields.forEach(data => {
member_profile.urls['data_fields'][data.name] = data.url;
});
this.state.member_types.forEach(data => {
member_profile.urls[data.name] = data.url;
member_profile.urls['member_types'][data.name] = data.url;
});
membership.association_specific_data.forEach(data => {
member_profile.urls['specific_data'][data.name] = data.url;
});
// Replace null values with undefined to prevent error later on
let null_keys = [];
Object.entries(member_profile).forEach(([key, value])=> {
if (value === null) {
null_keys.push(key);
}
});
null_keys.forEach(null_key=>member_profile[null_key]=undefined);
return member_profile
});
};
......@@ -76,15 +113,14 @@ class MembersCurrent extends Component {
{columnName: 'address', editingEnabled: false},
{columnName: 'zip_code', editingEnabled: false},
{columnName: 'type', editingEnabled: true},
{columnName: 'membership_fee', editingEnabled: false},
{columnName: 'date_joined', editingEnabled: true},
{columnName: 'date_left', editingEnabled: true},
{columnName: 'bic_code', editingEnabled: false},
{columnName: 'iban', editingEnabled: false},
{columnName: 'is_master', editingEnabled: false},
];
let specific_data = this.state.members_current.map(member_profile => member_profile.association_specific_data);
// TODO this is going to give bugs in the future
specific_data[1].forEach((data, i) =>
this.state.data_fields.forEach((data, i) =>
editingStateColumnExtensions.push(
{columnName: data.name, editingEnabled: true}
)
......@@ -92,28 +128,6 @@ class MembersCurrent extends Component {
return editingStateColumnExtensions
};
onCommitChanges = ({added, changed, archived}) => {
let {rows} = this.state;
if (changed) {
rows = rows.map((row, index) => (changed[index] ? {...row, ...changed[index]} : row));
let row = rows[Object.keys(changed)[0]];
this.patchGroupMembership({
url: row.url,
date_joined: row.date_joined,