Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • maspect/amiv-admintool
  • emustafa/amiv-admintool
  • dvruette/amiv-admintool
  • amiv/amiv-admintool
4 results
Show changes
Showing
with 2126 additions and 865 deletions
import ItemView from './views/itemView';
import EditView from './views/editView';
import { inputGroup, selectGroup, submitButton } from './views/elements';
import TableView from './views/tableView';
import { events as config } from './config.json';
const m = require('mithril');
export class EventView extends ItemView {
constructor() {
super('events');
this.memberships = [];
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
let comissionBadge = m('span.label.label-important', 'Who is resp of this event?');
if (this.data.membership === 'kultur') {
comissionBadge = m('span.label.label-success', 'Kulturi event');
} else if (this.data.membership === 'eestec') {
comissionBadge = m('span.label.label-important', 'EESTEC event');
} else if (this.data.membership === 'limes') {
comissionBadge = m('span.label.label-warning', 'LIMES event');
}
// TODO Question Lio171201:are we missing a "responsible" key?
const detailKeys = [
'title_de',
'rfid',
'location', 'time_start', 'time_end',
'show_website', 'catchphrase',
'time_register_start', 'price', 'allow_email_signup'];
return m('div', [
m('h1', `${this.data.title_de}`),
comissionBadge,
m('table', detailKeys.map(key => m('tr', [
m('td.detail-descriptor', config.keyDescriptors[key]),
m('td', this.data[key] ? this.data[key] : ''),
]))),
m('h2', 'Location'), m('br'),
m(TableView, {
resource: 'events',
keys: ['event.location'],
query: {
where: { user: this.id },
embedded: { group: 1 },
},
}),
m('h2', 'Signups'), m('br'),
m(TableView, {
resource: 'events',
keys: ['event.title_de'],
query: {
where: { user: this.id },
embedded: { event: 1 },
},
}),
]);
}
}
class EventEdit extends EditView {
constructor(vnode) {
super(vnode, 'events');
}
getForm() {
return m('form', [
m('div.row', [
m(inputGroup, this.bind({ title: 'Deutscher Titel', name: 'title_de' })),
m(inputGroup, this.bind({ title: 'English Title', name: 'title_en' })),
m(inputGroup, this.bind({ title: 'Location', name: 'location' })),
// m(inputGroup, this.bind({ title: 'Date-start', name: 'datetimepicker1' })),
// $('#datetimepicker1').datetimepicker();
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'May non-AMIV members register?',
name: 'allow_email_signup',
options: [true, false],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Show on the website?',
name: 'show_website',
options: [true, false],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Piority from 1 to 10?',
name: 'priority',
// could be done with array.apply:
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
})),
]),
m('span', JSON.stringify(this.data)),
m('span', JSON.stringify(this.errors)),
]);
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
return m('form', [
this.getForm(),
m(submitButton, {
active: this.valid,
args: {
onclick: this.submit('PATCH', config.patchableKeys),
class: 'btn-warning',
},
text: 'Update',
}),
]);
}
}
export class NewEvent extends EventEdit {
constructor(vnode) {
super(vnode);
this.data = {
title_de: 'Unvollstaendiges Event',
priority: 7,
show_website: false,
};
this.valid = false;
// if the creation is finished, UI should switch to new Event
this.callback = (response) => { m.route.set(`/events/${response.data._id}`); };
}
view() {
return m('form', [
this.getForm(),
m(submitButton, {
active: this.valid,
args: {
onclick: this.submit('POST', config.patchableKeys),
class: 'btn-warning',
},
text: 'Create',
}),
]);
}
}
export class EventModal {
constructor() {
this.edit = false;
}
view() {
if (this.edit) {
return m(EventEdit, { onfinish: () => { this.edit = false; m.redraw(); } });
}
// else
return m('div', [
m('div.btn.btn-default', { onclick: () => { this.edit = true; } }, 'Edit'),
m('br'),
m(EventView),
]);
}
}
import m from 'mithril';
import { styler } from 'polythene-core-css';
import {
RadioGroup, Switch, Dialog, Button, Tabs, Icon, TextField,
} from 'polythene-mithril';
import { FileInput, ListSelect, DatalistController } from 'amiv-web-ui-components';
import { TabsCSS, ButtonCSS } from 'polythene-css';
// eslint-disable-next-line import/extensions
import { apiUrl, ownUrl } from 'networkConfig';
import { ResourceHandler } from '../auth';
import { colors } from '../style';
import { icons } from '../views/elements';
import EditView from '../views/editView';
import Base64 from '../base64';
ButtonCSS.addStyle('.nav-button', {
color_light_border: 'rgba(0, 0, 0, 0.09)',
color_light_disabled_background: 'rgba(0, 0, 0, 0.09)',
color_light_disabled_border: 'transparent',
});
TabsCSS.addStyle('.edit-tabs', {
color_light: '#555555',
// no hover effect
color_light_hover: '#555555',
color_light_selected: colors.amiv_blue,
color_light_tab_indicator: colors.amiv_blue,
});
const styles = [
{
'.imgPlaceholder': {
background: '#999',
position: 'relative',
},
'.imgPlaceholder > div': {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
'font-size': '16px',
display: 'flex',
'justify-content': 'center',
'align-items': 'center',
},
'.imgBackground': {
'background-size': 'contain',
'background-position': 'center',
'background-repeat': 'no-repeat',
},
},
];
styler.add('eventEdit', styles);
export default class newEvent extends EditView {
constructor(vnode) {
super(vnode);
this.currentpage = 1;
// Create a usercontroller to handle the moderator field
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
// check whether the user has the right to create events or can only propose
this.rightSubmit = !m.route.get().startsWith('/proposeevent');
// proposition URL-link decoder
if (this.rightSubmit && m.route.param('proposition')) {
const data = JSON.parse(Base64.decode(m.route.param('proposition')));
this.form.data = data;
}
if (this.form.data.priority === 10) this.form.data.high_priority = true;
// read additional_fields to make it editable
if (this.form.data.additional_fields) {
const copy = JSON.parse(this.form.data.additional_fields);
this.form.data.add_fields_sbb = 'sbb_abo' in copy.properties;
this.form.data.add_fields_food = 'food' in copy.properties;
this.form.data.additional_fields = null;
let i = 0;
while (`text${i}` in copy.properties) {
this.form.data[`add_fields_text${i}`] = copy.properties[`text${i}`].title;
i += 1;
}
// TODO: find a better solution to keep track of the additional textfields
this.add_fields_text_index = i;
} else {
this.add_fields_text_index = 0;
}
// price can either not be set or set to null
// if it is 0 however, that would mean that there actually is a price that
// you can edit
this.hasprice = 'price' in this.form.data && this.form.data.price !== null;
this.hasregistration = 'spots' in this.form.data || 'time_registration_start' in this.form.data;
}
beforeSubmit() {
// Here comes all the processing from the state of our input form into data send to the api.
// In particular, we have to:
// - remove images from the patch that should not get changed, add images in right format that
// should be changed
// - transfer states like add_fields_sbb etc. into actual additional_fields
// - dependent on user rights, either submit to API or create an event proposal link
const { data } = JSON.parse(JSON.stringify(this.form));
// Images that should be changed have new_{key} set, this needs to get uploaded to the API
// All the other images should be removed from the upload to not overwrite them.
const images = {};
['thumbnail', 'infoscreen', 'poster'].forEach((key) => {
if (this.form.data[`new_${key}`]) {
images[`img_${key}`] = this.form.data[`new_${key}`];
delete data[`new_${key}`];
}
if (data[`img_${key}`] !== undefined && data[`img_${key}`] !== null) {
delete data[`img_${key}`];
}
});
// Merge Options for additional fields
// This is the sceleton schema:
const additionalFields = {
$schema: 'http://json-schema.org/draft-04/schema#',
additionalProperties: false,
title: 'Additional Fields',
type: 'object',
properties: {},
required: [],
};
if (data.add_fields_sbb) {
additionalFields.properties.sbb_abo = {
type: 'string',
title: 'SBB Abonnement',
enum: ['None', 'GA', 'Halbtax', 'Gleis 7', 'HT + Gleis 7'],
};
additionalFields.required.push('sbb_abo');
}
if (data.add_fields_food) {
additionalFields.properties.food = {
type: 'string',
title: 'Food',
enum: ['Omnivor', 'Vegi', 'Vegan', 'Other'],
};
additionalFields.properties.food_special = {
type: 'string',
title: 'Special Food Requirements',
};
additionalFields.required.push('food');
}
// There can be an arbitrary number of text fields added.
let i = 0;
while (`add_fields_text${i}` in data) {
const fieldName = `text${i}`;
additionalFields.properties[fieldName] = {
type: 'string',
minLength: 1,
title: data[`add_fields_text${i}`],
};
additionalFields.required.push(fieldName);
delete data[`add_fields_text${i}`];
i += 1;
}
// Remove our intermediate form states from the the data that is uploaded
if ('add_fields_sbb' in data) delete data.add_fields_sbb;
if ('add_fields_food' in data) delete data.add_fields_food;
// If there are no additional_fields, the properties are empty, and we null the whole field,
// otherwise we send a json string of the additional fields object
if (Object.keys(additionalFields.properties).length > 0) {
data.additional_fields = JSON.stringify(additionalFields);
} else {
data.additional_fields = null;
}
// Translate state high_priority into a priority for the event
if (data.high_priority === true) data.priority = 10;
else data.priority = 1;
delete data.high_priority;
// if spots is not set, also remove 'allow_email_signup'
if (!('spots' in data) && 'allow_email_signup' in data
&& !data.allow_email_signup) {
delete data.allow_email_signup;
}
// Propose Event <=> Submit Changes dependent on the user rights
if (this.rightSubmit) {
// Submition tool
// Change moderator from user object to user id
if (data.moderator) data.moderator = data.moderator._id;
// first upload the data as JSON, then the images as form data
this.submit(data).then(({ _id, _etag }) => {
if (Object.keys(images).length > 0) {
// there are changed images to upload, they are added as an additional PATCH on this event
const imageForm = new FormData();
Object.keys(images).forEach(key => imageForm.append(key, images[key]));
imageForm.append('_id', _id);
imageForm.append('_etag', _etag);
this.controller.patch(imageForm).then(() => {
m.route.set(`/events/${_id}`);
})
.catch(() => {
this.controller.id = _id;
this.controller.handler.getItem(_id).then((item) => {
this.controller.data = item;
this.form.data = item;
this.controller.changeModus('edit');
window.history.replaceState({}, '', `/events/${_id}`);
});
});
} else {
m.route.set(`/events/${_id}`);
}
});
} else {
// Propose tool
Dialog.show({
title: 'Congratulations!',
body: [
m(
'div',
'You sucessfuly setup an event.',
'Please send this link to the respectiv board member for validation.',
),
m('input', {
type: 'text',
style: { width: '335px' },
value: `${ownUrl}/newevent?${m.buildQueryString({
proposition: Base64.encode(JSON.stringify(this.form.data)),
})}`,
id: 'textId',
}),
],
backdrop: true,
footerButtons: [
m(Button, {
label: 'Copy',
events: {
onclick: () => {
const copyText = document.getElementById('textId');
copyText.select();
document.execCommand('copy');
},
},
}),
],
});
}
}
view() {
// load image urls from the API data
['thumbnail', 'poster', 'infoscreen'].forEach((key) => {
const img = this.form.data[`img_${key}`];
if (typeof (img) === 'object' && img !== null && 'file' in img) {
// the data from the API has a weird format, we only need the url to display the image
this.form.data[`img_${key}`] = { url: `${apiUrl}${img.file}` };
}
});
// Define the number of Tabs and their titles
const titles = ['Event Description', 'When and Where?', 'Signups', 'Internal Info'];
if (this.rightSubmit) titles.push('Images');
// Data fields of the event in the different tabs. Ordered the same way as the titles
const keysPages = [[
'title_en',
'catchphrase_en',
'description_en',
'title_de',
'catchphrase_de',
'description_de',
],
['time_start', 'time_end', 'location'],
['price', 'spots', 'time_register_start', 'time_register_end', 'time_deregister_end'],
['moderator', 'time_advertising_start', 'time_advertising_end'],
[],
];
// Look which Tabs have errors
const errorPages = keysPages.map(keysOfOnePage => keysOfOnePage.map((key) => {
if (this.form.errors && key in this.form.errors) return this.form.errors[key].length > 0;
return false;
}).includes(true));
// Navigation Buttons to go to next/previous page
const buttonRight = m(Button, {
label: m('div.pe-button__label', m(Icon, {
svg: { content: m.trust(icons.ArrowRight) },
style: { top: '-5px', float: 'right' },
}), 'next'),
disabled: this.currentpage === titles.length,
ink: false,
border: true,
className: 'nav-button',
events: { onclick: () => { this.currentpage = Math.min(this.currentpage + 1, 5); } },
});
const buttonLeft = m(Button, {
label: m('div.pe-button__label', m(Icon, {
svg: { content: m.trust(icons.ArrowLeft) },
style: { top: '-5px', float: 'left' },
}), 'previous'),
disabled: this.currentpage === 1,
ink: false,
border: true,
className: 'nav-button',
events: { onclick: () => { this.currentpage = Math.max(1, this.currentpage - 1); } },
});
const radioButtonSelectionMode = m(RadioGroup, {
name: 'Selection Mode',
buttons: [
{
value: 'fcfs',
label: 'First come, first serve',
},
{
value: 'manual',
label: 'Selection made by organizer',
},
],
onChange: (state) => {
this.selection_strategy = state.value;
this.form.data.selection_strategy = state.value;
},
value: this.selection_strategy,
});
// Processing for additional text fields users have to fill in for the signup.
const addFieldsText = [];
let i = 0;
while (`add_fields_text${i}` in this.form.data) {
addFieldsText.push(this.form._renderField(`add_fields_text${i}`, {
type: 'string',
label: `Label for Textfield ${i}`,
}));
const fieldIndex = i;
addFieldsText.push(m(Button, {
label: `Remove Textfield ${i}`,
className: 'red-row-button',
events: {
onclick: () => {
let index = fieldIndex;
while (`add_fields_text${index + 1}` in this.form.data) {
this.form.data[`add_fields_text${index}`] = this.form.data[
`add_fields_text${index + 1}`];
index += 1;
}
delete this.form.data[`add_fields_text${index}`];
this.add_fields_text_index = index;
},
},
}));
i += 1;
}
// checks currentPage and selects the fitting page
return this.layout([
// navigation bar
// all pages are displayed, current is highlighted,
// validation errors are shown per page by red icon-background
m(Tabs, {
className: 'edit-tabs',
// it would be too easy if we could set the background color in the theme class
style: { backgroundColor: colors.orange },
onChange: ({ index }) => { this.currentpage = index + 1; },
centered: true,
selectedTabIndex: this.currentpage - 1,
}, [...titles.entries()].map((numAndTitle) => {
const buttonAttrs = { label: numAndTitle[1] };
if (errorPages[numAndTitle[0]]) {
// in case of an error, put an error icon before the tab label
buttonAttrs.label = m('div', m(Icon, {
svg: { content: m.trust(icons.error) },
style: { top: '-2px', 'margin-right': '4px' },
}), numAndTitle[1]);
}
return buttonAttrs;
})),
m('div.maincontainer', { style: { height: 'calc(100vh - 180px)', 'overflow-y': 'auto' } }, [
// page 1: title & description
m('div', {
style: { display: (this.currentpage === 1) ? 'block' : 'none' },
}, [
...this.form.renderSchema(['title_en', 'catchphrase_en']),
this.form._renderField('description_en', {
type: 'string',
label: 'English Description',
multiLine: true,
rows: 5,
}),
...this.form.renderSchema(['title_de', 'catchphrase_de']),
this.form._renderField('description_de', {
type: 'string',
label: 'German Description',
multiLine: true,
rows: 5,
}),
]),
// page 2: when & where
m('div', {
style: { display: (this.currentpage === 2) ? 'block' : 'none' },
}, this.form.renderSchema(['time_start', 'time_end', 'location'])),
// page 3: registration
m('div', {
style: { display: (this.currentpage === 3) ? 'block' : 'none' },
}, [
m(Switch, {
label: 'people have to pay something to attend this event',
style: { 'margin-bottom': '5px' },
checked: this.hasprice,
onChange: ({ checked }) => {
this.hasprice = checked;
if (!checked) {
// if it originally had a price, set to null, otherwise delete
if (this.controller.data.price) this.form.data.price = null;
else delete this.form.data.price;
}
},
}),
...(this.hasprice ? this.form.renderSchema(['price']) : []),
m('br'),
m(Switch, {
label: 'people have to register to attend this event',
style: { 'margin-bottom': '5px' },
checked: this.hasregistration,
onChange: ({ checked }) => {
this.hasregistration = checked;
if (!checked) {
// remove all the data connected to registration
delete this.form.data.spots;
delete this.form.data.time_register_start;
delete this.form.data.time_register_end;
delete this.form.data.time_deregister_end;
delete this.form.data.add_fields_sbb;
delete this.form.data.add_fields_food;
delete this.form.data.allow_email_signup;
delete this.form.data.selection_strategy;
}
},
}),
...(this.hasregistration ? this.form.renderSchema([
'spots', 'time_register_start', 'time_register_end', 'time_deregister_end']) : []),
this.hasregistration && this.form._renderField('add_fields_food', {
type: 'boolean',
label: 'Food Limitations',
}),
this.hasregistration && this.form._renderField('add_fields_sbb', {
type: 'boolean',
label: 'SBB Abonnement',
}),
this.hasregistration && m('br'),
...(this.hasregistration ? addFieldsText : []),
this.hasregistration && m('br'),
this.hasregistration && m(Button, {
label: 'Additional Textfield',
className: 'blue-button',
border: true,
events: {
onclick: () => {
this.form.data[`add_fields_text${this.add_fields_text_index}`] = '';
this.add_fields_text_index += 1;
},
},
}),
this.hasregistration && m('br'),
...(this.hasregistration ? this.form.renderSchema(['allow_email_signup']) : []),
m('br'),
...(this.hasregistration ? 'Only check the "Email Signup" box, '
+ 'if you expect non-ETH members to attend the event!' : []),
m('br'),
this.hasregistration && radioButtonSelectionMode,
m('br'),
m(Switch, {
label: 'people can register via an external link',
checked: this.hasexternalregistration,
onChange: ({ checked }) => {
this.hasexternalregistration = checked;
if (!checked) {
delete this.form.data.external_registration;
}
},
}),
this.hasexternalregistration && this.form._renderField('external_registration', {
type: 'string',
label: 'External Registration Link',
}),
]),
// PAGE 4: Internal Info
m('div', {
style: { display: (this.currentpage === 4) ? 'block' : 'none' },
}, [
m('div', { style: { display: 'flex', 'margin-top': '5px' } }, [
m(TextField, {
label: 'Moderator: ',
disabled: true,
style: { width: '200px' },
help: 'Can edit the event and see signups.',
}),
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController,
selection: this.form.data.moderator,
listTileAttrs: user => Object.assign(
{},
{ title: `${user.firstname} ${user.lastname}` },
),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.form.data.moderator = data; },
})),
]),
...this.form.renderSchema(['time_advertising_start', 'time_advertising_end']),
...this.form.renderSchema(['type']),
...this.form.renderSchema(['show_website', 'show_announce', 'show_infoscreen']),
// pritority update
this.form._renderField('high_priority', {
type: 'boolean',
label: 'Set high Priority',
}),
m('div', 'Please send an email to info@amiv.ch in order to show your event on'
+ 'the infoscreen until the new infoscreen tool is ready.'),
]),
// page 5: images
m('div', {
style: { display: (this.currentpage === 5) ? 'block' : 'none' },
}, [
m('div', 'Formats for the files: Thumbnail: 1:1, Poster: Any DIN-A, Infoscreen: 16:9'),
// All images and placeholders are placed next to each other in the following div:
m('div', { style: { width: '90%', display: 'flex' } }, [
// POSTER
m('div', { style: { width: '30%' } }, [
m('div', 'Poster'),
// imgPlaceholder has exactly a 1:1 aspect ratio
m('div.imgPlaceholder', { style: { width: '100%', 'padding-bottom': '141%' } }, [
// inside, we display the image. if it has a wrong aspect ratio, grey areas
// from the imgPlaceholder will be visible behind the image
this.form.data.img_poster ? m('div.imgBackground', {
style: { 'background-image': `url(${this.form.data.img_poster.url})` },
// Placeholder in case that there is no image
}) : m('div', 'No Poster'),
]),
m(Button, {
className: 'red-row-button',
borders: false,
label: 'remove',
events: { onclick: () => { this.form.data.img_poster = null; } },
}),
]),
// INFOSCREEN
m('div', { style: { width: '50%', 'margin-left': '5%' } }, [
m('div', 'Infoscreen'),
// imgPlaceholder has exactly a 16:9 aspect ratio
m('div.imgPlaceholder', { style: { width: '100%', 'padding-bottom': '56.25%' } }, [
// inside, we display the image. if it has a wrong aspect ratio, grey areas
// from the imgPlaceholder will be visible behind the image
this.form.data.img_infoscreen ? m('div.imgBackground', {
style: { 'background-image': `url(${this.form.data.img_infoscreen.url})` },
// Placeholder in case that there is no image
}) : m('div', 'No Infoscreen Image'),
]),
m(Button, {
className: 'red-row-button',
borders: false,
label: 'remove',
events: { onclick: () => { this.form.data.img_infoscreen = null; } },
}),
]),
// THUMBNAIL
m('div', { style: { width: '10%', 'margin-left': '5%' } }, [
m('div', 'Thumbnail'),
// imgPlaceholder has exactly a 16:9 aspect ratio
m('div.imgPlaceholder', { style: { width: '100%', 'padding-bottom': '100%' } }, [
// inside, we display the image. if it has a wrong aspect ratio, grey areas
// from the imgPlaceholder will be visible behind the image
this.form.data.img_thumbnail ? m('div.imgBackground', {
style: { 'background-image': `url(${this.form.data.img_thumbnail.url})` },
// Placeholder in case that there is no image
}) : m('div', 'No Thumbnail'),
]),
m(Button, {
className: 'red-row-button',
borders: false,
label: 'remove',
events: { onclick: () => { this.form.data.img_thumbnail = null; } },
}),
]),
]),
// old stuff, goes through all images
['thumbnail', 'poster', 'infoscreen'].map(key => [
// input to upload a new image
m(FileInput, this.form.bind({
name: `new_${key}`,
label: `New ${key} Image`,
accept: 'image/png, image/jpeg',
onChange: ({ value }) => {
// if a new image file is selected, we display it using a data encoded url
const reader = new FileReader();
reader.onload = ({ target: { result } }) => {
this.form.data[`img_${key}`] = { url: result };
m.redraw();
};
reader.readAsDataURL(value);
this.form.data[`new_${key}`] = value;
},
})),
]),
]),
// bottom back & forth Buttons
m('div', {
style: {
display: 'flex',
'justify-content': 'space-between',
padding: '35px',
'padding-top': '20px',
},
}, [buttonLeft, buttonRight]),
]),
], this.rightSubmit ? 'submit' : 'propose', false);
}
}
import { styler } from 'polythene-core-css';
import m from 'mithril';
import { Button, RadioGroup, TextField } from 'polythene-mithril';
import EditView from '../views/editView';
const draftStyle = [
{
'.footer': {
position: 'fixed',
left: 0,
bottom: 0,
width: '100%',
'background-color': '#E8462B',
color: '#FFFFFF',
'text-align': 'right',
}
}
]
styler.add('eventDraft', draftStyle);
export default class eventDraft extends EditView {
constructor(vnode) {
super(vnode, 'events');
this.data = {};
}
view(){
const radioButtonSelectionMode = m(RadioGroup, {
name: 'Selection Mode',
buttons: [
{
value: true,
label: 'Yes, create the project.',
},
{
value: false,
label: 'No, deny the request.',
},
],
onChange: ({ value }) => { this.data.create = value; console.log(this.data); }
});
const Comment = m(TextField, {
label: 'Comment',
required: true,
onChange: ({ value }) => this.data.comment = value,
});
// Submit Button
const buttonMaker = m(Button, {
label: 'Submit Request',
color: 'white',
// Error pop-up in case not all mandatory fields were completed
// CURRENTLY: Error triggered onclick
//if(one of the fields isn't )
events: {
onclick: () => alert('You did not complete all prioritary fields!'),
}
});
return m('div', [
m("main", [
m('h2', { class: 'title' }, 'Creating a new event:'),
m('h3', 'Do you wish to create this event?(Admin only): '),
radioButtonSelectionMode,
Comment,
m('div.footer', buttonMaker),
]
)
]
)
}
}
import m from 'mithril';
import viewEvent from './viewEvent';
import newEvent from './newEvent';
export default class EventModal {
constructor() {
this.edit = false;
}
view() {
if (this.edit) {
return m(newEvent);
}
return m(viewEvent, { onEdit: () => { this.edit = true; } });
}
}
import m from 'mithril';
import {
TextField,
Button,
Card
} from 'polythene-mithril';
import EditView from '../views/editView';
import {styler} from 'polythene-core-css';
const draftStyle = [
{
'.footer': {
position: 'fixed',
left: 0,
bottom: 0,
width: '100%',
'background-color': '#E8462B',
'text-align': 'right',
}
}
]
styler.add('eventDraft', draftStyle);
export default class eventWithExport extends EditView {
constructor(vnode) {
super(vnode, 'events');
this.performedEdits = 0;
}
view() {
// Editable by event creator.
const fieldTitleEn = m(TextField, {
label: 'Event Title [EN]',
required: true,
floatingLabel: true,
dense: true,
onChange : (newState) => {this.title_en = newState.value; console.log(this.title_en);},
value: this.title_en,
});
const fieldDescriptionEn = m(TextField, {
label: 'Description [EN]',
required: true,
floatingLabel: true,
dense: true,
multiLine: true,
rows: 6,
onChange : (newState) => {this.fieldDescriptionEn = newState.value; console.log(this.fieldDescriptionEn);},
value: this.fieldDescriptionEn,
});
// Needs administrator (Kulturi).
const fieldLocation = m(TextField, {
label: 'Location:',
floatingLabel: true,
required: true,
onChange : (newState) => {this.fieldLocation = newState.value; console.log(this.fieldLocation);},
value: this.fieldLocation,
});
// Bottom.
const buttonMaker = m(Button, {
// console.log(JSON.stringify(this.keyDescriptors)),
label: "Submit Request!",
onClick: () => alert("You did not finish the editing of the fields.")
});
// Return!
return m('div', {
style: { height: '100%', 'overflow-y': 'scroll'}
}, [
m('h1', 'For the event creator:', fieldTitleEn , fieldDescriptionEn, 'For the AMIV administrator:', fieldLocation),
m('div.footer', buttonMaker),
]);
}
}
\ No newline at end of file
import m from 'mithril';
import viewEvent from './viewEvent';
import editEvent from './editEvent';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
export default class EventItem {
constructor() {
this.controller = new ItemController('events', { moderator: 1 });
}
view() {
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(editEvent, { controller: this.controller });
return m(viewEvent, { controller: this.controller });
}
}
import m from 'mithril';
import { Button, Checkbox, RadioGroup, IconButton, SVG, TextField } from 'polythene-mithril';
import { styler } from 'polythene-core-css';
import EditView from '../views/editView';
import { icons, textInput, datetimeInput } from '../views/elements';
const style = [
{
'.mywrapper': {
padding: '10px',
},
},
];
styler.add('event-add', style);
export default class newEvent extends EditView {
constructor(vnode) {
super(vnode, 'events', {});
this.currentpage = 1;
this.food = false;
this.sbbAbo = false;
this.data = {};
}
addOne() {
this.currentpage = this.currentpage + 1;
if (this.currentpage === 5) {
this.currentpage = 4;
}
if (this.currentpage === 6) {
this.currentpage = 6;
}
}
subOne() {
this.currentpage = this.currentpage - 1;
if (this.currentpage === 0) {
this.currentpage = 1;
}
if (this.currentpage === 6) {
this.currentpage = 6;
}
}
view() {
if (!this.currentpage) return '';
const firstTableInputs = {
title_en: {
label: 'English Event Title',
},
catchphrase_en: {
label: 'English Catchphrase',
},
description_en: {
label: 'English Description',
multiLine: true,
rows: 5,
},
title_de: {
label: 'German Event Title',
},
catchphrase_de: {
label: 'German Catchphrase',
},
description_de: {
label: 'German Description',
multiLine: true,
rows: 5,
},
};
const thirdTableInputs = {
spots: {
label: 'Number of Spots',
help: '0 for open event',
focusHelp: true,
},
price: {
label: 'Price',
},
time_register_start: {
label: 'Start of Registration',
},
time_register_end: {
label: 'End of Registration',
},
};
const forthTableInputs = {
time_advertising_start: {
label: 'Start of Advertisement',
type: 'datetime',
required: true,
},
time_advertising_end: {
label: 'End of Advertisement',
required: true,
},
priority: {
label: 'Priority',
},
};
const iconRight = m(
IconButton,
{ events: { onclick: () => { this.addOne(); } } },
m(SVG, m.trust(icons.ArrowRight)),
);
const iconLeft = m(
IconButton,
{ events: { onclick: () => { this.subOne(); } } },
m(SVG, m.trust(icons.ArrowLeft)),
);
const checkboxAnnounce = m(Checkbox, {
defaultChecked: false,
label: 'Advertise in Announce?',
value: '100',
onChange: (state) => {
this.show_announce = state.checked;
console.log(this.show_announce);
},
});
const checkboxWebsite = m(Checkbox, {
defaultChecked: false,
label: 'Advertise on Website?',
value: '100',
onChange: (state) => {
this.show_website = state.checked;
},
});
const checkboxInfoScreen = m(Checkbox, {
defaultChecked: false,
label: 'Advertise on Infoscreen?',
value: '100',
onChange: (state) => {
this.show_infoscreen = state.checked;
},
});
const checkboxAllowMail = m(Checkbox, {
defaultChecked: false,
label: 'Allow non AMIV Members?',
value: '100',
onChange: (state) => {
this.allow_email_signup = state.checked;
},
checked: this.allow_email_signup,
});
const addFood = m(Checkbox, {
defaultChecked: false,
label: 'Food limitations',
value: '100',
onChange: (state) => {
this.food = state.checked;
console.log(this.food);
},
checked: this.food,
});
const addSBB = m(Checkbox, {
defaultChecked: false,
label: 'SBB ABO',
value: '100',
onChange: (state) => {
this.sbbAbo = state.checked;
console.log(this.sbbAbo);
},
checked: this.sbbAbo,
});
const radioButtonSelectionMode = m(RadioGroup, {
name: 'Selection Mode',
buttons: [
{
value: 'fcfs',
label: 'First come, first serve',
},
{
value: 'manual',
label: 'Selection made by organizer',
},
],
onChange: (state) => {
this.selection_strategy = state.value;
this.data.selection_strategy = state.value;
console.log(this.data); // Temp proof of concept.
},
value: this.selection_strategy,
});
const buttonFinish = m(Button, {
label: 'Create event',
events: {
onclick: () => {
const additionalFields = {
title: 'Additional Fields',
type: 'object',
properties: {},
required: [],
};
if (this.sbbAbo) {
additionalFields.properties.SBB_Abo = {
type: 'string',
enum: ['None', 'GA', 'Halbtax', 'Gleis 7'],
};
additionalFields.required.push('SBB_Abo');
}
if (this.food) {
additionalFields.properties.Food = {
type: 'string',
enum: ['Omnivor', 'Vegi', 'Vegan', 'Other'],
};
additionalFields.properties.specialFood = {
'Special Food Requirements': {
type: 'string',
},
};
additionalFields.required.push('Food');
}
this.data.additional_fields = additionalFields;
console.log(this.data.additional_fields);
this.submit('POST');
},
},
});
const title = [
'Create an Event', 'When and Where?', 'Signups', 'Advertisement',
][this.currentpage - 1];
// checks currentPage and selects the fitting page
return m('div.mywrapper', [
m('h1', title),
m('br'),
iconLeft,
iconRight,
m('br'),
m('div', {
style: {
display: (this.currentpage === 1) ? 'block' : 'none',
},
}, Object.keys(firstTableInputs).map((key) => {
const attrs = firstTableInputs[key];
const attributes = Object.assign({}, attrs);
attributes.name = key;
attributes.floatingLabel = true;
return m(textInput, this.bind(attributes));
})),
m('div', {
style: {
display: (this.currentpage === 2) ? 'block' : 'none',
},
}, [
m(datetimeInput, this.bind({
name: 'time_start',
label: 'Event Start Time',
})),
m(datetimeInput, this.bind({
name: 'time_end',
label: 'Event End Time',
})),
m(textInput, this.bind({
name: 'location',
label: 'Location',
floatingLabel: true,
})),
]),
m('div', {
style: {
display: (this.currentpage === 3) ? 'block' : 'none',
},
}, [
Object.keys(thirdTableInputs).map((key) => {
const attrs = thirdTableInputs[key];
const attributes = Object.assign({}, attrs);
attributes.name = key;
attributes.floatingLabel = true;
return m(textInput, this.bind(attributes));
}),
addFood, addSBB, m('br'), checkboxAllowMail, radioButtonSelectionMode,
]),
m('div', {
style: {
display: (this.currentpage === 4) ? 'block' : 'none',
},
}, [
Object.keys(forthTableInputs).map((key) => {
const attrs = forthTableInputs[key];
const attributes = Object.assign({}, attrs);
attributes.name = key;
attributes.floatingLabel = true;
return m(textInput, this.bind(attributes));
}),
checkboxWebsite, checkboxAnnounce, checkboxInfoScreen, m('br'), buttonFinish,
]),
m('div', {
style: {
display: (this.currentpage === 6) ? 'block' : 'none',
},
}, ['Event created!',
]),
]);
}
}
import m from 'mithril';
import { ListSelect, DatalistController, Form } from 'amiv-web-ui-components';
import { Toolbar, ToolbarTitle, Dialog, Card, Button } from 'polythene-mithril';
import { ResourceHandler } from '../auth';
import RelationlistController from '../relationlistcontroller';
import TableView from '../views/tableView';
import { dateFormatter } from '../utils';
export class ParticipantsController {
constructor() {
this.signupHandler = new ResourceHandler('eventsignups');
this.signupCtrl = new DatalistController((query, search) => this.signupHandler.get({
search, ...query,
}));
this.userHandler = new ResourceHandler('users');
this.acceptedUserController = new RelationlistController({
primary: 'eventsignups',
secondary: 'users',
query: { where: { accepted: true } },
searchKeys: ['email'],
includeWithoutRelation: true,
});
this.waitingUserController = new RelationlistController({
primary: 'eventsignups',
secondary: 'users',
query: { where: { accepted: false } },
searchKeys: ['email'],
includeWithoutRelation: true,
});
this.allParticipants = [];
}
setEventId(eventId) {
this.signupCtrl.setQuery({ where: { event: eventId } });
this.acceptedUserController.setQuery({ where: { event: eventId, accepted: true } });
this.waitingUserController.setQuery({ where: { event: eventId, accepted: false } });
}
refresh() {
this.signupCtrl.getFullList().then((list) => {
this.allParticipants = list;
});
this.acceptedUserController.refresh();
this.waitingUserController.refresh();
}
}
// Helper class to either display the signed up participants or those on the
// waiting list.
export class ParticipantsTable {
constructor({
attrs: {
participantsCtrl,
waitingList,
additional_fields_schema: additionalFieldsSchema,
},
}) {
this.participantsCtrl = participantsCtrl;
if (waitingList) {
this.ctrl = this.participantsCtrl.waitingUserController;
} else {
this.ctrl = this.participantsCtrl.acceptedUserController;
}
this.add_fields_schema = additionalFieldsSchema
? JSON.parse(additionalFieldsSchema).properties : null;
// true while in the modus of adding a signup
this.addmode = false;
this.userHandler = new ResourceHandler('users');
this.userController = new DatalistController(
(query, search) => this.userHandler.get({ search, ...query }).then(data => ({
...data,
_items: data._items.map(user => ({
hasSignup: this.participantsCtrl.allParticipants.some(
signupUser => user._id === signupUser.user,
),
...user,
})),
})),
);
}
exportAsCSV(filePrefix) {
this.ctrl.getFullList().then((list) => {
const csvData = (list.map((item) => {
const additionalFields = item.additional_fields && JSON.parse(item.additional_fields);
const line = [
item.position,
item._created,
item.user ? item.user.firstname : '',
item.user ? item.user.lastname : '',
item.user ? item.user.membership : 'none',
item.email,
item.accepted,
item.confirmed,
...Object.keys(this.add_fields_schema || {}).map(key => (
additionalFields && additionalFields[key] ? additionalFields[key] : '')),
].join('","');
return `"${line}"`;
})).join('\n');
const headercontent = [
'Position', 'Date', 'Firstname', 'Lastname',
'Membership', 'Email', 'Accepted', 'Confirmed',
...Object.keys(this.add_fields_schema || {}).map(key => this.add_fields_schema[key].title),
].join('","');
const header = `"${headercontent}"`;
const filename = `${filePrefix}_participants_export.csv`;
const fileContent = `data:text/csv;charset=utf-8,${header}\n${csvData}`;
const link = document.createElement('a');
link.setAttribute('href', encodeURI(fileContent));
link.setAttribute('download', filename);
link.click();
});
}
itemRow(data) {
// TODO list should not have hardcoded size outside of stylesheet
const hasPatchRights = data._links.self.methods.indexOf('PATCH') > -1;
const additionalFields = data.additional_fields && JSON.parse(data.additional_fields);
const canBeAccepted = !data.accepted;
return [
m('div', { style: { width: '9em' } }, dateFormatter(data._created)),
m('div', { style: { width: '16em' } }, [
...data.user ? [`${data.user.firstname} ${data.user.lastname}`, m('br')] : '',
data.email,
]),
m(
'div', { style: { width: '14em' } },
m('div', ...data.user ? `Membership: ${data.user.membership}` : ''),
(additionalFields && this.add_fields_schema) ? Object.keys(additionalFields).map(
key => m('div', `${this.add_fields_schema[key].title}: ${additionalFields[key]}`),
) : '',
),
m('div', { style: { 'flex-grow': '100' } }),
canBeAccepted ? m('div', m(Button, {
// Button to accept this eventsignup
className: 'blue-row-button',
style: {
margin: '0px 4px',
},
borders: false,
label: 'accept',
events: {
onclick: () => {
// preapare data for patch request
const patch = (({ _id, _etag }) => ({ _id, _etag }))(data);
patch.accepted = true;
this.ctrl.handler.patch(patch).then(() => {
this.participantsCtrl.refresh();
m.redraw();
});
},
},
})) : '',
hasPatchRights ? m('div', m(Button, {
// Button to remove this eventsignup
className: 'red-row-button',
borders: false,
label: 'remove',
events: {
onclick: () => {
this.ctrl.handler.delete(data).then(() => {
this.participantsCtrl.refresh();
m.redraw();
});
},
},
})) : '',
];
}
editEventSignup(user, event) {
const form = new Form();
const schema = JSON.parse(event.additional_fields);
if (schema && schema.$schema) {
// ajv fails to verify the v4 schema of some resources
schema.$schema = 'http://json-schema.org/draft-06/schema#';
form.setSchema(schema);
}
const elements = form.renderSchema();
Dialog.show({
body: m('form', { onsubmit: () => false }, elements),
backdrop: true,
footerButtons: [
m(Button, {
label: 'Cancel',
events: { onclick: () => Dialog.hide() },
}),
m(Button, {
label: 'Submit',
events: {
onclick: () => {
const additionalFieldsString = JSON.stringify(form.getData());
const data = {
event: event._id,
additional_fields: additionalFieldsString,
};
data.user = user._id;
this.ctrl.handler.post(data).then(() => {
Dialog.hide();
this.participantsCtrl.refresh();
m.redraw();
});
},
},
})],
});
}
view({ attrs: { title, filePrefix, event, waitingList } }) {
return m(Card, {
style: { height: '400px', 'margin-bottom': '10px' },
content: m('div', [
this.addmode ? m(ListSelect, {
controller: this.userController,
listTileAttrs: user => Object.assign({}, {
title: `${user.firstname} ${user.lastname}`,
style: (user.hasSignup ? { color: 'rgba(0, 0, 0, 0.2)' } : {}),
hoverable: !user.hasSignup,
}),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSubmit: (user) => {
this.addmode = false;
if (event.additional_fields) {
this.editEventSignup(user, event);
} else {
this.ctrl.handler.post({
user: user._id,
event: event._id,
accepted: !waitingList,
}).then(() => {
this.participantsCtrl.refresh();
m.redraw();
});
}
},
onCancel: () => { this.addmode = false; m.redraw(); },
}) : '',
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: title }),
(!waitingList
|| event.selection_strategy === 'manual'
|| event.signup_count >= event.spots) && m(Button, {
style: { margin: '0px 4px' },
className: 'blue-button',
borders: true,
label: 'add',
events: { onclick: () => { this.addmode = true; } },
}),
m(Button, {
className: 'blue-button',
borders: true,
label: 'export CSV',
events: { onclick: () => this.exportAsCSV(filePrefix) },
}),
]),
m(TableView, {
tableHeight: '275px',
controller: this.ctrl,
tileContent: data => this.itemRow(data),
clickOnRows: false,
titles: [
{ text: 'Date of Signup', width: '9em' },
{ text: 'Participant', width: '16em' },
{ text: 'Additional Info', width: '16em' },
],
}),
]),
});
}
}
import m from 'mithril';
import { events as config } from '../config.json';
import { Snackbar } from 'polythene-mithril';
import { DatalistController } from 'amiv-web-ui-components';
import axios from 'axios';
import { hookUrl } from 'networkConfig';
import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
import { get } from '../localStorage';
/* Table of all Events
......@@ -10,10 +14,23 @@ import { dateFormatter } from '../utils';
* Makes use of the standard TableView
*/
const triggerHook = () => {
axios.post(hookUrl, { token: get('token') }).then(() => {
Snackbar.show({ title: 'Successful', style: { color: 'green' } });
}).catch((e) => {
// eslint-disable-next-line no-console
console.log(e);
Snackbar.show({
title: 'Network Error, please contact administrator',
style: { color: 'red' },
});
});
};
export default class EventTable {
constructor() {
this.ctrl = new DatalistController('events', {}, config.tableKeys);
this.handler = new ResourceHandler('events');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
......@@ -25,16 +42,36 @@ export default class EventTable {
}
view() {
const now = new Date();
return m(TableView, {
controller: this.ctrl,
keys: config.tableKeys,
keys: ['titel_en', 'time_start', 'time_end'],
tileContent: this.getItemData,
titles: [
{ text: 'Titel', width: 'calc(100% - 18em)' },
{ text: 'Start', width: '9em' },
{ text: 'End', width: '9em' },
],
onAdd: () => { m.route.set('/newevent'); },
filters: [[{
name: 'upcoming',
query: { time_start: { $gte: `${now.toISOString().slice(0, -5)}Z` } },
}, {
name: 'past',
query: { time_start: { $lt: `${now.toISOString().slice(0, -5)}Z` } },
}]],
buttons: this.handler.rights.includes('POST') ? [
{ text: 'Rerender website', onclick: triggerHook },
] : [],
// per default, enable the 'upcoming' filter
initFilterIdxs: [[0, 0]],
onAdd: (this.handler.rights.length > 0)
? () => {
if (this.handler.rights.includes('POST')) {
m.route.set('/newevent');
} else {
m.route.set('/proposeevent');
}
} : false,
});
}
}
import m from 'mithril';
import {
Switch,
Button,
Card,
TextField,
Icon,
} from 'polythene-mithril';
import { Button } from 'polythene-mithril';
import Stream from 'mithril/stream';
import { styler } from 'polythene-core-css';
import { DropdownCard, Chip } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import ItemView from '../views/itemView';
import { apiUrl, eventsignups as signupConfig } from '../config.json';
import TableView from '../views/tableView';
import DatalistController from '../listcontroller';
import { ParticipantsController, ParticipantsTable } from './participants';
import { dateFormatter } from '../utils';
import { icons, DropdownCard, Property } from '../views/elements';
import { ResourceHandler } from '../auth';
import { Property, FilterChip, icons } from '../views/elements';
import { colors } from '../style';
const viewLayout = [
{
'.eventViewContainer': {
display: 'grid',
'grid-template-columns': '40% 55%',
'grid-gap': '50px',
},
'.propertyLangIndicator': {
width: '30px',
height: '20px',
......@@ -50,197 +41,239 @@ styler.add('eventView', viewLayout);
// small helper class to display both German and English content together, dependent
// on which content is available.
class PropertyInfo {
class DuoLangProperty {
view({ attrs: { title, de, en } }) {
if (de && en) {
return m(
'div',
m('p.propertyTitle', { style: { 'margin-top': '10px', 'margin-bottom': '3px' } }, [title]),
m('div', [
m('div', { className: 'propertyLangIndicator' }, 'DE'),
m('p.propertyText', de),
]),
m('div', [
m('div', { className: 'propertyLangIndicator' }, 'EN'),
m('p.propertyText', en),
]),
);
} else if (de) {
return m(
'div',
m('p.propertyTitle', { style: { 'margin-top': '10px', 'margin-bottom': '3px' } }, [title]),
m('div', [
m('div', { className: 'propertyLangIndicator' }, 'DE'),
m('p.propertyText', de),
]),
);
} else if (en) {
return m(
'div',
m('p.propertyTitle', { style: { 'margin-top': '10px', 'margin-bottom': '3px' } }, [title]),
m('div', [
m('div', { className: 'propertyLangIndicator' }, 'EN'),
m('p.propertyText', en),
]),
);
}
// TODO Lang indicators should be smaller and there should be less margin
// between languages
return m(
Property,
{ title },
de ? m('div', [
m('div', { className: 'propertyLangIndicator' }, 'DE'),
m('p', de),
]) : '',
en ? m('div', [
m('div', { className: 'propertyLangIndicator' }, 'EN'),
m('p', en),
]) : '',
);
}
}
// Helper class to either display the signed up participants or those on the
// waiting list.
class ParticipantsTable {
constructor({ attrs: { where } }) {
this.ctrl = new DatalistController('eventsignups', {
embedded: { user: 1 },
where,
}, ['email', 'user.firstname', 'user.lastname'], false);
class ParticipantsSummary {
constructor() {
this.onlyAccepted = true;
}
getItemData(data) {
// TODO list should not have hardcoded size outside of stylesheet
return [
m('div', { style: { width: '9em' } }, dateFormatter(data._created)),
m('div', { style: { width: '9em' } }, data.user.lastname),
m('div', { style: { width: '9em' } }, data.user.firstname),
m('div', { style: { width: '9em' } }, data.email),
];
}
view({ attrs: { participants, additionalFields = "{'properties': {}}" } }) {
// Parse the JSON from additional fields into an object
const parsedParticipants = participants.map(signup => ({
...signup,
additional_fields: signup.additional_fields
? JSON.parse(signup.additional_fields) : {},
}));
// Filter if only accepted participants should be shown
const filteredParticipants = parsedParticipants.filter(
participant => (this.onlyAccepted ? participant.accepted : true),
);
view() {
return m(Card, {
style: { height: '300px' },
content: m(TableView, {
controller: this.ctrl,
keys: signupConfig.tableKeys,
tileContent: this.getItemData,
titles: [
{ text: 'Date of Signup', width: '9em' },
{ text: 'Name', width: '9em' },
{ text: 'First Name', width: '9em' },
{ text: 'Email', width: '9em' },
],
}),
});
}
}
// check which additional fields should get summarized
let hasSBB = false;
let hasFood = false;
if (additionalFields) {
hasSBB = 'sbb_abo' in JSON.parse(additionalFields).properties;
hasFood = 'food' in JSON.parse(additionalFields).properties;
}
class EmailList {
view({ attrs: { list } }) {
const emails = list.toString().replace(/,/g, '; ');
return m(Card, {
content: m(TextField, { value: emails, label: '', multiLine: true }, ''),
});
return m('div', [
m('div', {
style: {
height: '50px',
'overflow-x': 'auto',
'overflow-y': 'hidden',
'white-space': 'nowrap',
padding: '0px 5px',
},
}, [].concat(['Filters: '], ...[
m(FilterChip, {
selected: this.onlyAccepted,
onclick: () => { this.onlyAccepted = !this.onlyAccepted; },
}, 'accepted users'),
])),
hasSBB ? m('div', { style: { display: 'flex' } }, [
m(Property, { title: 'No SBB', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'None',
).length),
m(Property, { title: 'GA', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'GA',
).length),
m(Property, { title: 'Halbtax', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'Halbtax',
).length),
m(Property, { title: 'Gleis 7', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.sbb_abo === 'Gleis 7',
).length),
]) : '',
hasFood ? m('div', { style: { display: 'flex' } }, [
m(Property, { title: 'Omnivors', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.food === 'Omnivor',
).length),
m(Property, { title: 'Vegis', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.food === 'Vegi',
).length),
m(Property, { title: 'Vegans', leftAlign: false }, filteredParticipants.filter(
signup => signup.additional_fields.food === 'Vegan',
).length),
]) : '',
m('textarea', {
style: { opacity: '0', width: '0px' },
id: 'participantsemails',
}, filteredParticipants.map(signup => signup.email).toString().replace(/,/g, '; ')),
m(Button, {
label: 'Copy Emails',
events: {
onclick: () => {
document.getElementById('participantsemails').select();
document.execCommand('copy');
},
},
}),
]);
}
}
export default class viewEvent extends ItemView {
constructor() {
super('events');
this.signupHandler = new ResourceHandler('eventsignups');
constructor(vnode) {
super(vnode);
this.participantsCtrl = new ParticipantsController();
this.description = false;
this.advertisement = false;
this.registration = false;
this.emailAdresses = false;
this.emaillist = [''];
this.showAllEmails = false;
this.modalDisplay = Stream('none');
}
oninit() {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
this.setUpEmailList(this.showAllEmails);
this.participantsCtrl.setEventId(this.data._id);
}
setUpEmailList(showAll) {
// setup where query
const where = { event: this.id };
if (!showAll) {
// only show accepted
where.accepted = true;
cloneEvent() {
const event = Object.assign({}, this.data);
const eventInfoToDelete = [
'_id',
'_created',
'_etag',
'_links',
'_updated',
'signup_count',
'unaccepted_count',
'__proto__',
];
const now = new Date();
if (event.time_end < `${now.toISOString().slice(0, -5)}Z`) {
eventInfoToDelete.push(...[
'time_advertising_end',
'time_advertising_start',
'time_end',
'time_register_end',
'time_deregister_end',
'time_register_start',
'time_start']);
}
this.signupHandler.get({ where }).then((data) => {
this.emaillist = (data._items.map(item => item.email));
m.redraw();
eventInfoToDelete.forEach((key) => {
delete event[key];
});
}
view({ attrs: { onEdit } }) {
if (!this.data) return '';
this.controller.changeModus('new');
this.controller.data = event;
}
view() {
let displaySpots = '-';
const stdMargin = { margin: '5px' };
// Get the image and insert it inside the modal -
// use its "alt" text as a caption
const modalImg = document.getElementById('modalImg');
if (this.data.spots !== 0) {
displaySpots = this.data.spots;
}
return m('div', {
style: { height: '100%', 'overflow-y': 'scroll', padding: '10px' },
}, [
m(Button, {
element: 'div',
label: 'Update Event',
events: { onclick: onEdit },
}),
m('div', [
return this.layout([
// this div is the title line
m('div.maincontainer', [
// event image if existing
this.data.img_thumbnail ? m('img', {
src: `${apiUrl.slice(0, -1)}${this.data.img_thumbnail.file}`,
src: `${apiUrl}${this.data.img_thumbnail.file}`,
height: '50px',
style: { float: 'left' },
style: { float: 'left', margin: '0 5px' },
}) : '',
m('h1', { style: { 'margin-top': '0px', 'margin-bottom': '0px' } }, [this.data.title_de || this.data.title_en]),
m('h1', this.data.title_de || this.data.title_en),
]),
this.data.signup_count ? m(Property, {
style: { float: 'left', 'margin-right': '20px' },
title: 'Signups',
}, `${this.data.signup_count} / ${displaySpots}`) : m.trust('&nbsp;'),
this.data.location ? m(Property, {
style: { float: 'left', 'margin-right': '20px' },
title: 'Location',
}, `${this.data.location}`) : m.trust('&nbsp;'),
this.data.time_start ? m(Property, {
title: 'Time',
}, `${dateFormatter(this.data.time_start)} - ${dateFormatter(this.data.time_end)}`) : m.trust('&nbsp;'),
m('div.eventViewContainer', { style: { 'margin-top': '50px' } }, [
m('div.eventViewLeft', [
// below the title, most important details are listed
m('div.maincontainer', { style: { display: 'flex' } }, [
this.data.type && m(Property, {
style: stdMargin,
title: 'Type',
}, `${this.data.type.charAt(0).toUpperCase() + this.data.type.slice(1)}`),
(this.data.spots !== null && 'signup_count' in this.data
&& this.data.signup_count !== null)
? m(Property, {
style: stdMargin,
title: 'Signups',
}, `${this.data.signup_count} / ${displaySpots}`) : '',
this.data.location && m(Property, {
style: stdMargin,
title: 'Location',
}, `${this.data.location}`),
this.data.time_start && m(Property, {
title: 'Time',
style: stdMargin,
}, `${dateFormatter(this.data.time_start)} - ${dateFormatter(this.data.time_end)}`),
this.data.moderator && m(Property, {
title: 'Moderator',
style: stdMargin,
}, m.trust(`${this.data.moderator.firstname} ${this.data.moderator.lastname}
(<a href='mailto:${this.data.moderator.email}'>${this.data.moderator.email}</a>)`)),
]),
// everything else is not listed in DropdownCards, which open only on request
m('div.viewcontainer', [
m('div.viewcontainercolumn', [
m(DropdownCard, { title: 'description' }, [
m(PropertyInfo, {
m(DuoLangProperty, {
title: 'Catchphrase',
de: this.data.catchphrase_de,
en: this.data.catchphrase_en,
}),
m(PropertyInfo, {
m(DuoLangProperty, {
title: 'Description',
de: this.data.description_de,
en: this.data.description_en,
}),
]),
m(DropdownCard, { title: 'advertisement' }, [
m(DropdownCard, { title: 'advertisement', style: { margin: '10px 0' } }, [
[
m(Icon, {
style: { float: 'left' },
svg: m.trust(this.data.show_annonce ? icons.checked : icons.clear),
}),
m('span', { style: { float: 'left' } }, 'annonce'),
m(Icon, {
style: { float: 'left' },
svg: m.trust(this.data.show_infoscreen ? icons.checked : icons.clear),
}),
m('span', { style: { float: 'left' } }, 'infoscreen'),
m(Icon, {
style: { float: 'left' },
svg: m.trust(this.data.show_website ? icons.checked : icons.clear),
}),
m('span', { style: { float: 'left' } }, 'website'),
m(Chip, {
svg: this.data.show_announce ? icons.checked : icons.clear,
border: '1px #aaaaaa solid',
}, 'announce'),
m(Chip, {
svg: this.data.show_infoscreen ? icons.checked : icons.clear,
border: '1px #aaaaaa solid',
margin: '4px',
}, 'infoscreen'),
m(Chip, {
svg: this.data.show_website ? icons.checked : icons.clear,
border: '1px #aaaaaa solid',
}, 'website'),
],
this.data.time_advertising_start ? m(
Property,
'Advertising Time',
`${dateFormatter(this.data.time_advertising_start)} - ${dateFormatter(this.data.time_advertising_end)}`,
{ title: 'Advertising Time' },
`${dateFormatter(this.data.time_advertising_start)} - `
+ `${dateFormatter(this.data.time_advertising_end)}`,
) : '',
this.data.priority ? m(
Property,
......@@ -249,43 +282,163 @@ export default class viewEvent extends ItemView {
) : '',
]),
m(DropdownCard, { title: 'Registration' }, [
this.data.price ? m(Property, 'Price', `${this.data.price}`) : '',
m(DropdownCard, { title: 'Registration', style: { margin: '10px 0' } }, [
this.data.price ? m(Property, { title: 'Price' }, `${this.data.price}`) : '',
this.data.time_register_start ? m(
Property,
{ title: 'Registration Time' },
`${dateFormatter(this.data.time_register_start)} - ${dateFormatter(this.data.time_register_end)}`,
`${dateFormatter(this.data.time_register_start)} - `
+ `${dateFormatter(this.data.time_register_end)}`,
) : '',
this.data.time_deregister_end ? m(
Property,
{ title: 'Deregistration Time' },
`${dateFormatter(this.data.time_deregister_end)}`,
) : '',
this.data.selection_strategy ? m(
Property,
{ title: 'Selection Mode' },
m.trust(this.data.selection_strategy),
) : '',
this.data.allow_email_signup ? m(Property, 'non AMIV-Members allowed') : '',
this.data.allow_email_signup && m(Property, 'non AMIV-Members allowed'),
this.data.additional_fields && m(
Property,
{ title: 'Registration Form' },
this.data.additional_fields,
),
this.data.external_registration && m(
Property,
{ title: 'External Registration' },
m('a', { href: this.data.external_registration, target: '_blank' },
this.data.external_registration),
),
]),
m(DropdownCard, { title: 'Email Adresses' }, [
m(Switch, {
defaultChecked: false,
label: 'show unaccepted',
onChange: () => {
this.showAllEmails = !this.showAllEmails;
this.setUpEmailList(this.showAllEmails);
// a list of email adresses of all participants, easy to copy-paste
this.data.spots !== null ? m(DropdownCard, {
title: 'Participants Summary',
style: { margin: '10px 0' },
}, m(ParticipantsSummary, {
participants: this.participantsCtrl.allParticipants,
additionalFields: this.data.additional_fields,
})) : '',
m(DropdownCard, { title: 'Images' }, [
m('div', {
style: {
display: 'flex',
},
}),
m(EmailList, { list: this.emaillist }),
}, [
m('div', {
style: {
width: '40%',
padding: '5px',
},
}, [
this.data.img_poster && m('div', 'Poster'),
this.data.img_poster && m('img', {
src: `${apiUrl}${this.data.img_poster.file}`,
width: '100%',
onclick: () => {
this.modalDisplay('block');
modalImg.src = `${apiUrl}${this.data.img_poster.file}`;
},
}),
]),
m('div', {
style: {
width: '52%',
padding: '5px',
},
}, [
m('div', [
this.data.img_infoscreen && m('div', 'Infoscreen'),
this.data.img_infoscreen && m('img', {
src: `${apiUrl}${this.data.img_infoscreen.file}`,
width: '100%',
onclick: () => {
this.modalDisplay('block');
modalImg.src = `${apiUrl}${this.data.img_infoscreen.file}`;
},
}),
]),
]),
]),
]),
]),
m('div.eventViewRight', [
m('h4', 'Accepted Participants'),
m(ParticipantsTable, { where: { accepted: true, event: this.data._id } }),
m('p', ''),
m('h4', 'Participants on Waiting List'),
m(ParticipantsTable, { where: { accepted: false, event: this.data._id } }),
m('div.viewcontainercolumn', { style: { width: '50em' } }, [
this.data.time_register_start ? m(ParticipantsTable, {
title: 'Accepted Participants',
filePrefix: 'accepted',
event: this.data,
waitingList: false,
additional_fields_schema: this.data.additional_fields,
participantsCtrl: this.participantsCtrl,
}) : '',
this.data.time_register_start ? m(ParticipantsTable, {
title: 'Participants on Waiting List',
filePrefix: 'waitinglist',
event: this.data,
waitingList: true,
additional_fields_schema: this.data.additional_fields,
participantsCtrl: this.participantsCtrl,
}) : '',
]),
]),
m('div', {
id: 'imgModal',
style: {
display: this.modalDisplay(),
position: 'fixed',
'z-index': '100',
'padding-top': '100px',
left: 0,
top: 0,
width: '100vw',
height: '100vh',
overflow: 'auto',
'background-color': 'rgba(0, 0, 0, 0.9)',
},
}, [
m('img', {
id: 'modalImg',
style: {
margin: 'auto',
display: 'block',
'max-width': '80vw',
'max-heigth': '80vh',
},
}),
m('div', {
onclick: () => {
this.modalDisplay('none');
},
style: {
top: '15px',
right: '35px',
color: '#f1f1f1',
transition: '0.3s',
'z-index': 10,
position: 'absolute',
'font-size': '40px',
'font-weight': 'bold',
},
}, 'x'),
]),
], [
m(Button, {
label: 'Clone Event',
border: true,
style: {
color: colors.light_blue,
'border-color': colors.light_blue,
},
events: {
// opens 'new event' ,
// coping All information but the 'event_id', past dates and API generated properties
onclick: () => this.cloneEvent(),
},
}),
]);
}
}
import m from 'mithril';
import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController, Select } from 'amiv-web-ui-components';
// eslint-disable-next-line import/extensions
import { apiUrl } from 'networkConfig';
import { ResourceHandler } from '../auth';
import EditView from '../views/editView';
/**
* Table of all possible permissions to edit
*
* @class PermissionEditor (name)
*/
class PermissionEditor {
oninit() {
// load all possible API endpoints, as permissions are defined at endpoint/resource level
m.request(apiUrl).then((response) => {
this.apiEndpoints = response._links.child;
});
}
/**
*
* @attr {object} permissions the permissions as defined so far for the group
* @attr {function} onChange is called with the changed permissions any timne the
* permissions are changed in this editor.
*/
view({ attrs: { permissions, onChange } }) {
// make a local copy of permissions to edit
const internalPerm = Object.assign({}, permissions);
if (!this.apiEndpoints) return '';
return m('div', [
m('span', {
style: {
color: 'rgba(0, 0, 0, 0.54)',
'font-size': '10pt',
},
}, 'Permissions granted by membership in this group'),
m('div', {
style: {
padding: '10px',
border: '1px solid rgba(0, 0, 0, 0.54)',
'border-radius': '10px',
},
}, m('div', {
style: { display: 'flex', width: '100%', 'flex-flow': 'row wrap' },
}, this.apiEndpoints.map(apiEndpoint => m('div', {
style: { display: 'flex', width: '220px', 'padding-right': '20px' },
}, [
m(Select, {
label: apiEndpoint.title,
options: ['no permission', 'read', 'readwrite'],
default: 'no permission',
style: { width: '200px' },
onChange({ value }) {
if (value === 'no permission') {
// the api equivalent to no permission is to delete the key out of the dict
if (internalPerm[apiEndpoint.href]) delete internalPerm[apiEndpoint.href];
} else internalPerm[apiEndpoint.href] = value;
onChange(internalPerm);
},
value: internalPerm[apiEndpoint.href],
}),
])))),
]);
}
}
export default class NewGroup extends EditView {
constructor(vnode) {
super(vnode);
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
}
beforeSubmit() {
const data = Object.assign({}, this.form.data);
// exchange moderator object with string of id, will return null if moderator is null
data.moderator = data.moderator ? data.moderator._id : null;
this.submit(data).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
...this.form.renderSchema(['name', 'allow_self_enrollment', 'requires_storage']),
m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'Group Moderator: ', disabled: true, style: { width: '160px' } }),
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController,
selection: this.form.data.moderator,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.form.data.moderator = data; },
})),
]),
m(PermissionEditor, {
permissions: this.form.data.permissions,
onChange: (newPermissions) => { this.form.data.permissions = newPermissions; },
}),
]);
}
}
import m from 'mithril';
import viewGroup from './viewGroup';
import editGroup from './editGroup';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
export default class GroupItem {
constructor() {
this.controller = new ItemController('groups', { moderator: 1 });
}
view() {
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(editGroup, { controller: this.controller });
return m(viewGroup, { controller: this.controller });
}
}
import m from 'mithril';
import { Card, Button, ListTile } from 'polythene-mithril';
import { DatalistController } from 'amiv-web-ui-components';
import { loadingScreen } from '../layout';
import { ResourceHandler, getCurrentUser } from '../auth';
class GroupListItem {
view({ attrs: { name, _id } }) {
return m(ListTile, {
title: name,
hoverable: true,
rounded: true,
style: { width: '250px' },
url: {
href: `/groups/${_id}`,
oncreate: m.route.link,
},
});
}
}
class GroupListCard {
view({ attrs: { title, groups, onAdd = false } }) {
return m('div.maincontainer', { style: { 'margin-top': '5px' } }, m(Card, {
content: m('div', [
m('div', { style: { display: 'flex', 'align-items': 'center' } }, [
m('div.pe-card__title', title),
onAdd && m(Button, {
style: { 'margin-right': '20px' },
className: 'blue-button',
extraWide: true,
label: 'add',
events: { onclick: () => onAdd() },
}),
]),
m('div', {
style: { display: 'flex', 'flex-wrap': 'wrap', margin: '0px 5px 5px 5px' },
}, groups.map(item => m(GroupListItem, { name: item.name, _id: item._id }))),
]),
}));
}
}
export default class GroupList {
constructor() {
this.handler = new ResourceHandler('groups', ['name']);
this.ctrl = new DatalistController(
(query, search) => this.handler.get({ search, ...query }),
{ sort: [['name', 1]] },
);
this.groups = [];
this.moderatedGroups = [];
this.ctrl.getFullList().then((list) => {
this.groups = list;
this.ctrl.setQuery({ where: { moderator: getCurrentUser() } });
this.ctrl.getFullList().then((moderatedList) => {
this.moderatedGroups = moderatedList;
m.redraw();
});
});
}
view() {
if (!this.groups) return m(loadingScreen);
return m('div', [
// groups moderated by the current user
this.moderatedGroups.length > 0
&& m(GroupListCard, { title: 'moderated by you', groups: this.moderatedGroups }),
// all groups
m(GroupListCard, {
title: 'all groups',
groups: this.groups,
onAdd: () => { m.route.set('/newgroup'); },
}),
]);
}
}
import m from 'mithril';
import {
Button,
Card,
Toolbar,
ToolbarTitle,
TextField,
Icon,
} from 'polythene-mithril';
import { DatalistController, ListSelect, DropdownCard, Chip } from 'amiv-web-ui-components';
import { icons, Property } from '../views/elements';
import { colors } from '../style';
import ItemView from '../views/itemView';
import TableView from '../views/tableView';
import RelationlistController from '../relationlistcontroller';
import { ResourceHandler } from '../auth';
// Helper class to either display the signed up participants or those on the
// waiting list.
class MembersTable {
constructor({ attrs: { group, hasPatchRights } }) {
this.group_id = group;
this.hasPatchRights = hasPatchRights;
this.ctrl = new RelationlistController({
primary: 'groupmemberships', secondary: 'users', query: { where: { group } },
});
// true while in the modus of adding a member
this.addmode = false;
this.userHandler = new ResourceHandler('users');
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
}
itemRow(data) {
// TODO list should not have hardcoded size outside of stylesheet
return [
m('div', { style: { width: '18em' } }, `${data.user.firstname} ${data.user.lastname}`),
m('div', { style: { width: '9em' } }, data.user.email),
m('div', { style: { 'flex-grow': '100' } }),
this.hasPatchRights && m('div', m(Button, {
// Button to remove this groupmembership
className: 'red-row-button',
borders: false,
label: 'remove',
events: {
onclick: () => {
this.ctrl.handler.delete(data).then(() => {
this.ctrl.refresh();
m.redraw();
});
},
},
})),
];
}
view() {
return m(Card, {
style: { height: '500px' },
content: m('div', [
this.addmode ? m(ListSelect, {
controller: this.userController,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSubmit: (user) => {
this.addmode = false;
this.ctrl.handler.post({
user: user._id,
group: this.group_id,
}).then(() => {
this.ctrl.refresh();
m.redraw();
});
},
onCancel: () => { this.addmode = false; m.redraw(); },
}) : '',
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: 'Members' }),
this.hasPatchRights && m(Button, {
className: 'blue-button',
borders: true,
label: 'add',
events: { onclick: () => { this.addmode = true; } },
}),
]),
m(TableView, {
tableHeight: '375px',
controller: this.ctrl,
keys: ['user.lastname', 'user.firstname', 'user.email'],
tileContent: data => this.itemRow(data),
clickOnRows: false,
titles: [
{ text: 'Name', width: '18em' },
{ text: 'Email', width: '9em' },
],
}),
]),
});
}
}
// Table for list of email adresses, both forward_to and receive
class EmailTable {
constructor({ attrs: { onRemove = false } }) {
this.addmode = false;
this.dirty = false;
this.newvalue = '';
this.onRemove = onRemove;
}
item(data) {
return m('div', {
style: {
margin: '10px',
padding: '5px',
height: '30px',
'background-color': '#dddddd',
},
}, [
data,
this.onRemove && m(Icon, {
style: { 'margin-left': '3px' },
svg: { content: m.trust(icons.clear) },
size: 'small',
events: {
onclick: () => { this.onRemove(data); },
},
}),
]);
}
view({ attrs: { list, title, style = {}, onSubmit = false } }) {
return m(Card, {
style: { height: '200px', ...style },
content: m('div', [
this.addmode ? m(Toolbar, {
compact: true,
style: { background: 'rgb(78, 242, 167)' },
}, [
m(TextField, {
label: 'enter email address',
type: 'email',
onChange: ({ value }) => {
this.dirty = value !== '';
this.newvalue = value;
},
}),
m(Button, {
label: this.dirty ? 'Submit' : 'Cancel',
className: 'blue-button',
events: {
onclick: () => {
if (this.dirty) {
onSubmit(this.newvalue);
this.addmode = false;
this.newvalue = '';
} else {
this.addmode = false;
}
},
},
value: this.newvalue,
}),
]) : '',
m(Toolbar, { compact: true }, [
m(ToolbarTitle, { text: title }),
onSubmit && m(Button, {
className: 'blue-button',
borders: true,
label: 'add',
events: { onclick: () => { this.addmode = true; } },
}),
]),
m('div', {
style: { padding: '10px', display: 'flex', 'flex-wrap': 'wrap' },
}, list.map(item => this.item(item))),
]),
});
}
}
export default class viewGroup extends ItemView {
oninit() {
// load the number of members in this group
const handler = new ResourceHandler('groupmemberships');
handler.get({ where: { group: this.data._id } }).then((memberships) => {
this.numMembers = memberships._meta.total;
m.redraw();
});
}
view() {
// update the reference to the controller data, as this may be refreshed in between
this.data = this.controller.data;
const hasPatchRights = this.data._links.self.methods.indexOf('PATCH') > -1;
const stdMargin = { margin: '5px' };
return this.layout([
// this div is the title line
m('div.maincontainer', [
m('h1', this.data.name),
this.data.requires_storage && m(Chip, {
svg: icons.cloud,
svgColor: '#ffffff',
svgBackground: colors.orange,
...stdMargin,
}, 'has a folder on the AMIV Cloud'),
m('div', { style: { display: 'flex' } }, [
('numMembers' in this)
&& m(Property, { title: 'Members', style: stdMargin }, this.numMembers),
this.data.moderator && m(Property, {
title: 'Moderator',
onclick: () => { m.route.set(`/users/${this.data.moderator._id}`); },
style: stdMargin,
}, `${this.data.moderator.firstname} ${this.data.moderator.lastname}`),
]),
]),
m('div.viewcontainer', [
// now-column layout: This first column are the members
m('div.viewcontainercolumn', [
this.data.permissions ? m(
DropdownCard,
{ title: 'Permissions', style: { 'margin-bottom': '20px' } },
Object.keys(this.data.permissions)
.map(key => m(Property, { title: key }, this.data.permissions[key])),
) : '',
m(MembersTable, { group: this.data._id, hasPatchRights }),
]),
// the second column contains receive_from and forward_to emails
m('div.viewcontainercolumn', [
m(EmailTable, {
list: this.data.receive_from || [],
title: 'Receiving Email Adresses',
onSubmit: hasPatchRights ? (newItem) => {
const oldList = this.data.receive_from || [];
this.controller.patch({
_id: this.data._id,
_etag: this.data._etag,
receive_from: [...oldList, newItem],
});
} : undefined,
onRemove: hasPatchRights ? (item) => {
const oldList = this.data.receive_from;
// remove the first occurence of the given item-string
const index = oldList.indexOf(item);
if (index !== -1) {
oldList.splice(index, 1);
this.controller.patch({
_id: this.data._id,
_etag: this.data._etag,
receive_from: oldList,
});
}
} : undefined,
}),
m(EmailTable, {
list: this.data.forward_to || [],
title: 'Forwards to Email Adresses',
style: { 'margin-top': '10px' },
onSubmit: hasPatchRights ? (newItem) => {
const oldList = this.data.forward_to || [];
this.controller.patch({
_id: this.data._id,
_etag: this.data._etag,
forward_to: [...oldList, newItem],
});
} : undefined,
onRemove: hasPatchRights ? (item) => {
const oldList = this.data.forward_to;
// remove the first occurence of the given item-string
const index = oldList.indexOf(item);
if (index !== -1) {
oldList.splice(index, 1);
this.controller.patch({
_id: this.data._id,
_etag: this.data._etag,
forward_to: oldList,
});
}
} : undefined,
}),
]),
]),
]);
}
}
import m from 'mithril';
import LoginScreen from './login';
import TableView from './views/tableView';
import { UserModal, UserTable, NewUser } from './userTool';
import { MembershipView } from './membershipTool';
import { OauthRedirect } from './auth';
import GroupList from './groups/list';
import GroupItem from './groups/item';
import BlacklistTable from './blacklist/viewBlacklist';
import NewBlacklist from './blacklist/editBlacklist';
import { UserItem, UserTable } from './users/userTool';
import MembershipView from './membershipTool';
import EventTable from './events/table';
import newEvent from './events/newEvent';
import EventModal from './events/eventModal';
import eventDraft from './events/eventDraft';
import eventWithExport from './events/eventWithExport';
import Layout from './layout';
import EventItem from './events/item';
import JobTable from './jobs/table';
import JobItem from './jobs/item';
import StudydocTable from './studydocs/list';
import studydocItem from './studydocs/item';
import InfoscreenTable from './infoscreen/table';
import { Layout, Error404 } from './layout';
import './style';
const root = document.body;
function layoutWith(view) {
return {
view() {
......@@ -22,24 +26,28 @@ function layoutWith(view) {
};
}
m.route(root, '/users', {
m.route.prefix('');
m.route(root, '/events', {
'/users': layoutWith(UserTable),
'/users/:id': layoutWith(UserModal),
'/newuser': layoutWith(NewUser),
'/users/:id': layoutWith(UserItem),
'/newuser': layoutWith(UserItem),
'/groupmemberships/:id': layoutWith(MembershipView),
'/events': layoutWith(EventTable),
'/events/:id': layoutWith(EventModal),
'/newevent': layoutWith(newEvent),
'/draftevent': layoutWith(eventDraft),
'/eventwithexport': layoutWith(eventWithExport),
'/groups': layoutWith({
view() {
return m(TableView, {
resource: 'groups',
keys: ['name'],
});
},
}),
'/login': LoginScreen,
// '/announce': layoutWith(AnnounceTool),
'/events/:id': layoutWith(EventItem),
'/newevent': layoutWith(EventItem),
'/proposeevent': layoutWith(EventItem),
'/infoscreen': layoutWith(InfoscreenTable),
'/groups': layoutWith(GroupList),
'/groups/:id': layoutWith(GroupItem),
'/newgroup': layoutWith(GroupItem),
'/blacklist': layoutWith(BlacklistTable),
'/newblacklistentry': layoutWith(NewBlacklist),
'/oauthcallback': OauthRedirect,
'/joboffers': layoutWith(JobTable),
'/newjoboffer': layoutWith(JobItem),
'/joboffers/:id': layoutWith(JobItem),
'/studydocuments': layoutWith(StudydocTable),
'/studydocuments/:id': layoutWith(studydocItem),
'/newstudydocument': layoutWith(studydocItem),
'/404': layoutWith(Error404),
});
import m from 'mithril';
import { Snackbar } from 'polythene-mithril';
import { apiUrl } from 'networkConfig';
import { DatalistController } from 'amiv-web-ui-components';
import { ResourceHandler } from '../auth';
import { dateFormatter } from '../utils';
import TableView from '../views/tableView';
const getImgUrl = img => `${apiUrl}${img.file}`;
const exportCSV = (ctrl) => {
ctrl.getFullList().then(
(list) => {
let csv = '';
csv += [
'id',
'title_en',
'time_advertising_start',
'time_advertising_end',
'img_infoscreen_url',
].join(';');
csv += '\n';
list.forEach((event) => {
const fields = [
event._id,
event.title_en.replace(';', ''),
event.time_advertising_start,
event.time_advertising_end,
event.img_infoscreen ? getImgUrl(event.img_infoscreen) : '',
];
csv += fields.join(';');
csv += '\n';
});
const blob = new Blob([csv], { type: 'text/csv' });
const dl = window.document.createElement('a');
const now = new Date();
const pain = [
now.getFullYear().toString(),
now.getMonth().toString().padStart(2, '0'),
now.getDay().toString().padStart(2, '0')].join('-');
dl.href = window.URL.createObjectURL(blob);
dl.download = `infoscreen-export_${pain}.csv`;
dl.style.display = 'none';
document.body.appendChild(dl);
dl.click();
document.body.removeChild(dl);
},
() => {
Snackbar.show({
title: 'Export failed',
style: { color: 'red' },
});
},
);
};
export default class InfoscreenTable {
constructor() {
this.handler = new ResourceHandler('events');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
return [
m('div', { style: { width: 'calc(100% - 36em)' } }, data.title_de || data.title_en),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_start)),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_advertising_start)),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_advertising_end)),
m('div',
{ style: { width: '9em' } },
data.img_infoscreen
? m('a', { href: getImgUrl(data.img_infoscreen) }, data.img_infoscreen.name)
: 'no image'),
];
}
view() {
const now = new Date();
return m(TableView, {
controller: this.ctrl,
keys: [
'titel_en',
'time_start',
'time_advertising_start',
'time_advertising_start',
'img_infoscreen'],
tileContent: this.getItemData,
titles: [
{ text: 'Title', width: 'calc(100% - 36em)' },
{ text: 'Start', width: '9em' },
{ text: 'Advertising Start', width: '9em' },
{ text: 'Advertising End', width: '9em' },
{ text: 'Infoscreen Image', width: '9em' },
],
filters: [[{
name: 'upcoming',
query: { time_start: { $gte: `${now.toISOString().slice(0, -5)}Z` } },
},
{
name: 'advertising upcoming',
query: { time_advertising_start: { $gte: `${now.toISOString().slice(0, -5)}Z` } },
},
{
name: 'advertising in progress',
query: {
time_advertising_start: { $lte: `${now.toISOString().slice(0, -5)}Z` },
time_advertising_end: { $gte: `${now.toISOString().slice(0, -5)}Z` },
},
},
],
[{
name: 'has image',
query: { img_infoscreen: { $ne: null } },
}]],
buttons: [
{ text: 'Export CSV', onclick: () => exportCSV(this.ctrl) },
],
// per default, enable the 'upcoming' filter
initFilterIdxs: [[0, 0], [1, 0]],
});
}
}
import m from 'mithril';
import { ResourceHandler } from './auth';
export default class ItemController {
constructor(resource, embedded) {
this.resource = resource;
this.id = m.route.param('id');
if (this.id) {
this.modus = 'view';
} else {
this.modus = 'new';
this.data = undefined;
}
this.handler = new ResourceHandler(resource);
this.embedded = embedded || {};
if (this.id) {
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
}
}
post(data) {
return new Promise((resolve, reject) => {
this.handler.post(data).then((response) => {
this.id = response._id;
resolve(response);
}).catch(reject);
});
}
patch(data) {
return new Promise((resolve, reject) => {
this.handler.patch(data).then((response) => {
resolve(response);
}).catch(reject);
});
}
cancel() {
if (this.modus === 'edit') this.changeModus('view');
else m.route.set(`/${this.resource}`);
}
changeModus(newModus) {
this.modus = newModus;
if (newModus === 'view') {
// reload item to current state, patches do not return embeddinds...
this.handler.getItem(this.id, this.embedded).then((item) => {
this.data = item;
m.redraw();
});
}
}
}
import m from 'mithril';
import { FileInput } from 'amiv-web-ui-components';
import EditView from '../views/editView';
export default class newJob extends EditView {
beforeSubmit() {
// remove all unchanged files
if (this.form.data.pdf !== undefined
&& (this.form.data.pdf === null || 'upload_date' in this.form.data.pdf)) {
delete this.form.data.pdf;
}
if (this.form.data.logo !== undefined
&& (this.form.data.logo === null || 'upload_date' in this.form.data.logo)) {
delete this.form.data.logo;
}
// post everyhing together as FormData
const submitData = new FormData();
Object.keys(this.form.data).forEach((key) => {
submitData.append(key, this.form.data[key]);
});
this.submit(submitData).then(() => this.controller.changeModus('view'));
}
view() {
return this.layout([
...this.form.renderSchema(['company']),
m(FileInput, this.form.bind({
name: 'logo',
label: 'Company Logo',
accept: 'image/png, image/jpeg',
})),
...this.form.renderSchema(['show_website', 'time_end', 'title_en']),
this.form._renderField('description_en', {
multiLine: true,
rows: 5,
...this.form.schema.properties.description_en,
}),
...this.form.renderSchema(['title_de']),
this.form._renderField('description_de', {
multiLine: true,
rows: 5,
...this.form.schema.properties.description_de,
}),
m(FileInput, this.form.bind({
name: 'pdf',
label: 'PDF',
accept: 'application/pdf',
})),
]);
}
}
import m from 'mithril';
import viewJob from './viewJob';
import editJob from './editJob';
import ItemController from '../itemcontroller';
import { loadingScreen } from '../layout';
export default class jobModal {
constructor() {
this.controller = new ItemController('joboffers');
}
view() {
if (!this.controller || (!this.controller.data && this.controller.modus !== 'new')) {
return m(loadingScreen);
}
if (this.controller.modus !== 'view') return m(editJob, { controller: this.controller });
return m(viewJob, { controller: this.controller });
}
}
import m from 'mithril';
import { DatalistController } from 'amiv-web-ui-components';
import TableView from '../views/tableView';
import { dateFormatter } from '../utils';
import { ResourceHandler } from '../auth';
/* Table of all current Jobs
*
* Makes use of the standard TableView
*/
export default class JobTable {
constructor() {
this.handler = new ResourceHandler('joboffers');
this.ctrl = new DatalistController((query, search) => this.handler.get({ search, ...query }));
}
getItemData(data) {
return [
m('div', { style: { width: 'calc(100% - 30em)' } }, data.title_de || data.title_en),
m('div', { style: { width: '21em' } }, data.company),
m('div', { style: { width: '9em' } }, dateFormatter(data.time_end)),
];
}
view(data) {
return m(TableView, {
controller: this.ctrl,
keys: [(data.title_de) ? 'title_de' : 'title_en', 'company', 'time_end'],
tileContent: this.getItemData,
titles: [
{ text: 'Title', width: 'calc(100% - 30em)' },
{ text: 'Company', width: '21em' },
{ text: 'End', width: '9em' },
],
onAdd: () => { m.route.set('/newjoboffer'); },
});
}
}