Skip to content
This repository has been archived by the owner on Sep 9, 2019. It is now read-only.

Commit

Permalink
Added holiday view
Browse files Browse the repository at this point in the history
  • Loading branch information
Philipp Fehr authored and tourn committed Jan 9, 2019
1 parent bd19e35 commit 0639693
Show file tree
Hide file tree
Showing 13 changed files with 632 additions and 6 deletions.
4 changes: 4 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"@types/react-jss": "^8.6.0",
"@types/react-router": "^4.4.3",
"@types/react-router-dom": "^4.3.1",
"@types/react-widgets": "^4.2.0",
"@types/react-widgets-moment": "^4.0.1",
"@types/reactstrap": "^6.4.3",
"@types/yup": "^0.26.4",
"axios": "^0.18.0",
Expand All @@ -26,6 +28,8 @@
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-scripts-ts": "3.1.0",
"react-widgets": "^4.4.8",
"react-widgets-moment": "^4.0.24",
"reactstrap": "^7.0.2",
"yup": "^0.26.6"
},
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import * as React from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'izitoast/dist/css/iziToast.css';
import 'react-widgets/dist/css/react-widgets.css';

import { Route, Switch } from 'react-router-dom';
import { IziviLayout } from './layout/IziviLayout';

import { Home } from './views/Home';
import { Login } from './views/Login';
import { ForgotPassword } from './views/ForgotPassword';
import { HolidayOverview } from './views/Holiday';

class App extends React.Component {
public render() {
Expand All @@ -16,6 +19,7 @@ class App extends React.Component {
<Route component={Home} exact path={'/'} />
<Route component={Login} exact path={'/login'} />
<Route component={ForgotPassword} exact path={'/forgotPassword'} />
<Route component={HolidayOverview} exact path={'/holidays'} />
<Route>
<>404</>
</Route>
Expand Down
55 changes: 54 additions & 1 deletion frontend/src/form/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import Input, { InputType } from 'reactstrap/lib/Input';
import FormGroup from 'reactstrap/lib/FormGroup';
import Label from 'reactstrap/lib/Label';
import FormFeedback from 'reactstrap/lib/FormFeedback';
import { DateTimePicker } from 'react-widgets';

export type FormProps = {
label: string;
label?: string;
children: ReactElement<any>; //tslint:disable-line:no-any
required?: boolean;
multiline?: boolean;
Expand All @@ -22,6 +23,25 @@ export type InputFieldProps = {
disabled?: boolean;
} & FormProps;

export type DateTimePickerFieldProps = FieldProps & {
label: string;
required?: boolean;
onChange?: (date?: Date) => void;
time?: boolean;
editFormat?: string;
format?: string;
value: Date;
delayed?: boolean;
disabled?: boolean;
};

export type SelectFieldProps = {
options: Array<{
id: string;
name: string;
}>;
} & InputFieldProps;

export const ValidatedFormGroupWithLabel = ({ label, field, form: { touched, errors }, children, required }: FormProps) => {
const hasErrors: boolean = !!errors[field.name] && !!touched[field.name];

Expand All @@ -46,10 +66,43 @@ const InputFieldWithValidation = ({ label, field, form, unit, required, multilin
);
};

const SelectFieldWithValidation = ({ label, field, form, unit, required, multiline, options, ...rest }: SelectFieldProps) => {
return (
<ValidatedFormGroupWithLabel label={label} field={field} form={form} required={required}>
<Input {...field} value={field.value === null ? '' : field.value} {...rest}>
{options.map(option => (
<option value={option.id} key={option.id}>
{option.name}
</option>
))}
</Input>
</ValidatedFormGroupWithLabel>
);
};

const DateTimePickerFieldWithValidation = ({ label, field, form, required, ...rest }: DateTimePickerFieldProps) => (
<ValidatedFormGroupWithLabel label={label} field={field} form={form} required={required}>
<DateTimePicker onChange={(date?: Date) => form.setFieldValue(field.name, date)} defaultValue={new Date(field.value)} {...rest} />
</ValidatedFormGroupWithLabel>
);

export const EmailField = (props: InputFieldProps) => <InputFieldWithValidation type={'email'} {...props} />;

export const NumberField = (props: InputFieldProps) => <InputFieldWithValidation type={'number'} {...props} />;

export const PasswordField = (props: InputFieldProps) => <InputFieldWithValidation type={'password'} {...props} />;

export const TextField = (props: InputFieldProps & { multiline?: boolean }) => <InputFieldWithValidation type={'text'} {...props} />;

export const DateField = (props: InputFieldProps) => <InputFieldWithValidation type={'date'} {...props} />;

export const DatePickerField = (props: DateTimePickerFieldProps) => (
<DateTimePickerFieldWithValidation
time={false}
editFormat={props.format ? props.format : 'DD.MM.YYYY'}
format={'DD.MM.YYYY'}
{...props}
/>
);

export const SelectField = (props: SelectFieldProps) => <SelectFieldWithValidation type={'select'} {...props} />;
6 changes: 6 additions & 0 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import * as Sentry from '@sentry/browser';
import { StoreProvider } from './utilities/StoreProvider';
import { ThemeProvider } from 'react-jss';
import { theme } from './layout/theme';
import moment from 'moment';
import 'moment/locale/de-ch';
import momentLocalizer from 'react-widgets-moment';

const browserHistory = createBrowserHistory();
const sentryDSN = 'SENTRY_DSN'; //this value will be replaced by a build script
Expand All @@ -19,6 +22,9 @@ if (sentryDSN.startsWith('https')) {
console.log('no raven');
}

moment.locale('de-ch');
momentLocalizer();

ReactDOM.render(
<StoreProvider history={browserHistory}>
<ThemeProvider theme={theme}>
Expand Down
135 changes: 135 additions & 0 deletions frontend/src/stores/domainStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//tslint:disable:no-console
import { action } from 'mobx';
import { MainStore } from './mainStore';

/**
* This class wraps all common store functions with success/error popups. The desired methods that start with "do" should be overriden in the specific stores.
*/
export class DomainStore<T, OverviewType = T> {
constructor(protected mainStore: MainStore) {}

protected get entityName() {
return { singular: 'Die Entität', plural: 'Die Entitäten' };
}

public get entity(): T | undefined {
throw new Error('Not implemented');
}

public set entity(e: T | undefined) {
throw new Error('Not implemented');
}

public get entities(): Array<OverviewType> {
throw new Error('Not implemented');
}

@action
public async fetchAll() {
try {
await this.doFetchAll();
} catch (e) {
this.mainStore.displayError(`${this.entityName.plural} konnten nicht geladen werden.`);
console.error(e);
throw e;
}
}

protected async doFetchAll() {
throw new Error('Not implemented');
}

@action
public async fetchOne(id: number) {
try {
this.entity = undefined;
return await this.doFetchOne(id);
} catch (e) {
this.mainStore.displayError(`${this.entityName.plural} konnten nicht geladen werden.`);
console.error(e);
throw e;
}
}

protected async doFetchOne(id: number): Promise<T | void> {
throw new Error('Not implemented');
}

@action
public async post(entity: T) {
this.displayLoading(async () => {
try {
await this.doPost(entity);
this.mainStore.displaySuccess(`${this.entityName.singular} wurde gespeichert.`);
} catch (e) {
this.mainStore.displayError(`${this.entityName.singular} konnte nicht gespeichert werden.`);
console.error(e);
throw e;
}
});
}

protected async doPost(entity: T) {
throw new Error('Not implemented');
}

@action
public async put(entity: T) {
this.displayLoading(async () => {
try {
await this.doPut(entity);
this.mainStore.displaySuccess(`${this.entityName.singular} wurde gespeichert.`);
} catch (e) {
this.mainStore.displayError(`${this.entityName.singular} konnte nicht gespeichert werden.`);
console.error(e);
throw e;
}
});
}

@action
protected async doPut(entity: T) {
throw new Error('Not implemented');
}

@action
public async delete(id: number) {
this.displayLoading(async () => {
try {
await this.doDelete(id);
this.mainStore.displaySuccess(`${this.entityName.singular} wurde gelöscht.`);
} catch (e) {
this.mainStore.displayError(`${this.entityName.singular} konnte nicht gelöscht werden.`);
console.error(e);
throw e;
}
});
}

@action
protected async doDelete(id: number) {
throw new Error('Not implemented');
}

public async displayLoading<T>(f: () => Promise<T>) {
//TODO: trigger loading indicator in MainStore
await f();
}

public async notifyProgress<P>(f: () => Promise<P>, { errorMessage = 'Fehler!', successMessage = 'Erfolg!' } = {}) {
this.displayLoading(async () => {
try {
await f();
if (successMessage) {
this.mainStore.displaySuccess(successMessage);
}
} catch (e) {
if (successMessage) {
this.mainStore.displayError(errorMessage);
}
console.error(e);
throw e;
}
});
}
}
65 changes: 65 additions & 0 deletions frontend/src/stores/holidayStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { action, computed, observable } from 'mobx';
import { MainStore } from './mainStore';
import { DomainStore } from './domainStore';
import { Holiday } from '../types';

export class HolidayStore extends DomainStore<Holiday> {
protected get entityName() {
return {
singular: 'Der Feiertag',
plural: 'Die Feiertage',
};
}

@computed
get entities(): Array<Holiday> {
return this.holidays;
}

@computed
get entity(): Holiday | undefined {
return this.holiday;
}

set entity(holiday: Holiday | undefined) {
this.holiday = holiday;
}

@observable
public holidays: Holiday[] = [];

@observable
public holiday?: Holiday;

constructor(mainStore: MainStore) {
super(mainStore);
}

public filter = (h: Holiday) => {
return [h.description, this.mainStore!.formatDate(h.date_from)].some(s => s.toLowerCase().includes(this.searchQuery));
};

@action
protected async doDelete(id: number) {
await this.mainStore.api.delete('/holidays/' + id);
await this.doFetchAll();
}

@action
protected async doFetchAll() {
const res = await this.mainStore.api.get<Holiday[]>('/holidays');
this.holidays = res.data;
}

@action
protected async doPost(holiday: Holiday) {
const response = await this.mainStore.api.post<Holiday[]>('/holidays', holiday);
this.holidays = response.data;
}

@action
protected async doPut(holiday: Holiday) {
const response = await this.mainStore.api.put<Holiday[]>('/holidays/' + holiday.id, holiday);
this.holidays = response.data;
}
}
27 changes: 26 additions & 1 deletion frontend/src/stores/mainStore.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import { observable } from 'mobx';
import { displayError, displaySuccess, displayWarning } from '../utilities/notification';
import { observable, action } from 'mobx';
import { ApiStore } from './apiStore';
import { History } from 'history';
import { Formatter } from 'src/utilities/formatter';

export class MainStore {
@observable
public navOpen = false;

@observable
showArchived = false;

public get api() {
return this.apiStore.api;
}

constructor(private apiStore: ApiStore, public readonly formatter: Formatter, private history: History) {}

// --- formatting
public formatDate = this.formatter.formatDate;
public formatDuration = this.formatter.formatDuration;
public formatCurrency = this.formatter.formatCurrency;
public trimString = this.formatter.trimString;

// --- routing / navigation
@action
public navigateTo(path: string): void {
this.history.push(path);
}

// --- notifications
public displayWarning = displayWarning;
public displaySuccess = displaySuccess;
public displayError = displayError;
Expand Down
Loading

0 comments on commit 0639693

Please sign in to comment.