Sfoglia il codice sorgente

most things working

Tomi Cvetic 6 anni fa
parent
commit
bd662ed958

+ 1 - 0
.gitignore

@@ -18,6 +18,7 @@ node_modules/
 .env.development.local
 .env.test.local
 .env.production.local
+local-constants.js
 
 npm-debug.log*
 yarn-debug.log*

+ 0 - 2
client/.env

@@ -1,2 +0,0 @@
-REACT_APP_DEBUG=SZTM*
-REACT_APP_API="https://sztmapi.slurm.ch"

+ 0 - 2
client/src/.env

@@ -1,2 +0,0 @@
-REACT_APP_DEBUG=SZTM*
-REACT_APP_API="https://sztmapi.slurm.ch"

+ 38 - 0
client/src/api/components/Login.js

@@ -0,0 +1,38 @@
+import React from 'react'
+
+class Login extends React.Component {
+
+    constructor () {
+      super()
+      this.handleSubmit = this.handleSubmit.bind(this)
+    }
+  
+    handleSubmit (event) {
+      event.preventDefault()
+      console.log(this.props.usersActions)
+      const { loginRequest } = this.props.usersActions
+      const data = {
+        username: this.username.value,
+        password: this.password.value
+      }
+      console.log('submit login data', data)
+      loginRequest(data)
+    }
+  
+
+  render () {
+      return (
+    <div>
+        <form ref='loginForm' onSubmit={this.handleSubmit}>
+        <label>Benutzername</label>
+        <input type='text' id='username' ref={(input) => {this.username = input}}/>
+        <label>Passwort</label>
+        <input type='password' id='password' ref={(input) => {this.password = input}}/>
+        <input type='submit' />
+        </form>
+    </div>
+      )
+  }
+}
+
+export default Login

+ 4 - 0
client/src/api/components/index.js

@@ -0,0 +1,4 @@
+import Login from './Login'
+
+export { Login }
+export default { Login }

+ 70 - 0
client/src/api/helpers.js

@@ -0,0 +1,70 @@
+function apiHandler (module, name, ...args) {
+    const funcional = name.split().map(word => `${word[0].toUpper()}${word.slice(1)}`).join()
+    const actional = name.split().map(word => word.toUpper()).join('_')
+
+    // e.g. name: 'create user'
+    // createUserRequest = (data) => {type: 'MODULE/CREATE_USER_REQUEST'}
+    const actions = {
+        [`${funcional}Request`] : (data) => {
+            type: `${module.toUpper()}/${actional}_REQUEST`,
+            data
+        },
+        [`${functional}Success`] : (data) => {
+            type: `${module.toUpper()}/${actional}_SUCCESS`,
+            data
+        },
+        [`${functional}Failure`] : (error) => {
+            type: `${module.toUpper()}/${actional}_FAILURE`,
+            error
+        }
+    }
+
+    // e.g. createUserState: [0] [1] [2]
+    const reducer = (state = {}, action) => {
+        switch (action) {
+            case `${module.toUpper()}/${actional}_REQUEST`:
+                return 
+            case `${module.toUpper()}/${actional}_SUCCESS`:
+                return
+            case `${module.toUpper()}/${actional}_FAILURE`:
+                return
+        }
+    }
+
+    if (args.variable)
+    const state = {
+        [variable]: args.variable.initState
+    }
+    const saga = async (action) => {}
+
+    return {actions, reducer, state, saga}
+}
+
+function crudHandler (module, name, ...args) {
+    const 
+    const actions = {}
+    const reducer = (state = [], action) => {
+        let index
+        switch (action.type) {
+            // Create
+            case '':
+                return [ ...state, action.data ]
+            // Read
+            case '':
+                return action.data
+            // Update
+            case '':
+                index = state.findIndex(element => element._id === action.data._id)
+                return [ ...state.slice(0, index), action.data, ...state.slice(index + 1) ]
+            // Delete
+            case '':
+                index = state.findIndex(element => element._id === action.data._id)
+                return [ ...state.slice(0, index), ...state.slice(index + 1) ]
+        }
+    }
+    const state = {}
+    const saga = async (action) => {}
+
+    return {actions, reducer, state, saga}
+}
+

+ 8 - 0
client/src/api/index.js

@@ -0,0 +1,8 @@
+import { actions, reducer, state, saga } from './state'
+import components from './components'
+
+const filters = {}
+
+const selectors = {}
+
+export default { actions, components, filters, selectors, reducer, state, saga }

+ 133 - 0
client/src/api/state.js

@@ -0,0 +1,133 @@
+/** @module users/state */
+import { call, put, takeEvery, select } from 'redux-saga/effects'
+import { replace } from 'connected-react-router'
+import jwt from 'jwt-simple'
+import { SZTM_API } from '../local-constants'
+
+/**
+* state.js
+*
+* Collection of everything which has to do with state changes.
+**/
+
+/** actionTypes define what actions are handeled by the reducer. */
+export const actions = {
+    apiRequest: (data) => {
+        return {
+            type: 'API/REQUEST',
+            data
+        }
+    },
+    apiSuccess: (data) => {
+        return {
+            type: 'API/SUCCESS',
+            data
+        }
+    },
+    apiFailure: (error) => {
+        return {
+            type: 'API/FAILURE',
+            error
+        }
+    },
+    apiAuthenticationExpired: () => {
+        return {
+            type: 'API/AUTHENTICATION_EXPIRED'
+        }
+    },
+}
+console.log('State actions', actions)
+
+/** state definition */
+
+// Check, if there is a valid token.
+function validateToken (token) {
+    const tokenData = !!token && jwt.decode(token, null, true)
+    const tokenValid = !!tokenData && tokenData.expires > Date.now()
+    return { token, tokenData, tokenValid }
+}
+const { token, tokenData, tokenValid } = validateToken(localStorage.getItem('accessToken'))
+
+export const state = {
+    loginStatus: 'uninitialized',
+    token,
+    tokenData,
+    tokenValid
+}
+console.log('State state', state)
+
+/** reducer is called by the redux dispatcher and handles all component actions */
+export function reducer (state = [], action) {
+    switch (action.type) {
+        case 'API/SUCCESS':
+            return { ...state, ...action.data }
+        case 'API/FAILURE':
+            return { ...state, loginRequested: false }
+        case 'API/AUTHENTICATION_EXPIRED':
+            return { ...state, tokenValid: false }
+        default:
+            return state
+    }
+}
+
+function * login (action) {
+    try {
+        const state = yield select()
+        console.log('User login requested', state, action) 
+        const response = yield call(fetch, `${SZTM_API}/authenticate/login`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify(action.data)
+        })
+        const responseJson = yield response.json()
+        if (response.status != 200) {
+            throw new Error(responseJson)
+        }
+        const { token, tokenData, tokenValid } = validateToken(responseJson)
+        localStorage.setItem('accessToken', token)
+        console.log('User login success!', token, tokenData)
+        yield put(actions.loginSuccess({token, tokenData, tokenValid}))
+        state.router.location.originalAction ? yield put(state.router.location.originalAction) : null
+        state.router.location.originalPath ? yield put(replace({pathname: state.router.location.originalPath})) : yield put(replace({pathname: '/swisstennis'}))
+    } catch (error) {
+        console.log('User login failure!', error)
+        yield put(actions.loginFailure(error))
+    }
+}
+
+function * api (action) {
+    const state = yield select()
+    console.log('API requested', action, 'state', state, 'token', token)
+    const { path, method, headers, body, onSuccess, onFailure } = action.data
+    if (!state.api.tokenValues || state.api.tokenValues.expires < new Date()) yield put(replace({
+        path: '/login',
+        state: {}
+    }))
+    const response = yield call(fetch, `${SZTM_API}/${path}`, {
+        method: method || 'GET',
+        headers: {
+            'Content-Type': 'application/json',
+            'x-access-token': token,
+            headers
+        },
+        body
+    })
+    console.log('API received response', response) 
+    if (response.status === 200) {
+        return yield put(onSuccess(yield response.json()))
+    } else if (response.status === 403) {
+        yield put(onFailure(yield response.json()))
+        return yield put(replace({pathname: '/login', originalAction: action, originalPath: state.router.location.pathname}))
+    } else {
+        return yield put(onFailure(yield response.text()))
+    }
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {
+    console.log('User saga started.')
+    yield takeEvery('API/REQUEST', api)
+    yield takeEvery('API/LOGIN', login)
+}

+ 1 - 1
client/src/config/components/ConfigList.js

@@ -67,7 +67,7 @@ class ConfigList extends React.Component {
               <td>{configData.value}</td>
               <td>
                 <a onClick={(event) => actions.configLoadForm(key)}>editieren</a>
-                <a onClick={(event) => actions.configDeleteRequest(key)}>loeschen</a>
+                <a onClick={(event) => actions.configDeleteRequest({key: configData.key})}>loeschen</a>
               </td>
             </tr>
             ) : ""}

+ 42 - 98
client/src/config/state.js

@@ -1,6 +1,7 @@
 /** @module config/state */
 import { call, put, takeEvery, select } from 'redux-saga/effects'
-import { SZTM_API } from '../config/static'
+import { SZTM_API } from '../local-constants'
+import api from '../api'
 
 /**
  * state.js
@@ -80,10 +81,10 @@ export const actions = {
       error
     }
   },
-  configDeleteRequest: (key) => {
+  configDeleteRequest: (data) => {
     return {
       type: 'CONFIG/DELETE_REQUEST',
-      key
+      data
     }
   },
   configDeleteSuccess: (data) => {
@@ -105,7 +106,7 @@ const emptyForm = {
   _id: null,
   key: '',
   description: '',
-  value: ''
+  value: '',
 }
 
 /** state definition */
@@ -121,7 +122,9 @@ console.log('State state', state)
 
 /** reducer is called by the redux dispatcher and handles all component actions */
 export function reducer (state = [], action) {
-  switch (action.type) {
+  let index
+  let newConfig
+  switch (action.type) { 
     case 'CONFIG/CLEAR_FORM':
       return { ...state, configForm: emptyForm }
     case 'CONFIG/LOAD_FORM':
@@ -135,25 +138,25 @@ export function reducer (state = [], action) {
     case 'CONFIG/ADD_REQUEST':
       return { ...state, addRequested: true }
     case 'CONFIG/ADD_SUCCESS':
-      console.log(action)
       return { ...state, configs: [ ...state.configs, action.data ], addRequested: false }
     case 'CONFIG/ADD_FAILURE':
       return { ...state, addRequested: false, alert: action.error }
     case 'CONFIG/EDIT_REQUEST':
       return { ...state, editRequested: true }
     case 'CONFIG/EDIT_SUCCESS':
-      const nextConfigs = state.configs.map(config => {
-        return config._id == action.data._id ? action.data : config
-      })
-      return { ...state, configs: nextConfigs, editRequested: false }
+      index = state.configs.findIndex(config => config._id === action.data._id)
+      newConfig = [ ...state.configs.slice(0, index), action.data, ...state.configs.slice(index + 1)]
+      return { ...state, configs: newConfig, editRequested: false }
     case 'CONFIG/EDIT_FAILURE':
       return { ...state, editRequested: false, alert: action.error }
     case 'CONFIG/DELETE_REQUEST':
       return { ...state, deleteRequested: true }
     case 'CONFIG/DELETE_SUCCESS':
-      const newConfigs = [ ...state.configs.slice(0, action.data), ...state.configs.slice(action.data + 1) ] 
-      delete newConfigs[action.data.key]
-      return { ...state, configs: newConfigs, deleteRequested: false }
+      index = state.configs.findIndex(config => config.key === action.data.key)
+      console.log('index', index)
+      newConfig = [ ...state.configs.slice(0, index), ...state.configs.slice(index + 1)]
+      console.log('newConfig', newConfig)
+      return { ...state, configs: newConfig, deleteRequested: false }
     case 'CONFIG/DELETE_FAILURE':
       return { ...state, deleteRequested: false, alert: action.error }
     default:
@@ -162,100 +165,41 @@ export function reducer (state = [], action) {
 }
 
 function * getConfig (action) {
-  try {
-    const token = localStorage.getItem('accessToken')
-    console.log('Get config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'GET',
-      headers: {
-        'Content-Type': 'application/json',
-        'x-access-token': token
-      }
-    }) 
-    if (response.status != 200) {
-      console.log(response)
-      throw Error(response.status)
-    } else {
-      const responseJson = yield response.json()
-      yield put(actions.configGetSuccess(responseJson))
-    }
-  } catch (error) {
-    console.log('Config failure!', actions.configGetFailure(error))
-    yield put(actions.configGetFailure(error))
-  }
+    yield put(api.actions.apiRequest({
+      path: 'api/config',
+      onSuccess: actions.configGetSuccess,
+      onFailure: actions.configGetFailure
+    }))
 }
 
 function * addConfig (action) {
-  try {
-    const token = localStorage.getItem('accessToken')
-    console.log('Add config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'POST',
-      headers: {
-        'Content-Type': 'application/json',
-        'x-access-token': token
-      },
-      body: JSON.stringify(action.data)
-    }) 
-    if (response.status != 200) {
-      throw Error(response.status)
-    } else {
-      yield put(actions.configAddSuccess(action.data))
-      yield put(actions.configGetRequest())
-    }
-  } catch (error) {
-    console.log('Config failure!', actions.configAddFailure(error))
-    yield put(actions.configAddFailure(error))
-  }
+  yield put(api.actions.apiRequest({
+    path: 'api/config',
+    method: 'POST',
+    body: JSON.stringify(action.data),
+    onSuccess: actions.configAddSuccess,
+    onFailure: actions.configAddFailure
+  }))
 }
 
 function * editConfig (action) {
-  try {
-    const token = localStorage.getItem('accessToken')
-    console.log('Edit config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'PUT',
-      headers: {
-        'Content-Type': 'application/json',
-        'x-access-token': token
-      },
-      body: JSON.stringify(action.data)
-    }) 
-    if (response.status != 200) {
-      throw Error(response.status)
-    } else {
-      yield put(actions.configEditSuccess(action.data))
-    }
-  } catch (error) {
-    console.log('Config failure!', actions.configEditFailure(error))
-    yield put(actions.configEditFailure(error))
-  }
+  yield put(api.actions.apiRequest({
+    path: 'api/config',
+    method: 'PUT',
+    body: JSON.stringify(action.data),
+    onSuccess: actions.configEditSuccess,
+    onFailure: actions.configEditFailure
+  }))
 }
 
 function * deleteConfig (action) {
-  try {
-    const token = localStorage.getItem('accessToken')
-    const state = yield select()
-    const { configs } = state.config
-    console.log('Delete config requested', action, token, state, configs)
-    const key = configs[action.key].key
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'DELETE',
-      headers: {
-        'Content-Type': 'application/json',
-        'x-access-token': token
-      },
-      body: JSON.stringify({ key })
-    }) 
-    if (response.status != 200) {
-      throw Error(response.status)
-    } else {
-      yield put(actions.configDeleteSuccess(action.key))
-    }
-  } catch (error) {
-    console.log('Config failure!', actions.configDeleteFailure(error))
-    yield put(actions.configDeleteFailure(error))
-  }
+  yield put(api.actions.apiRequest({
+    path: 'api/config',
+    method: 'DELETE',
+    body: JSON.stringify(action.data),
+    onSuccess: actions.configDeleteSuccess,
+    onFailure: actions.configDeleteFailure
+  }))
 }
 
 /** sagas are asynchronous workers (JS generators) to handle the state. */

+ 0 - 5
client/src/config/static.js

@@ -1,5 +0,0 @@
-//const SZTM_API = 'https://sztmapi.slurm.ch'
-const SZTM_API = 'http://localhost:3002'
-
-export { SZTM_API }
-export default { SZTM_API }

+ 0 - 1
client/src/constants.js

@@ -1 +0,0 @@
-const FILTER_OFF = 'Alle'

+ 68 - 0
client/src/email/components/SMS.js

@@ -0,0 +1,68 @@
+import React from 'react'
+
+class SMS extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+    this.addRecipient = this.addRecipient.bind(this)
+    this.submitForm = this.submitForm.bind(this)
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+    const { changeForm } = this.props.smsActions
+    changeForm({
+      sender: this.sender.value,
+      body: this.body.value,
+      newRecipient: this.newRecipient.value
+    })
+  }
+
+  submitForm (event) {
+    event.preventDefault()
+    const { sendSMSRequest } = this.props.smsActions
+    const state = this.props.sms
+    sendSMSRequest(state)
+  }
+
+  addRecipient (event) {
+    event.preventDefault()
+    const { addRecipient } = this.props.smsActions
+    addRecipient(this.newRecipient.value)
+  }
+
+  render () {
+    const state = this.props.sms
+    const actions = this.props.smsActions
+    const { sender, body, newRecipient } = state
+
+    return (
+      <div>
+        <h2>SMS</h2>
+        <form>
+          <label htmlFor="sender">Sender Name</label>
+          <input type="input" id="sender" ref={(input => {this.sender = input})} value={sender} placeholder="Sender Name" onChange={this.handleChange}/>
+          <br />
+          <label htmlFor="body">Nachricht</label>
+          <textarea id="body" ref={(input => {this.body = input})} value={body} placeholder="Nachricht" onChange={this.handleChange} />
+          <br />
+          <label htmlFor="newRecipient">Neuer Empfaenger</label>
+          <input type="input" id="newRecipient" ref={(input => {this.newRecipient = input})} value={newRecipient} placeholder="Neuer Empfaenger" onChange={this.handleChange}/>
+          <input type="button" value="ok" onClick={this.addRecipient}/>
+          <br />
+          <input type="submit" value="Senden" onClick={this.submitForm}/>
+        </form>
+        <h3>Nachricht</h3>
+        <pre>{JSON.stringify(state.message, null, 2)}</pre>
+        <h3>Alle Empfaenger</h3>
+        <ul>
+          {state.recipients.map((recipient, key) => 
+          <li key={key}>{(typeof recipient === 'string') ? recipient : `${recipient.fullName} (${recipient.phone ? recipient.phone : 'keine Nummer'})`}</li>
+          )}
+        </ul>
+      </div>
+    )
+  }
+}
+
+export default SMS

+ 4 - 0
client/src/email/components/index.js

@@ -0,0 +1,4 @@
+import SMS from './SMS'
+
+export { SMS }
+export default { SMS }

+ 10 - 0
client/src/email/index.js

@@ -0,0 +1,10 @@
+import { actions, reducer, state, saga } from './state'
+import components from './components'
+
+const filters = {
+  all: matches => matches
+}
+
+const selectors = {}
+
+export default { actions, components, filters, selectors, reducer, state, saga }

+ 122 - 0
client/src/email/state.js

@@ -0,0 +1,122 @@
+/** @module email/state */
+import { call, put, takeEvery } from 'redux-saga/effects'
+import { SZTM_API } from '../config/static'
+
+/**
+ * state.js
+ *
+ * Collection of everything which has to do with state changes.
+ **/
+
+/** actionTypes define what actions are handeled by the reducer. */
+export const actions = {
+  changeForm: (data) => {
+    return {
+      type: 'EMAIL/CHANGE_FORM',
+      data
+    }
+  },
+  setRecipients: (data) => {
+    return {
+      type: 'EMAIL/SET_RECIPIENTS',
+      data
+    }
+  },
+  addRecipient: (data) => {
+    return {
+      type: 'EMAIL/ADD_RECIPIENT',
+      data
+    }
+  },
+  sendEmailRequest: (data) => {
+    return {
+      type: 'EMAIL/SEND_REQUEST',
+      data
+    }
+  },
+  sendEmailSuccess: (data) => {
+    return {
+      type: 'EMAIL/SEND_SUCCESS',
+      data
+    }
+  },
+  sendEmailFailure: (error) => {
+    return {
+      type: 'EMAIL/SEND_FAILURE',
+      error
+    }
+  }
+}
+console.log('State actions', actions)
+
+/** state definition */
+export const state = {
+  recipients: [],
+  sender: 'SZTM',
+  body: '',
+  sending: false,
+  newRecipient: '',
+  message: ''
+}
+console.log('State state', state)
+
+/** reducer is called by the redux dispatcher and handles all component actions */
+export function reducer (state = [], action) {
+  switch (action.type) {
+    case 'SMS/CHANGE_FORM':
+      return { ...state, ...action.data }
+    case 'SMS/SET_RECIPIENTS':
+      return { ...state, recipients: action.data }
+    case 'SMS/ADD_RECIPIENT':
+      const number = normalizePhone(action.data)
+      if (number) {
+        return { ...state, recipients: [ number, ...state.recipients ], newRecipient: '' }
+      } else {
+        return state
+      }      
+    case 'SMS/SEND_REQUEST':
+      return { ...state, sending: true }
+    case 'SMS/SEND_SUCCESS':
+      return { ...state, sending: false, message: action.data, recipients: [], newRecipient: '', body: '' }
+    case 'SMS/SEND_FAILURE':
+      return { ...state, sending: false, message: action.error }
+    default:
+      return state
+  }
+}
+
+function * sendSMS (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Send SMS requested', action, token)
+    const { body, sender, recipients } = action.data
+    const response = yield call(fetch, `${SZTM_API}/api/sms/send`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'x-access-token': token
+      },
+      body: JSON.stringify({
+        recipients,
+        body,
+        sender
+      })
+    })
+    if (response.status != 200) {
+      console.log(response)
+      throw Error(yield response.json())
+    } else {
+      const responseJson = yield response.json()
+      yield put(actions.sendSMSSuccess(responseJson))
+    }
+  } catch (error) {
+    console.log('Config failure!', actions.sendSMSFailure(error.toString()))
+    yield put(actions.sendSMSFailure(error))
+  }
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {
+  console.log('SMS saga started.')
+  yield takeEvery('SMS/SEND_REQUEST', sendSMS)
+}

+ 0 - 5
client/src/index.css

@@ -1,5 +0,0 @@
-body {
-  margin: 0;
-  padding: 0;
-  font-family: sans-serif;
-}

+ 5 - 0
client/src/index.js

@@ -16,6 +16,7 @@ import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-reac
 import Main from './Main'
 
 // Import the submodules
+import api from './api'
 import matches from './matches'
 import layout from './layout'
 import alerts from './alerts'
@@ -36,6 +37,7 @@ console.log('history:', history)
 
 /** The root reducer is combined from all sub-module reducers */
 const rootReducer = combineReducers({
+  api: api.reducer,
   matches: matches.reducer,
   layout: layout.reducer,
   alerts: alerts.reducer,
@@ -48,6 +50,7 @@ console.log('Root reducer:', rootReducer)
 
 /** The default state is combined from all sub-module states */
 const defaultState = {
+  api: api.state,
   matches: matches.state,
   layout: layout.state,
   alerts: alerts.state,
@@ -62,6 +65,7 @@ console.log('Default state:', defaultState)
 function * rootSaga () {
   console.log('rootSaga called')
   yield all([
+    api.saga(),
     matches.saga(),
     layout.saga(),
     alerts.saga(),
@@ -126,6 +130,7 @@ sagaMiddleware.run(rootSaga)
 
 /** Collect the action creators from all modules in actionCreators */
 const actionCreators = {
+  api: api.actions,
   matches: matches.actions,
   layout: layout.actions,
   alerts: alerts.actions,

+ 11 - 11
client/src/layout/components/Header.js

@@ -1,6 +1,5 @@
 import React from 'react'
 import { Link } from 'react-router-dom'
-console.log('Link: ', Link)
 
 class Header extends React.Component {
     render () {
@@ -8,16 +7,17 @@ class Header extends React.Component {
     <header>
         <h1>SZTM Helfer</h1>
         <p>Spiellisten und Zahllisten einfach gemacht</p>
-        <nav>
-            <Link to='/users'>Benutzer</Link>
-            <Link to='/config'>Konfiguration</Link>
-            <Link to='/swisstennis'>Swisstennis</Link>
-            <Link to='/players'>Spieler</Link>
-            <Link to='/matches'>Matches</Link>
-            <Link to='/schedule'>Spielliste</Link>
-            <Link to='/paylist'>Zahlliste</Link>
-            <Link to='/sms'>SMS</Link>
-            <Link to='/email'>Emails</Link>
+        <nav className="navbar">
+            <ul className="navbar-nav">
+                <li className="nav-item"><Link to='/users'>Benutzer</Link></li>
+                <li className="nav-item"><Link to='/config'>Konfiguration</Link></li>
+                <li className="nav-item"><Link to='/swisstennis'>Swisstennis</Link></li>
+                <li className="nav-item"><Link to='/players'>Spieler</Link></li>
+                <li className="nav-item"><Link to='/matches'>Matches</Link></li>
+                <li className="nav-item"><Link to='/pdfs'>Listen</Link></li>
+                <li className="nav-item"><Link to='/sms'>SMS</Link></li>
+                <li className="nav-item"><Link to='/email'>Emails</Link></li>
+            </ul>
         </nav>
     </header>
         )

File diff suppressed because it is too large
+ 0 - 2
client/src/logo.svg


+ 5 - 2
client/src/matches/state.js

@@ -1,7 +1,7 @@
 /** @module matches/state */
 import { call, put, takeEvery, select } from 'redux-saga/effects'
 import moment from 'moment'
-import { SZTM_API } from '../config/static'
+import { SZTM_API } from '../local-constants'
 
 /**
  * state.js
@@ -188,7 +188,10 @@ function * setFilter (action) {
 
   const filteredPlayers = players.filter(player => {
     return (
-      (filter.category ? player.category.includes(filter.category) : true)
+      (filter.player ? player.fullName.includes(filter.player) : true) &&
+      (filter.category ? player.category.includes(filter.category) : true) &&
+      (filter.junior ? player.junior : true) &&
+      (filter.paid ? player.paid : true)
     )
   })
   

+ 7 - 2
client/src/sms/components/SMS.js

@@ -8,6 +8,11 @@ class SMS extends React.Component {
     this.submitForm = this.submitForm.bind(this)
   }
 
+  componentDidMount () {
+    const { getCreditRequest } = this.props.smsActions
+    getCreditRequest()
+  }
+
   handleChange (event) {
     event.preventDefault()
     const { changeForm } = this.props.smsActions
@@ -39,12 +44,14 @@ class SMS extends React.Component {
     return (
       <div>
         <h2>SMS</h2>
+        <p>Guthaben: {state.credit}</p>
         <form>
           <label htmlFor="sender">Sender Name</label>
           <input type="input" id="sender" ref={(input => {this.sender = input})} value={sender} placeholder="Sender Name" onChange={this.handleChange}/>
           <br />
           <label htmlFor="body">Nachricht</label>
           <textarea id="body" ref={(input => {this.body = input})} value={body} placeholder="Nachricht" onChange={this.handleChange} />
+          <p>{body.length}/160 Zeichen</p>
           <br />
           <label htmlFor="newRecipient">Neuer Empfaenger</label>
           <input type="input" id="newRecipient" ref={(input => {this.newRecipient = input})} value={newRecipient} placeholder="Neuer Empfaenger" onChange={this.handleChange}/>
@@ -52,8 +59,6 @@ class SMS extends React.Component {
           <br />
           <input type="submit" value="Senden" onClick={this.submitForm}/>
         </form>
-        <h3>Nachricht</h3>
-        <pre>{JSON.stringify(state.message, null, 2)}</pre>
         <h3>Alle Empfaenger</h3>
         <ul>
           {state.recipients.map((recipient, key) => 

+ 43 - 5
client/src/sms/state.js

@@ -1,7 +1,8 @@
 /** @module swisstennis/state */
 import { call, put, takeEvery } from 'redux-saga/effects'
-import { SZTM_API } from '../config/static'
+import { SZTM_API } from '../local-constants'
 import { normalizePhone } from '../helpers'
+import api from '../api'
 
 /**
  * state.js
@@ -46,6 +47,24 @@ export const actions = {
       type: 'SMS/SEND_FAILURE',
       error
     }
+  },
+  getCreditRequest: (data) => {
+    return {
+      type: 'SMS/CREDIT_REQUEST',
+      data
+    }
+  },
+  getCreditSuccess: (data) => {
+    return {
+      type: 'SMS/CREDIT_SUCCESS',
+      data
+    }
+  },
+  getCreditFailure: (error) => {
+    return {
+      type: 'SMS/CREDIT_FAILURE',
+      error
+    }
   }
 }
 console.log('State actions', actions)
@@ -57,7 +76,8 @@ export const state = {
   body: '',
   sending: false,
   newRecipient: '',
-  message: ''
+  message: '',
+  credit: '?'
 }
 console.log('State state', state)
 
@@ -67,7 +87,9 @@ export function reducer (state = [], action) {
     case 'SMS/CHANGE_FORM':
       return { ...state, ...action.data }
     case 'SMS/SET_RECIPIENTS':
-      return { ...state, recipients: action.data }
+      const recipients = action.data.map(player => player.phone).filter(number => !!number)
+      console.log(recipients)
+      return { ...state, recipients }
     case 'SMS/ADD_RECIPIENT':
       const number = normalizePhone(action.data)
       if (number) {
@@ -78,9 +100,15 @@ export function reducer (state = [], action) {
     case 'SMS/SEND_REQUEST':
       return { ...state, sending: true }
     case 'SMS/SEND_SUCCESS':
-      return { ...state, sending: false, message: action.data, recipients: [], newRecipient: '', body: '' }
+      return { ...state, sending: false, recipients: [], newRecipient: '', body: '' }
     case 'SMS/SEND_FAILURE':
-      return { ...state, sending: false, message: action.error }
+      return { ...state, sending: false, }
+    case 'SMS/CREDIT_REQUEST':
+      return { ...state, credit: '...' }
+    case 'SMS/CREDIT_SUCCESS':
+      return { ...state, credit: action.data }
+    case 'SMS/CREDIT_FAILURE':
+      return { ...state, credit: '?' }
     default:
       return state
   }
@@ -116,8 +144,18 @@ function * sendSMS (action) {
   }
 }
 
+function * getCredit (action) {
+  console.log('getting credit.')
+  yield put(api.actions.apiRequest({
+    path: 'api/sms/credits',
+    onSuccess: actions.getCreditSuccess,
+    onFailure: actions.getCreditFailure
+  }))
+}
+
 /** sagas are asynchronous workers (JS generators) to handle the state. */
 export function * saga () {
   console.log('SMS saga started.')
   yield takeEvery('SMS/SEND_REQUEST', sendSMS)
+  yield takeEvery('SMS/CREDIT_REQUEST', getCredit)
 }

+ 57 - 16
client/src/swisstennis/components/Swisstennis.js

@@ -5,6 +5,7 @@ class Swisstennis extends React.Component {
   constructor() {
     super()
     this.handleChange = this.handleChange.bind(this)
+    this.initDownload = this.initDownload.bind(this)
   }
 
   componentDidMount () {
@@ -19,30 +20,61 @@ class Swisstennis extends React.Component {
     event.preventDefault()
   }
 
+  initDownload (event) {
+    event.preventDefault()
+    const state = this.props.swisstennis
+    const { loginRequest } = this.props.swisstennisActions
+    loginRequest({ sequence: true })
+  }
+
+  indicate (status) {
+    if (status === 'uninitialized') {
+      return (<span>{'\u2610'}</span>)
+    } else if (status === 'request') {
+      return (<span style={{color: 'grey'}}>{'\u2610'}</span>)
+    } else if (status === 'success') {
+      return (<span style={{color: 'green'}}>{'\u2611'}</span>)
+    } else if (status === 'failure') {
+      return (<span style={{color: 'red'}}>{'\u2612'}</span>)
+    }
+  }
+
+  statusValue (status) {
+    if (status === 'uninitialized' || status === 'failure') return 0
+    if (status === 'request') return 1
+    if (status === 'success') return 2
+  }
+
   render () {
     const state = this.props.swisstennis
     const actions = this.props.swisstennisActions
+    const progress = this.statusValue(state.loginStatus) + this.statusValue(state.playerListDownloadStatus) +
+      this.statusValue(state.playerListParseStatus) + this.statusValue(state.calendarDownloadStatus) +
+      this.statusValue(state.calendarParseStatus)
+    
     return (
       <div>
         <h2>Download</h2>
-        <button onClick={null}>Importieren</button><br />
-        <progress value={4} max={7}></progress>
+        <button onClick={this.initDownload}>Importieren</button><br />
+        <progress value={progress} max={10}>{progress / 10}%</progress>
         <table>
+          <tbody>
           <tr>
-            <td>Login</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+            <td>Login</td><td>{this.indicate(state.loginStatus)}</td>
           </tr>
           <tr>
-            <td>PlayerList herunterladen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+            <td>PlayerList herunterladen</td><td>{this.indicate(state.playerListDownloadStatus)}</td>
           </tr>
           <tr>
-            <td>PlayerList einlesen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+            <td>PlayerList einlesen</td><td>{this.indicate(state.playerListParseStatus)}</td>
           </tr>
           <tr>
-            <td>Calendar herunterladen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+            <td>Calendar herunterladen</td><td>{this.indicate(state.calendarDownloadStatus)}</td>
           </tr>
           <tr>
-            <td>Calendar einlesen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+            <td>Calendar einlesen</td><td>{this.indicate(state.calendarParseStatus)}</td>
           </tr>
+          </tbody>
         </table>
 
         <h2>Dateien</h2>
@@ -53,11 +85,20 @@ class Swisstennis extends React.Component {
             </tr>
           </thead>
           <tbody>
-            {state.files.map(file => (
-            <tr>
-              <td>{file.filename}</td><td>{file.size/1024}kB</td><td>{moment(file.ctime).format('DD.MM.YYYY HH:mm')}</td><td>{(state.calendars.find(matchList => matchList.file == file.filename) || state.playerLists.find(playerList => playerList.file == file.filename)) ? 'Ja' : 'Nein'}</td><td><a>loeschen</a></td>
-            </tr>
-            ))}
+            {state.files.map((file, key) => {
+              return 
+            (<tr key={key}>
+              <td>{file.filename}</td>
+              <td>{file.size/1024}kB</td>
+              <td>{moment(file.ctime).format('DD.MM.YYYY HH:mm')}</td>
+              <td>{(state.calendars.find(matchList => matchList.file == file.filename) || 
+                           state.playerLists.find(playerList => playerList.file == file.filename)) ? 'Ja' : 'Nein'}</td>
+              <td>
+                <a>benutzen</a>
+                <a>loeschen</a>
+              </td>
+            </tr>)
+            })}
           </tbody>
         </table>
 
@@ -69,8 +110,8 @@ class Swisstennis extends React.Component {
             </tr>
           </thead>
           <tbody>
-            {state.calendars.sort((a, b) => a.imported < b.imported).map(calendar => (
-            <tr>
+            {state.calendars.sort((a, b) => a.imported < b.imported).map((calendar, key) => (
+            <tr key={key}>
               <td>{calendar.file}</td><td>{moment(calendar.imported).format('DD.MM.YYYY HH:mm')}</td><td>{calendar.matches.length}</td><td><a>verwenden</a><a>loeschen</a></td>
             </tr>
             ))}
@@ -85,8 +126,8 @@ class Swisstennis extends React.Component {
             </tr>
           </thead>
           <tbody>
-            {state.playerLists.sort((a, b) => a.imported < b.imported).map(playerList => (
-            <tr>
+            {state.playerLists.sort((a, b) => a.imported < b.imported).map((playerList, key) => (
+            <tr key={key}>
               <td>{playerList.file}</td><td>{moment(playerList.imported).format('DD.MM.YYYY HH:mm')}</td><td>{playerList.players.length}</td><td><a>verwenden</a><a>loeschen</a></td>
             </tr>
             ))}

+ 170 - 78
client/src/swisstennis/state.js

@@ -1,6 +1,6 @@
 /** @module swisstennis/state */
 import { call, put, takeEvery } from 'redux-saga/effects'
-import { SZTM_API } from '../config/static'
+import { SZTM_API } from '../local-constants'
 
 /**
  * state.js
@@ -27,43 +27,28 @@ export const actions = {
       error
     }
   },
-  loginRequest: () => {
+  loginRequest: (data) => {
     return {
-      type: 'SWISSTENNIS/LOGIN_REQUEST'
-    }
-  },
-  loginSuccess: (key) => {
-    return {
-      type: 'SWISSTENNIS/LOGIN_SUCCESS',
-      key
-    }
-  },
-  loginFailure: (data) => {
-    return {
-      type: 'SWISSTENNIS/LOGIN_FAILURE',
+      type: 'SWISSTENNIS/LOGIN_REQUEST',
       data
     }
   },
-  getPlayerListRequest: () => {
+  loginSuccess: (data) => {
     return {
-      type: 'SWISSTENNIS/GET_PLAYERLIST_REQUEST'
-    }
-  },
-  getPlayerListSuccess: (data) => {
-    return {
-      type: 'SWISSTENNIS/GET_PLAYERLIST_SUCCESS',
+      type: 'SWISSTENNIS/LOGIN_SUCCESS',
       data
     }
   },
-  getPlayerListFailure: error => {
+  loginFailure: (error) => {
     return {
-      type: 'SWISSTENNIS/GET_PLAYERLIST_FAILURE',
+      type: 'SWISSTENNIS/LOGIN_FAILURE',
       error
     }
   },
-  downloadPlayerListRequest: () => {
+  downloadPlayerListRequest: (data) => {
     return {
-      type: 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_REQUEST'
+      type: 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_REQUEST',
+      data
     }
   },
   downloadPlayerListSuccess: (data) => {
@@ -78,21 +63,21 @@ export const actions = {
       error
     }
   },
-  getCalendarRequest: (data) => {
+  parsePlayerListRequest: (data) => {
     return {
-      type: 'SWISSTENNIS/GET_CALENDAR_REQUEST',
+      type: 'SWISSTENNIS/PARSE_PLAYERLIST_REQUEST',
       data
     }
   },
-  getCalendarSuccess: (data) => {
+  parsePlayerListSuccess: (data) => {
     return {
-      type: 'SWISSTENNIS/GET_CALENDAR_SUCCESS',
+      type: 'SWISSTENNIS/PARSE_PLAYERLIST_SUCCESS',
       data
     }
   },
-  getCalendarFailure: error => {
+  parsePlayerListFailure: error => {
     return {
-      type: 'SWISSTENNIS/GET_CALENDAR_FAILURE',
+      type: 'SWISSTENNIS/PARSE_PLAYERLIST_FAILURE',
       error
     }
   },
@@ -114,39 +99,56 @@ export const actions = {
       error
     }
   },
-  parsePlayerListRequest: (data) => {
+  parseCalendarRequest: data => {
     return {
-      type: 'SWISSTENNIS/PARSE_PLAYERLIST_REQUEST',
+      type: 'SWISSTENNIS/PARSE_CALENDAR_REQUEST',
       data
     }
   },
-  parsePlayerListSuccess: (data) => {
+  parseCalendarSuccess: data => {
     return {
-      type: 'SWISSTENNIS/PARSE_PLAYERLIST_SUCCESS',
+      type: 'SWISSTENNIS/PARSE_CALENDAR_SUCCESS',
       data
     }
   },
-  parsePlayerListFailure: error => {
+  parseCalendarFailure: error => {
     return {
-      type: 'SWISSTENNIS/PARSE_PLAYERLIST_FAILURE',
+      type: 'SWISSTENNIS/PARSE_CALENDAR_FAILURE',
       error
     }
   },
-  parseCalendarRequest: (key) => {
+  getPlayerListRequest: () => {
     return {
-      type: 'SWISSTENNIS/PARSE_CALENDAR_REQUEST',
-      key
+      type: 'SWISSTENNIS/GET_PLAYERLIST_REQUEST'
     }
   },
-  parseCalendarSuccess: (data) => {
+  getPlayerListSuccess: (data) => {
     return {
-      type: 'SWISSTENNIS/PARSE_CALENDAR_SUCCESS',
+      type: 'SWISSTENNIS/GET_PLAYERLIST_SUCCESS',
       data
     }
   },
-  parseCalendarFailure: error => {
+  getPlayerListFailure: error => {
     return {
-      type: 'SWISSTENNIS/PARSE_CALENDAR_FAILURE',
+      type: 'SWISSTENNIS/GET_PLAYERLIST_FAILURE',
+      error
+    }
+  },
+  getCalendarRequest: (data) => {
+    return {
+      type: 'SWISSTENNIS/GET_CALENDAR_REQUEST',
+      data
+    }
+  },
+  getCalendarSuccess: (data) => {
+    return {
+      type: 'SWISSTENNIS/GET_CALENDAR_SUCCESS',
+      data
+    }
+  },
+  getCalendarFailure: error => {
+    return {
+      type: 'SWISSTENNIS/GET_CALENDAR_FAILURE',
       error
     }
   },
@@ -161,8 +163,11 @@ export const state = {
   calendarsStatus: 'uninitialized',
   playerLists: [],
   playerListsStatus: 'uninitialized',
-  transferStarted: false,
-  loginState: false,
+  playerListDownloadStatus: 'uninitialized',
+  playerListParseStatus: 'uninitialized',
+  calendarDownloadStatus: 'uninitialized',
+  calendarParseStatus: 'uninitialized',
+  loginStatus: 'uninitialized',
 }
 console.log('State state', state)
 
@@ -170,11 +175,47 @@ console.log('State state', state)
 export function reducer (state = [], action) {
   switch (action.type) {
     case 'SWISSTENNIS/FILE_LIST_REQUEST':
-      return { ...state, fileListStatus: 'loading' }
+      return { ...state, fileListStatus: 'request' }
     case 'SWISSTENNIS/FILE_LIST_SUCCESS':
-      return { ...state, fileListStatus: 'loaded', files: action.data }
+      return { ...state, fileListStatus: 'success', files: action.data }
     case 'SWISSTENNIS/FILE_LIST_FAILURE':
-      return { ...state, fileListStatus: 'failed' }
+      return { ...state, fileListStatus: 'failure' }
+    case 'SWISSTENNIS/LOGIN_REQUEST':
+      return { ...state, 
+        loginStatus: 'request', 
+        playerListDownloadStatus: action.data.sequence ? 'uninitialized' : state.playerListDownloadStatus,
+        playerListParseStatus: action.data.sequence ? 'uninitialized' : state.playerListParseStatus,
+        calendarDownloadStatus: action.data.sequence ? 'uninitialized' : state.calendarDownloadStatus,
+        calendarParseStatus: action.data.sequence ? 'uninitialized' : state.calendarParseStatus,
+         }
+    case 'SWISSTENNIS/LOGIN_SUCCESS':
+      return { ...state, loginStatus: 'success' }
+    case 'SWISSTENNIS/LOGIN_FAILURE':
+      return { ...state, loginStatus: 'failure' }
+    case 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_REQUEST':
+      return { ...state, playerListDownloadStatus: 'request' }
+    case 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_SUCCESS':
+      return { ...state, playerListDownloadStatus: 'success', files: [ ...state.files, action.data ] }
+    case 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_FAILURE':
+      return { ...state, playerListDownloadStatus: 'failure' }
+    case 'SWISSTENNIS/PARSE_PLAYERLIST_REQUEST':
+      return { ...state, playerListParseStatus: 'request' }
+    case 'SWISSTENNIS/PARSE_PLAYERLIST_SUCCESS':
+      return { ...state, playerListParseStatus: 'success', playerLists: [ ...state.playerLists, action.data.playerList ] }
+    case 'SWISSTENNIS/PARSE_PLAYERLIST_FAILURE':
+      return { ...state, playerListParseStatus: 'failure' }
+    case 'SWISSTENNIS/DOWNLOAD_CALENDAR_REQUEST':
+      return { ...state, calendarDownloadStatus: 'request' }
+    case 'SWISSTENNIS/DOWNLOAD_CALENDAR_SUCCESS':
+      return { ...state, calendarDownloadStatus: 'success', files: [ ...state.files, action.data ] }
+    case 'SWISSTENNIS/DOWNLOAD_CALENDAR_FAILURE':
+      return { ...state, calendarDownloadStatus: 'failure' }
+    case 'SWISSTENNIS/PARSE_CALENDAR_REQUEST':
+      return { ...state, calendarParseStatus: 'request' }
+    case 'SWISSTENNIS/PARSE_CALENDAR_SUCCESS':
+      return { ...state, calendarParseStatus: 'success', calendars: [ ...state.calendars, action.data.matchList ] }
+    case 'SWISSTENNIS/PARSE_CALENDAR_FAILURE':
+      return { ...state, calendarParseStatus: 'failure' }
     case 'SWISSTENNIS/GET_CALENDAR_REQUEST':
       return { ...state, calendarsStatus: 'loading' }
     case 'SWISSTENNIS/GET_CALENDAR_SUCCESS':
@@ -219,9 +260,9 @@ function * fileList (action) {
 function * login (action) {
   try {
     const token = localStorage.getItem('accessToken')
-    console.log('Get config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'GET',
+    console.log('Swisstennis login requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/login`, {
+      method: 'POST',
       headers: {
         'Content-Type': 'application/json',
         'x-access-token': token
@@ -232,11 +273,12 @@ function * login (action) {
       throw Error(response.status)
     } else {
       const responseJson = yield response.json()
-      yield put(actions.configGetSuccess(responseJson))
+      yield put(actions.loginSuccess(responseJson))
+      if (action.data.sequence) yield put(actions.downloadPlayerListRequest({sequence: true}))
     }
   } catch (error) {
-    console.log('Config failure!', actions.configAddFailure(error))
-    yield put(actions.configAddFailure(error))
+    console.log('Config failure!', actions.loginFailure(error))
+    yield put(actions.loginFailure(error))
   }
 }
 
@@ -263,11 +305,58 @@ function * getCalendars (action) {
   }
 }
 
+function * downloadCalendar (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Download calendar requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/calendar/download`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        'x-access-token': token
+      }
+    }) 
+    if (response.status != 200) {
+      throw Error(response.status)
+    } else {
+      const responseJson = yield response.json()
+      yield put(actions.downloadCalendarSuccess(responseJson))
+      if (action.data.sequence) yield put(actions.parseCalendarRequest({filename: responseJson.filename, sequence: true}))
+    }
+  } catch (error) {
+    console.log('Config failure!', actions.downloadCalendarFailure(error))
+    yield put(actions.downloadCalendarFailure(error))
+  }
+}
+
+function * parseCalendar (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Download calendar requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/calendar/parse/${action.data.filename}`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        'x-access-token': token
+      }
+    }) 
+    if (response.status != 200) {
+      throw Error(response.status)
+    } else {
+      const responseJson = yield response.json()
+      yield put(actions.parseCalendarSuccess(responseJson))
+    }
+  } catch (error) {
+    console.log('Config failure!', actions.parseCalendarFailure(error))
+    yield put(actions.parseCalendarFailure(error))
+  }
+}
+
 function * getPlayerLists (action) {
   try {
     const token = localStorage.getItem('accessToken')
     console.log('Get config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/playerList`, {
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/playerlist`, {
       method: 'GET',
       headers: {
         'Content-Type': 'application/json',
@@ -286,50 +375,49 @@ function * getPlayerLists (action) {
   }
 }
 
-function * addConfig (action) {
+function * downloadPlayerList (action) {
   try {
     const token = localStorage.getItem('accessToken')
-    console.log('Add config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'POST',
+    console.log('Playerlist download requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/playerlist/download`, {
+      method: 'GET',
       headers: {
-        'Content-Type': 'application/json',
         'x-access-token': token
-      },
-      body: JSON.stringify(action.data)
+      }
     }) 
     if (response.status != 200) {
       throw Error(response.status)
     } else {
-      yield put(actions.configAddSuccess(action.data))
-      yield put(actions.configGetRequest())
+      const responseJson = yield response.json()
+      yield put(actions.downloadPlayerListSuccess(responseJson))
+      if (action.data.sequence) yield put(actions.parsePlayerListRequest({filename: responseJson.filename, sequence: true}))
     }
   } catch (error) {
-    console.log('Config failure!', actions.configAddFailure(error))
-    yield put(actions.configAddFailure(error))
+    console.log('Config failure!', actions.downloadPlayerListFailure(error))
+    yield put(actions.downloadPlayerListFailure(error))
   }
 }
 
-function * editConfig (action) {
+function * parsePlayerList (action) {
   try {
     const token = localStorage.getItem('accessToken')
-    console.log('Edit config requested', action, token)
-    const response = yield call(fetch, `${SZTM_API}/api/config`, {
-      method: 'PUT',
+    console.log('PlayerList parse requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/playerlist/parse/${action.data.filename}`, {
+      method: 'GET',
       headers: {
-        'Content-Type': 'application/json',
         'x-access-token': token
-      },
-      body: JSON.stringify(action.data)
-    }) 
+      }
+    })
     if (response.status != 200) {
       throw Error(response.status)
     } else {
-      yield put(actions.configEditSuccess(action.data))
+      const responseJson = yield response.json()
+      yield put(actions.parsePlayerListSuccess(responseJson))
+      if (action.data.sequence) yield put(actions.downloadCalendarRequest({sequence: true}))
     }
   } catch (error) {
-    console.log('Config failure!', actions.configEditFailure(error))
-    yield put(actions.configEditFailure(error))
+    console.log('Config failure!', actions.parsePlayerListFailure(error))
+    yield put(actions.parsePlayerListFailure(error))
   }
 }
 
@@ -337,6 +425,10 @@ function * editConfig (action) {
 export function * saga () {
   console.log('Config saga started.')
   yield takeEvery('SWISSTENNIS/FILE_LIST_REQUEST', fileList)
+  yield takeEvery('SWISSTENNIS/DOWNLOAD_PLAYERLIST_REQUEST', downloadPlayerList)
+  yield takeEvery('SWISSTENNIS/PARSE_PLAYERLIST_REQUEST', parsePlayerList)
+  yield takeEvery('SWISSTENNIS/DOWNLOAD_CALENDAR_REQUEST', downloadCalendar)
+  yield takeEvery('SWISSTENNIS/PARSE_CALENDAR_REQUEST', parseCalendar)
   yield takeEvery('SWISSTENNIS/GET_CALENDAR_REQUEST', getCalendars)
   yield takeEvery('SWISSTENNIS/GET_PLAYERLIST_REQUEST', getPlayerLists)
   yield takeEvery('SWISSTENNIS/LOGIN_REQUEST', login)

+ 1 - 1
client/src/users/state.js

@@ -2,7 +2,7 @@
 import { call, put, takeEvery } from 'redux-saga/effects'
 import jwt from 'jwt-simple'
 import bcrypt from 'bcryptjs'
-import { SZTM_API } from '../config/static'
+import { SZTM_API } from '../local-constants'
 
 /**
 * state.js

+ 3 - 0
server/Dockerfile

@@ -1,5 +1,8 @@
 FROM node:latest
 
+RUN echo "Europe/Zurich" > /etc/timezone && \
+    dpkg-reconfigure -f noninteractive tzdata
+
 WORKDIR /usr/src
 
 ENV PATH /usr/src/node_modules/.bin:$PATH

+ 1 - 1
server/src/restServer/helpers.js

@@ -28,7 +28,7 @@ function fileSize (int) {
     value = `${int / Math.pow(2, 30)}GB`
   }
   return value
-}
+} 
 
 function normalizePhone (item) {
   const phone = item ? String(item).replace(/\s|\+|\/|,|-|'/g, '').replace(/\(0\)/, '').replace(/^0+/, '') : ''

+ 10 - 8
server/src/restServer/models/player.js

@@ -29,23 +29,25 @@ const PlayerSchema = new mongoose.Schema({
   idString: String,
 })
 
-PlayerSchema.methods.equal = function (playerData, cb) {
+PlayerSchema.methods.delta = function (playerData, cb) {
+  const delta = {}
   for (let property in playerData) {
-    if (property == 'created') continue
-    if (property == 'paid') console.log('paid', playerData[property], this[property])
     if (this[property] instanceof Date) {
       if (this[property].getTime() != playerData[property].getTime()) {
-        console.log('dates dont match', property, this[property], playerData[property])
-        return false
+        delta[property] = {a: this[property], b: playerData[property]}
       }
     } else {
       if (this[property] != playerData[property]) {
-        console.log('property doesnt match', property, this[property], typeof this[property], playerData[property], typeof playerData[property])
-        return false
+        delta[property] = {a: this[property], b: playerData[property]}
       }
     }
   }
-  return true
+  return delta
+}
+
+PlayerSchema.methods.equal = function (playerData, cb) {
+  const { created, _id, ...rest } = this.delta(playerData)
+  return (Object.keys(rest).length === 0)
 }
 
 const Player = mongoose.model('Player', PlayerSchema)

+ 10 - 11
server/src/restServer/routes/config.js

@@ -4,20 +4,19 @@ import Config from '../models/config'
 
 const config = express.Router()
 
-config.post('/', (req, res) => {
+config.post('/', wrapAsync(async (req, res) => {
     try{
         const { key, value, description } = req.body
-        console.log(req.body)
         if (!key || !value) {
             throw Error('parameters key and value are mandatory.')
         }
         const configPair = new Config({ key, value, description })
-        configPair.save()
-        res.json({ msg: `Successfully added ${key}=${value}.` })
+        await configPair.save()
+        res.json(configPair)
     } catch (error) {
         res.status(400).json({ msg: error.toString() })
     }
-})
+}))
 
 config.get('/', wrapAsync(async (req, res) => {
     try {
@@ -34,7 +33,7 @@ config.get('/:key', wrapAsync(async (req, res) => {
         if (!key) {
             throw Error('parameter key is mandatory.')
         }
-        const config = await Config.find({ key })
+        const config = await Config.findOne({ key })
         res.json({ config })
     } catch (error) {
         res.status(400).json({ msg: error.toString() })
@@ -48,11 +47,11 @@ config.put('/', wrapAsync(async (req, res) => {
             throw Error('parameters key and value are mandatory.')
         }
         const updated = await Config.findOneAndUpdate({ key }, { value, description })
-        if (updated) {
-            res.json({ msg: `Successfully updated ${key} to ${value}.` })
-        } else {
+        if (!updated) {
             throw Error('key not found!')
-        }        
+        }  
+        const config = await Config.findOne({ key })   
+        res.json(config)   
     } catch (error) {
         res.status(400).json({ msg: error.toString() })
     }
@@ -66,7 +65,7 @@ config.delete('/', wrapAsync(async (req, res) => {
         }
         const deleted = await Config.findOneAndRemove({ key })
         if (deleted) {
-            res.json({ msg: `Successfully deleted ${key}.` })
+            res.json({ key })
         } else {
             throw Error('key not found!') 
         }

+ 2 - 2
server/src/restServer/routes/sms.js

@@ -43,7 +43,7 @@ sms.post('/send', async (req, res) => {
     }
 })
 
-sms.get('/profile', async (req, res) => {
+sms.get('/credits', async (req, res) => {
     try {
         const Authorization = `Basic ${base64.encode(`${bulksms.tokenId}:${bulksms.tokenSecret}`)}`
         const response = await fetch(`https://api.bulksms.com/v1/profile`, {
@@ -57,7 +57,7 @@ sms.get('/profile', async (req, res) => {
             throw Error(`Received status code ${response.status}`)
         }
         const responseJson = await response.json()
-        res.json(responseJson)
+        res.json(responseJson.credits.balance)
     } catch (error) {
         return res.status(400).json({ msg: error.toString() })
     }

+ 48 - 23
server/src/restServer/routes/swisstennis.js

@@ -8,6 +8,7 @@ import Player from '../models/player'
 import Match from '../models/match'
 import PlayerList from '../models/playerList'
 import MatchList from '../models/matchList'
+import moment from 'moment'
 import { normalize, normalizePhone } from '../helpers'
 
 // Create the router
@@ -28,8 +29,10 @@ Config.find({ key: { $in: ['tournament', 'tournamentId', 'tournamentPW'] } }).ex
   }
 })
 
-function fileDate (date) {
-  return `${(date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate()) * 1000000 + date.getHours() * 10000 + date.getMinutes() * 100 + date.getSeconds()}`
+async function checkLogin () {
+  const myTournamentsPage = await session.get('https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D')
+  const strMyTournamentsPage = myTournamentsPage.body.toString()
+  return strMyTournamentsPage.includes('Login-Zone') ? null : strMyTournamentsPage
 }
 
 /*
@@ -39,6 +42,12 @@ function fileDate (date) {
 // Login
 swisstennis.post('/login', async (req, res) => {
   try {
+    const currentState = await checkLogin()
+    console.log(currentState)
+    if (currentState) {
+      return res.json({ msg: 'Already logged in!' })
+    }
+
     const username = req.body.username || config.tournament
     const password = req.body.pasword || config.tournamentPW
     
@@ -54,7 +63,6 @@ swisstennis.post('/login', async (req, res) => {
       pwd: password,
       Tournament: ''
     }
-    const myTournamentsPage = await session.get('https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D')
     const loginPage = await session.post('https://comp.swisstennis.ch/advantage/servlet/Login', loginData)
     const strLoginPage = loginPage.body.toString()
     if (strLoginPage.includes('Zugriff verweigert')) {
@@ -71,11 +79,9 @@ swisstennis.get('/tournaments', async (req, res) => {
   try {
     const tournaments = {}
     let match
-    const myTournamentsPage = await session.get('https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D')
-    const strMyTournamentsPage = myTournamentsPage.body.toString()
-    if (strMyTournamentsPage.includes('Login-Zone')) {
-      throw Error('Not logged in.')
-    }
+    const myTournamentsPage = await checkLogin()
+    if (!myTournamentsPage) throw Error('Not logged in!')
+
     const tournamentRegexp = /<a href=".*ProtectedDisplayTournament.*tournament=Id(\d+)">([^<]+)<\/a>/gm
     
     do {
@@ -141,10 +147,17 @@ swisstennis.get('/playerlist/download', async (req, res) => {
     if (!tournament) {
       throw Error('No tournament given.')
     }
-    const playerListFile = fs.createWriteStream(`swisstennis_files/PlayerList-${tournament}-${fileDate(new Date())}.xls`)
+    const filename = `PlayerList-${tournament}-${moment().format('YYYYMMDDTHHmmss')}.xls`
+    const playerListFile = fs.createWriteStream(`swisstennis_files/${filename}`)
     const playerList = await session.get(`https://comp.swisstennis.ch/advantage/servlet/PlayerList.xls?tournament=Id${tournament}&lang=D`, {stream: true})
     playerList.pipe(playerListFile)
-    return res.json({ msg: 'Download successful.' })
+    const streamDone = new Promise((resolve, reject) => {
+      playerListFile.on('finish', resolve)
+      playerListFile.on('error', reject)
+    })
+    await streamDone
+    const stats = fs.statSync(`swisstennis_files/${filename}`)
+    return res.json({ filename, size: stats.size, ctime: stats.ctime })
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
   }
@@ -155,17 +168,17 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
     console.log('Parsing file', req.params.filename)
     
     const filePath = `swisstennis_files/${req.params.filename}`
-    const dateElems = req.params.filename.match(/\w+\-\d+\-(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.xls/).slice(1,7)
-    const fileDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} ${dateElems[3]}:${dateElems[4]}:${dateElems[5]}`)
+    const dateString = req.params.filename.match(/(\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2})/)[1]
+    const fileDate = moment(dateString)
+    console.log(fileDate)
     const stat = fs.statSync(filePath)
 
-    const fileNewer = await PlayerList.find({ imported: { $gte: fileDate } })
-    console.log(fileNewer, fileNewer.length)
+    const fileNewer = await PlayerList.find({ imported: { $gte: fileDate.toDate() } })
     if (fileNewer.length) {
       throw Error('File has to be newer.')
     }
 
-    console.log('About to read the player list.')
+    console.log('About to read the player list.', filePath)
     const worksheets = await Excel.readWorkbook(filePath)
     const worksheet = worksheets.Players
     if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
@@ -175,10 +188,11 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
     const headers = worksheet.slice(3, 4)
 
     const dbPlayers = await Player.find()
+    console.log('bump')
 
     const allPlayers = worksheet.slice(4, worksheet.length).map(data => {
       const filePlayer = {
-        created: fileDate,
+        created: fileDate.toDate(),
         category: normalize(data[0]),
         licenseNr: normalize(data[2]),
         name: normalize(data[5]),
@@ -197,6 +211,8 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
         confirmed: !!data[29],
         paid: !!data[31],
       }
+      console.log('bump2')
+
       filePlayer.gender = filePlayer.category ? 
         filePlayer.category[0] === 'M' ? 'm' : 
         filePlayer.category[0] === 'W' ? 
@@ -220,6 +236,7 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
       } else if (filePlayer.phoneMobile && filePlayer.phoneMobile.match(reMobile)) {
         filePlayer.phone = filePlayer.phoneMobile
       }
+      console.log('bump3')
 
       const dbPlayer = dbPlayers.filter(player => player.idString == filePlayer.idString).sort((a, b) => 
         a.created > b.created ? 1 : a.created == b.created ? 0 : -1
@@ -234,6 +251,7 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
         return player._id
       }
     })
+    console.log('bump3')
 
     const playerList = new PlayerList({
       imported: fileDate,
@@ -241,9 +259,9 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
       fileSize: stat.size,
       players: allPlayers
     })
-    playerList.save()
+    await playerList.save()
       
-    return res.json({ playerList })
+    return res.json({playerList})
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
   }
@@ -295,10 +313,17 @@ swisstennis.get('/calendar/download', async (req, res) => {
     return res.json({ msg: 'No tournament given.' })
   }
   try {
-    const calendarFile = fs.createWriteStream(`swisstennis_files/Calendar-${tournament}-${fileDate(new Date())}.xls`)
+    const filename = `Calendar-${tournament}-${moment().format('YYYYMMDDTHHmmss')}.xls`
+    const calendarFile = fs.createWriteStream(`swisstennis_files/${filename}`)
     const calendar = await session.get(`https://comp.swisstennis.ch/advantage/servlet/Calendar.xls?Lang=D&tournament=Id${tournament}&Type=Match&Inp_DateRangeFilter.fromDate=04.06.2018&Inp_DateRangeFilter.toDate=16.09.2018`, {stream: true})
-    calendar.pipe(calendarFile)
-    return res.json({ msg: 'Download successful.' })
+    await calendar.pipe(calendarFile)
+    const streamDone = new Promise((resolve, reject) => {
+      calendarFile.on('finish', resolve)
+      calendarFile.on('error', reject)
+    })
+    await streamDone
+    const stats = fs.statSync(`swisstennis_files/${filename}`)
+    return res.json({ filename, size: stats.size, ctime: stats.ctime })
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
   }
@@ -309,7 +334,7 @@ swisstennis.get('/calendar/parse/:filename', async (req,res) => {
     console.log('Parsing file', req.params.filename)
     
     const filePath = `swisstennis_files/${req.params.filename}`
-    const dateElems = req.params.filename.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1,7)
+    const dateElems = req.params.filename.match(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/).slice(1,7)
     const fileDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} ${dateElems[3]}:${dateElems[4]}:${dateElems[5]}`)
     const stat = fs.statSync(filePath)
 
@@ -361,7 +386,7 @@ swisstennis.get('/calendar/parse/:filename', async (req,res) => {
       fileSize: stat.size,
       matches: allMatches
     })
-    matchList.save()
+    await matchList.save()
       
     return res.json({ matchList })
   } catch (error) {

Some files were not shown because too many files changed in this diff