Sfoglia il codice sorgente

REST server working stable, GUI needs update.

Tomi Cvetic 6 anni fa
parent
commit
6d3508d2ab
36 ha cambiato i file con 1165 aggiunte e 152 eliminazioni
  1. 23 0
      client/package-lock.json
  2. 1 0
      client/package.json
  3. 5 1
      client/src/Main.js
  4. 4 7
      client/src/alerts/index.js
  5. 1 1
      client/src/alerts/state.js
  6. 81 0
      client/src/config/components/ConfigList.js
  7. 4 0
      client/src/config/components/index.js
  8. 10 0
      client/src/config/index.js
  9. 267 0
      client/src/config/state.js
  10. 9 5
      client/src/index.js
  11. 13 0
      client/src/layout/components/Footer.js
  12. 11 8
      client/src/layout/components/Header.js
  13. 3 1
      client/src/routes.js
  14. 81 0
      client/src/swisstennis/components/ConfigList.js
  15. 4 0
      client/src/swisstennis/components/index.js
  16. 10 0
      client/src/swisstennis/index.js
  17. 267 0
      client/src/swisstennis/state.js
  18. 0 1
      client/src/users/components/UserList.js
  19. 9 2
      server/src/restServer/api.js
  20. 5 0
      server/src/restServer/config/bulksms.js
  21. 5 0
      server/src/restServer/config/swisstennis.js
  22. 9 12
      server/src/restServer/helpers.js
  23. 16 0
      server/src/restServer/middleware/apiErrorHandler.js
  24. 11 0
      server/src/restServer/models/config.js
  25. 0 13
      server/src/restServer/models/file.js
  26. 26 2
      server/src/restServer/models/match.js
  27. 1 1
      server/src/restServer/models/matchList.js
  28. 17 0
      server/src/restServer/models/player.js
  29. 1 1
      server/src/restServer/models/playerList.js
  30. 1 1
      server/src/restServer/models/user.js
  31. 78 0
      server/src/restServer/routes/config.js
  32. 139 96
      server/src/restServer/routes/swisstennis.js
  33. 53 0
      server/src/restServer/routes/sztm.js
  34. BIN
      server/swisstennis_files/Calendar-20180517182239.xls
  35. BIN
      server/swisstennis_files/DisplayDraw480035-20180517182217.xls
  36. BIN
      server/swisstennis_files/PlayerList-20180517182234.xls

+ 23 - 0
client/package-lock.json

@@ -10482,6 +10482,29 @@
         }
       }
     },
+    "react-router-dom": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz",
+      "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==",
+      "requires": {
+        "history": "^4.7.2",
+        "invariant": "^2.2.4",
+        "loose-envify": "^1.3.1",
+        "prop-types": "^15.6.1",
+        "react-router": "^4.3.1",
+        "warning": "^4.0.1"
+      },
+      "dependencies": {
+        "warning": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.1.tgz",
+          "integrity": "sha512-rAVtTNZw+cQPjvGp1ox0XC5Q2IBFyqoqh+QII4J/oguyu83Bax1apbo2eqB8bHRS+fqYUBagys6lqUoVwKSmXQ==",
+          "requires": {
+            "loose-envify": "^1.0.0"
+          }
+        }
+      }
+    },
     "react-scripts": {
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-1.1.4.tgz",

+ 1 - 0
client/package.json

@@ -27,6 +27,7 @@
     "react-dom": "^15.5.4",
     "react-redux": "^5.0.5",
     "react-router": "^4.3.1",
+    "react-router-dom": "^4.3.1",
     "redux": "^3.6.0",
     "redux-saga": "^0.15.4",
     "request": "^2.81.0",

+ 5 - 1
client/src/Main.js

@@ -1,6 +1,8 @@
 import React from 'react'
 import Header from './layout/components/Header'
+import Footer from './layout/components/Footer'
 import Routes from './routes'
+import AlertList from './alerts/components/AlertList'
 
 /** Import CSS Styles */
 import 'bootstrap/dist/css/bootstrap.min.css'
@@ -11,9 +13,11 @@ class Main extends React.Component {
   render () {
     return (
       <div className='container'>
-        {console.log('Main route', this)}
+        {console.log('Main route', this.props)}
         <Header />
         <Routes {...this.props} />
+        <AlertList {...this.props} />
+        <Footer />
       </div>
     )
   }

+ 4 - 7
client/src/alerts/index.js

@@ -6,15 +6,12 @@ const filters = {}
 const selectors = {}
 
 const middleware = store => next => action => {
-  console.log('Alert middleware', store, next, action)
   const { alert, ...newAction } = action
-  if (!alert) {
-    return next(newAction)
+  if (alert) {
+    console.log('would add new alert:', alert, newAction)
+    store.dispatch(actions.alertAdd(alert))
   }
-  const result = next(newAction)
-  console.log('would add new alert:', alert, newAction)
-  store.dispatch(actions.alertAdd(alert))
-  return result
+  return next(newAction)
 }
 
 export default { actions, components, filters, selectors, reducer, state, saga, middleware }

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

@@ -1,4 +1,4 @@
-/** @module player/state */
+/** @module alert/state */
 // import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
 
 /**

+ 81 - 0
client/src/config/components/ConfigList.js

@@ -0,0 +1,81 @@
+import React from 'react'
+
+class ConfigList extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+  }
+
+  componentDidMount () {
+    console.log('ConfigList did mount', this)
+    const { configGetRequest } = this.props.configActions
+    configGetRequest()
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+    const { configChangeForm } = this.props.configActions
+    const nextConfig = {
+      _id: this.configid ? this.configid.value : null,
+      key: this.key.value,
+      description: this.description.value,
+      value: this.value.value
+    }
+    configChangeForm(nextConfig)
+  }
+
+  render () {
+    const state = this.props.config
+    const actions = this.props.configActions
+    const config = state.configForm
+
+    return (
+      <div>
+        <form>
+          <h2>{(config._id) ? "Konfiguration editieren" : "Neue Konfiguration"}</h2>
+          {config._id ? <input type="hidden" key="configid" value={config._id} ref={(input) => {this.configid = input}} id="configid"/> : null}
+          <label htmlFor="key">Schluessel</label>
+          <input type="input" ref={(input) => {this.key = input}} id="key" value={config.key} placeholder="Schluessel" onChange={this.handleChange} readOnly={!!config._id}></input>
+          <label htmlFor="description">Beschreibung</label>
+          <input type="input" ref={(input) => {this.description = input}} id="description" value={config.description} placeholder="Beschreibung" onChange={this.handleChange}></input>
+          <label htmlFor="value">Wert</label>
+          <input type="input" ref={(input) => {this.value = input}} id="value" value={config.value} placeholder="Wert" onChange={this.handleChange}></input>
+          <input type="submit" value={config._id ? "Aenderungen speichern" : "Konfiguration anlegen"} onClick={(event) => {
+              event.preventDefault()
+              if (config._id) {
+                actions.configEditRequest(config)
+              } else {
+                actions.configAddRequest(config)
+              }
+            }} />
+          <input type="reset" value={config._id ? "Abbrechen" : "Loeschen"} onClick={(event) => {
+              event.preventDefault()
+              actions.configClearForm(event)
+            }} />
+        </form>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Schluessel</th><th>Beschreibung</th><th>Wert</th><th>Wert</th>
+            </tr>
+          </thead>
+          <tbody>
+            {state.configs ? state.configs.map((configData, key) => 
+            <tr key={key}>
+              <td>{configData.key}</td>
+              <td>{configData.description}</td>
+              <td>{configData.value}</td>
+              <td>
+                <a onClick={(event) => actions.configLoadForm(key)}>editieren</a>
+                <a onClick={(event) => actions.configDeleteRequest(key)}>loeschen</a>
+              </td>
+            </tr>
+            ) : ""}
+          </tbody>
+        </table>
+      </div>
+    )
+  }
+}
+
+export default ConfigList

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

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

+ 10 - 0
client/src/config/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 }

+ 267 - 0
client/src/config/state.js

@@ -0,0 +1,267 @@
+/** @module config/state */
+import { call, put, takeEvery, select } from 'redux-saga/effects'
+
+/**
+ * state.js
+ *
+ * Collection of everything which has to do with state changes.
+ **/
+
+/** actionTypes define what actions are handeled by the reducer. */
+export const actions = {
+  configClearForm: () => {
+    return {
+      type: 'CONFIG/CLEAR_FORM'
+    }
+  },
+  configLoadForm: (key) => {
+    return {
+      type: 'CONFIG/LOAD_FORM',
+      key
+    }
+  },
+  configChangeForm: (data) => {
+    return {
+      type: 'CONFIG/CHANGE_FORM',
+      data
+    }
+  },
+  configGetRequest: () => {
+    return {
+      type: 'CONFIG/GET_REQUEST'
+    }
+  },
+  configGetSuccess: (data) => {
+    return {
+      type: 'CONFIG/GET_SUCCESS',
+      data
+    }
+  },
+  configGetFailure: error => {
+    return {
+      type: 'CONFIG/GET_FAILURE',
+      error
+    }
+  },
+  configAddRequest: (data) => {
+    return {
+      type: 'CONFIG/ADD_REQUEST',
+      data
+    }
+  },
+  configAddSuccess: (data) => {
+    return {
+      type: 'CONFIG/ADD_SUCCESS',
+      data
+    }
+  },
+  configAddFailure: error => {
+    return {
+      type: 'CONFIG/ADD_FAILURE',
+      error
+    }
+  },
+  configEditRequest: (data) => {
+    return {
+      type: 'CONFIG/EDIT_REQUEST',
+      data
+    }
+  },
+  configEditSuccess: (data) => {
+    return {
+      type: 'CONFIG/EDIT_SUCCESS',
+      data
+    }
+  },
+  configEditFailure: error => {
+    return {
+      type: 'CONFIG/EDIT_FAILURE',
+      error
+    }
+  },
+  configDeleteRequest: (key) => {
+    return {
+      type: 'CONFIG/DELETE_REQUEST',
+      key
+    }
+  },
+  configDeleteSuccess: (data) => {
+    return {
+      type: 'CONFIG/DELETE_SUCCESS',
+      data
+    }
+  },
+  configDeleteFailure: error => {
+    return {
+      type: 'CONFIG/DELETE_FAILURE',
+      error
+    }
+  },
+}
+console.log('State actions', actions)
+
+const emptyForm = {
+  _id: null,
+  key: '',
+  description: '',
+  value: ''
+}
+
+/** state definition */
+export const state = {
+  configs: [],
+  configForm: emptyForm,
+  getRequested: false,
+  addRequested: false,
+  editRequested: false,
+  deleteRequested: false
+}
+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 'CONFIG/CLEAR_FORM':
+      return { ...state, configForm: emptyForm }
+    case 'CONFIG/LOAD_FORM':
+      return { ...state, configForm: { ...state.configs[action.key] } }
+    case 'CONFIG/CHANGE_FORM':
+      return { ...state, configForm: { ...action.data } }
+    case 'CONFIG/GET_SUCCESS':
+      return { ...state, configs: action.data.config, getRequested: false }
+    case 'CONFIG/GET_FAILURE':
+      return { ...state, getRequested: false, alert: action.error }
+    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 }
+    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 }
+    case 'CONFIG/DELETE_FAILURE':
+      return { ...state, deleteRequested: false, alert: action.error }
+    default:
+      return state
+  }
+}
+
+function * getConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Get config requested', action, token)
+    const response = yield call(fetch, 'http://localhost:3002/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.configAddFailure(error))
+    yield put(actions.configAddFailure(error))
+  }
+}
+
+function * addConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Add config requested', action, token)
+    const response = yield call(fetch, 'http://localhost:3002/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))
+  }
+}
+
+function * editConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Edit config requested', action, token)
+    const response = yield call(fetch, 'http://localhost:3002/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))
+  }
+}
+
+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, 'http://localhost:3002/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))
+  }
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {
+  console.log('Config saga started.')
+  yield takeEvery('CONFIG/GET_REQUEST', getConfig)
+  yield takeEvery('CONFIG/ADD_REQUEST', addConfig)
+  yield takeEvery('CONFIG/EDIT_REQUEST', editConfig)
+  yield takeEvery('CONFIG/DELETE_REQUEST', deleteConfig)
+}

+ 9 - 5
client/src/index.js

@@ -23,7 +23,7 @@ import layout from './layout'
 import scraper from './scraper'
 import alerts from './alerts'
 import users from './users'
-import player from './classes/player';
+import config from './config'
 
 /** 
  * Browser History
@@ -42,7 +42,8 @@ const rootReducer = combineReducers({
   layout: layout.reducer,
   scraper: scraper.reducer,
   alerts: alerts.reducer,
-  users: users.reducer
+  users: users.reducer,
+  config: config.reducer
 })
 console.log('Root reducer:', rootReducer)
 
@@ -53,7 +54,8 @@ const defaultState = {
   layout: layout.state,
   scraper: scraper.state,
   alerts: alerts.state,
-  users: users.state
+  users: users.state,
+  config: config.state
 }
 console.log('Default state:', defaultState)
 
@@ -66,7 +68,8 @@ function * rootSaga () {
     layout.saga(),
     scraper.saga(),
     alerts.saga(),
-    users.saga()
+    users.saga(),
+    config.saga()
   ])
 }
 
@@ -129,7 +132,8 @@ const actionCreators = {
   layout: layout.actions,
   scraper: scraper.actions,
   alerts: alerts.actions,
-  users: users.actions
+  users: users.actions,
+  config: config.actions
 }
 
 /** Creates a function  */

+ 13 - 0
client/src/layout/components/Footer.js

@@ -0,0 +1,13 @@
+import React from 'react'
+
+class Header extends React.Component {
+    render () {
+        return (
+    <footer>
+        <p>So, das wars</p>
+    </footer>
+        )
+    }
+}
+
+export default Header

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

@@ -1,4 +1,6 @@
 import React from 'react'
+import { Link } from 'react-router-dom'
+console.log('Link: ', Link)
 
 class Header extends React.Component {
     render () {
@@ -7,14 +9,15 @@ class Header extends React.Component {
         <h1>SZTM Helfer</h1>
         <p>Spiellisten und Zahllisten einfach gemacht</p>
         <nav>
-            <a>Benutzer</a>
-            <a>Swisstennis</a>
-            <a>Spieler</a>
-            <a>Matches</a>
-            <a>Spielliste</a>
-            <a>Zahlliste</a>
-            <a>SMS</a>
-            <a>Emails</a>
+            <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>
     </header>
         )

+ 3 - 1
client/src/routes.js

@@ -2,15 +2,17 @@ import React from 'react'
 import { Switch, Route } from 'react-router'
 import UserList from './users/components/UserList'
 import LoginForm from './users/components/LoginForm'
+import ConfigList from './config/components/ConfigList'
 
 class Routes extends React.Component {
     render () {
         return (
     <main>
         <Switch>
-            {console.log(this.props)}
+            {console.log('Route props', this.props)}
             <Route path='/users' render={() => (<UserList {...this.props}/>)} />
             <Route path='/login' render={() => (<LoginForm {...this.props}/>)} />
+            <Route path='/config' render={() => (<ConfigList {...this.props}/>)} />
         </Switch>
     </main>
         )

+ 81 - 0
client/src/swisstennis/components/ConfigList.js

@@ -0,0 +1,81 @@
+import React from 'react'
+
+class ConfigList extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+  }
+
+  componentDidMount () {
+    console.log('ConfigList did mount', this)
+    const { configGetRequest } = this.props.configActions
+    configGetRequest()
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+    const { configChangeForm } = this.props.configActions
+    const nextConfig = {
+      _id: this.configid ? this.configid.value : null,
+      key: this.key.value,
+      description: this.description.value,
+      value: this.value.value
+    }
+    configChangeForm(nextConfig)
+  }
+
+  render () {
+    const state = this.props.config
+    const actions = this.props.configActions
+    const config = state.configForm
+
+    return (
+      <div>
+        <form>
+          <h2>{(config._id) ? "Konfiguration editieren" : "Neue Konfiguration"}</h2>
+          {config._id ? <input type="hidden" key="configid" value={config._id} ref={(input) => {this.configid = input}} id="configid"/> : null}
+          <label htmlFor="key">Schluessel</label>
+          <input type="input" ref={(input) => {this.key = input}} id="key" value={config.key} placeholder="Schluessel" onChange={this.handleChange} readOnly={!!config._id}></input>
+          <label htmlFor="description">Beschreibung</label>
+          <input type="input" ref={(input) => {this.description = input}} id="description" value={config.description} placeholder="Beschreibung" onChange={this.handleChange}></input>
+          <label htmlFor="value">Wert</label>
+          <input type="input" ref={(input) => {this.value = input}} id="value" value={config.value} placeholder="Wert" onChange={this.handleChange}></input>
+          <input type="submit" value={config._id ? "Aenderungen speichern" : "Konfiguration anlegen"} onClick={(event) => {
+              event.preventDefault()
+              if (config._id) {
+                actions.configEditRequest(config)
+              } else {
+                actions.configAddRequest(config)
+              }
+            }} />
+          <input type="reset" value={config._id ? "Abbrechen" : "Loeschen"} onClick={(event) => {
+              event.preventDefault()
+              actions.configClearForm(event)
+            }} />
+        </form>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Schluessel</th><th>Beschreibung</th><th>Wert</th><th>Wert</th>
+            </tr>
+          </thead>
+          <tbody>
+            {state.configs ? state.configs.map((configData, key) => 
+            <tr key={key}>
+              <td>{configData.key}</td>
+              <td>{configData.description}</td>
+              <td>{configData.value}</td>
+              <td>
+                <a onClick={(event) => actions.configLoadForm(key)}>editieren</a>
+                <a onClick={(event) => actions.configDeleteRequest(key)}>loeschen</a>
+              </td>
+            </tr>
+            ) : ""}
+          </tbody>
+        </table>
+      </div>
+    )
+  }
+}
+
+export default ConfigList

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

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

+ 10 - 0
client/src/swisstennis/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 }

+ 267 - 0
client/src/swisstennis/state.js

@@ -0,0 +1,267 @@
+/** @module config/state */
+import { call, put, takeEvery, select } from 'redux-saga/effects'
+
+/**
+ * state.js
+ *
+ * Collection of everything which has to do with state changes.
+ **/
+
+/** actionTypes define what actions are handeled by the reducer. */
+export const actions = {
+  configClearForm: () => {
+    return {
+      type: 'CONFIG/CLEAR_FORM'
+    }
+  },
+  configLoadForm: (key) => {
+    return {
+      type: 'CONFIG/LOAD_FORM',
+      key
+    }
+  },
+  configChangeForm: (data) => {
+    return {
+      type: 'CONFIG/CHANGE_FORM',
+      data
+    }
+  },
+  configGetRequest: () => {
+    return {
+      type: 'CONFIG/GET_REQUEST'
+    }
+  },
+  configGetSuccess: (data) => {
+    return {
+      type: 'CONFIG/GET_SUCCESS',
+      data
+    }
+  },
+  configGetFailure: error => {
+    return {
+      type: 'CONFIG/GET_FAILURE',
+      error
+    }
+  },
+  configAddRequest: (data) => {
+    return {
+      type: 'CONFIG/ADD_REQUEST',
+      data
+    }
+  },
+  configAddSuccess: (data) => {
+    return {
+      type: 'CONFIG/ADD_SUCCESS',
+      data
+    }
+  },
+  configAddFailure: error => {
+    return {
+      type: 'CONFIG/ADD_FAILURE',
+      error
+    }
+  },
+  configEditRequest: (data) => {
+    return {
+      type: 'CONFIG/EDIT_REQUEST',
+      data
+    }
+  },
+  configEditSuccess: (data) => {
+    return {
+      type: 'CONFIG/EDIT_SUCCESS',
+      data
+    }
+  },
+  configEditFailure: error => {
+    return {
+      type: 'CONFIG/EDIT_FAILURE',
+      error
+    }
+  },
+  configDeleteRequest: (key) => {
+    return {
+      type: 'CONFIG/DELETE_REQUEST',
+      key
+    }
+  },
+  configDeleteSuccess: (data) => {
+    return {
+      type: 'CONFIG/DELETE_SUCCESS',
+      data
+    }
+  },
+  configDeleteFailure: error => {
+    return {
+      type: 'CONFIG/DELETE_FAILURE',
+      error
+    }
+  },
+}
+console.log('State actions', actions)
+
+const emptyForm = {
+  _id: null,
+  key: '',
+  description: '',
+  value: ''
+}
+
+/** state definition */
+export const state = {
+  configs: [],
+  configForm: emptyForm,
+  getRequested: false,
+  addRequested: false,
+  editRequested: false,
+  deleteRequested: false
+}
+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 'CONFIG/CLEAR_FORM':
+      return { ...state, configForm: emptyForm }
+    case 'CONFIG/LOAD_FORM':
+      return { ...state, configForm: { ...state.configs[action.key] } }
+    case 'CONFIG/CHANGE_FORM':
+      return { ...state, configForm: { ...action.data } }
+    case 'CONFIG/GET_SUCCESS':
+      return { ...state, configs: action.data.config, getRequested: false }
+    case 'CONFIG/GET_FAILURE':
+      return { ...state, getRequested: false, alert: action.error }
+    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 }
+    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 }
+    case 'CONFIG/DELETE_FAILURE':
+      return { ...state, deleteRequested: false, alert: action.error }
+    default:
+      return state
+  }
+}
+
+function * getConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Get config requested', action, token)
+    const response = yield call(fetch, 'http://localhost:3002/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.configAddFailure(error))
+    yield put(actions.configAddFailure(error))
+  }
+}
+
+function * addConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Add config requested', action, token)
+    const response = yield call(fetch, 'http://localhost:3002/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))
+  }
+}
+
+function * editConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Edit config requested', action, token)
+    const response = yield call(fetch, 'http://localhost:3002/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))
+  }
+}
+
+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, 'http://localhost:3002/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))
+  }
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {
+  console.log('Config saga started.')
+  yield takeEvery('CONFIG/GET_REQUEST', getConfig)
+  yield takeEvery('CONFIG/ADD_REQUEST', addConfig)
+  yield takeEvery('CONFIG/EDIT_REQUEST', editConfig)
+  yield takeEvery('CONFIG/DELETE_REQUEST', deleteConfig)
+}

+ 0 - 1
client/src/users/components/UserList.js

@@ -91,7 +91,6 @@ class UserList extends React.Component {
           <input type="submit" value={this.state.user._id ? "Aenderungen speichern" : "Benutzer anlegen"} onClick={(event) => this.saveUser(this.state.user.key, event)} />
           <input type="reset" value={this.state.user._id ? "Abbrechen" : "Loeschen"} onClick={(event) => this.clearUser(event)} />
         </form>
-        <input type="button" onClick={actions.getUserList} value="Aktualisieren" />
         <table className='table table-bordered table-striped'>
           <thead>
             <tr>

+ 9 - 2
server/src/restServer/api.js

@@ -2,14 +2,18 @@ import express from 'express'
 import cors from 'cors'
 import bodyParser from 'body-parser'
 import mongoose from 'mongoose'
-import config from './config/database'
+import configFile from './config/database'
 
-mongoose.connect(config.database)
+mongoose.connect(configFile.database)
 
 import users from './routes/users'
 import swisstennis from './routes/swisstennis'
+import sztm from './routes/sztm'
+import config from './routes/config'
 import { authenticate, verify } from './routes/authenticate'
 
+import { errorHandler } from './middleware/apiErrorHandler'
+
 const port = process.env.PORT || 3002
 const app = express()
 app.use(cors())
@@ -26,8 +30,11 @@ app.use('/authenticate', authenticate)
 const apiRoutes = express.Router()
 apiRoutes.use('/users', users)
 apiRoutes.use('/swisstennis', swisstennis)
+apiRoutes.use('/sztm', sztm)
+apiRoutes.use('/config', config)
 app.use('/api', verify)
 app.use('/api', apiRoutes)
+app.use(errorHandler)
 
 app.listen(port)
 console.log(`Server running on port ${port}.`)

+ 5 - 0
server/src/restServer/config/bulksms.js

@@ -0,0 +1,5 @@
+export default {
+    tokenName: 'SZTM',
+    tokenId: 'ACED97E29E2D492E9E8EC475AF74C67C-02-8',
+    tokenSecret: 'WXE!lZICNlvPal!_X4f01cfQxTqBy'
+}

+ 5 - 0
server/src/restServer/config/swisstennis.js

@@ -0,0 +1,5 @@
+export default {
+    tournament: '105',
+    tournamentId: '111936',
+    password: 'S3589V'
+}

+ 9 - 12
server/src/restServer/helpers.js

@@ -12,8 +12,8 @@ function datetime2s (date) {
   return moment(date).format('DD.MM. HH:mm')
 }
 
-function normalize (item, type) {
-  return item ? String(item).replace(/\s+/g, ' ').trim() : null
+function normalize (item) {
+  return item ? String(item).replace(/\s+/g, ' ').trim() : ''
 }
 
 function fileSize (int) {
@@ -31,22 +31,19 @@ function fileSize (int) {
 }
 
 function normalizePhone (item) {
-  let phone = String(item).replace(/\s|\+|\/|,|-|'/g, '').replace(/\(0\)/, '').replace(/^0+/, '')
+  const phone = item ? String(item).replace(/\s|\+|\/|,|-|'/g, '').replace(/\(0\)/, '').replace(/^0+/, '') : ''
   if (phone.match(/[^\d*]/)) {
-    return `FEHLER (nicht-numerische Zeichen): ${phone}`
-  }
-  if (phone.length === 0) {
-    return null
+    return ''
+  } else if (phone.length === 0) {
+    return ''
   } else if (phone.length === 9) {
     // Assume swiss number
-    phone = `+41${phone}`
+    return `+41${phone}`
   } else if (phone.length === 11) {
-    phone = `+${phone}`
+    return `+${phone}`
   } else {
-    return `FEHLER (falsche Länge): ${phone}`
+    return ''
   }
-
-  return phone
 }
 
 function filterFuzzy (item, filter) {

+ 16 - 0
server/src/restServer/middleware/apiErrorHandler.js

@@ -0,0 +1,16 @@
+function errorHandler(err, req, res, next) {
+    console.log(err)
+    res.status(500).json({
+        msg: err.message,
+        stack: err.stack,
+    })
+}
+
+function wrapAsync(fn) {
+    return function (req, res, next) {
+        fn(req, res, next).catch(next)
+    }
+}
+
+export { errorHandler, wrapAsync }
+export default { errorHandler, wrapAsync }

+ 11 - 0
server/src/restServer/models/config.js

@@ -0,0 +1,11 @@
+import mongoose from 'mongoose'
+
+const ConfigSchema = new mongoose.Schema({
+  key: { type: String, unique: true },
+  description: String,
+  value: String,
+})
+
+const Config = mongoose.model('Config', ConfigSchema)
+
+export default Config

+ 0 - 13
server/src/restServer/models/file.js

@@ -1,13 +0,0 @@
-import mongoose from 'mongoose'
-
-const SwissTennisFileSchema = new mongoose.Schema({
-  downloaded: Date,
-  name: String,
-  type: Date,
-  size: String,
-  path: String
-})
-
-const SwissTennisFile = mongoose.model('SwissTennisFile', SwissTennisFileSchema)
-
-export default SwissTennisFile

+ 26 - 2
server/src/restServer/models/match.js

@@ -1,16 +1,40 @@
 import mongoose from 'mongoose'
 
 const MatchSchema = new mongoose.Schema({
-  _created: Date,
+  created: Date,
   idString: String,
-  date: Date,
+  fileLine: Number,
   category: String,
+  place: String,
+  date: Date,
   player1: { type: mongoose.Schema.Types.ObjectId, ref: 'Player' },
   player2: { type: mongoose.Schema.Types.ObjectId, ref: 'Player' },
   result: String,
   doubles: Boolean
 })
 
+MatchSchema.methods.equal = function (matchData, cb) {
+  for (let property in matchData) {
+    if (this[property] instanceof Date) {
+      if (this[property].getTime() != matchData[property].getTime()) {
+        console.log('dates dont match', property, this[property], matchData[property])
+        return false
+      }
+    } else if (this[property] instanceof mongoose.Types.ObjectId) {
+        if (!this[property].equals(matchData[property])) {
+            console.log('objectids dont match', this[property], matchData[property])
+            return false
+        }
+    } else {
+      if (this[property] != matchData[property]) {
+        console.log('property doesnt match', property, this[property], typeof this[property], matchData[property], typeof matchData[property])
+        return false
+      }
+    }
+  }
+  return true
+}
+
 const Match = mongoose.model('Match', MatchSchema)
 
 export default Match

+ 1 - 1
server/src/restServer/models/matchList.js

@@ -2,7 +2,7 @@ import mongoose from 'mongoose'
 
 const MatchListSchema = new mongoose.Schema({
     imported: Date,
-    file: String,
+    file: { type: String, unique: true },
     fileSize: Number,
     matches: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Match' }]
 })

+ 17 - 0
server/src/restServer/models/player.js

@@ -29,6 +29,23 @@ const PlayerSchema = new mongoose.Schema({
   idString: String,
 })
 
+PlayerSchema.methods.equal = function (playerData, cb) {
+  for (let property in playerData) {
+    if (this[property] instanceof Date) {
+      if (this[property].getTime() != playerData[property].getTime()) {
+        console.log('dates dont match', property, this[property], playerData[property])
+        return false
+      }
+    } else {
+      if (this[property] != playerData[property]) {
+        console.log('property doesnt match', property, this[property], typeof this[property], playerData[property], typeof playerData[property])
+        return false
+      }
+    }
+  }
+  return true
+}
+
 const Player = mongoose.model('Player', PlayerSchema)
 
 export default Player

+ 1 - 1
server/src/restServer/models/playerList.js

@@ -2,7 +2,7 @@ import mongoose from 'mongoose'
 
 const PlayerListSchema = new mongoose.Schema({
     imported: Date,
-    file: String,
+    file: { type: String, unique: true },
     fileSize: Number,
     players: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Player' }]
 })

+ 1 - 1
server/src/restServer/models/user.js

@@ -3,7 +3,7 @@ import bcrypt from 'bcrypt'
 
 // Define the user schema
 const UserSchema = new mongoose.Schema({
-  username: String,
+  username: { type: String, unique: true },
   name: String,
   password: String
 })

+ 78 - 0
server/src/restServer/routes/config.js

@@ -0,0 +1,78 @@
+import express from 'express'
+import { wrapAsync } from '../middleware/apiErrorHandler'
+import Config from '../models/config'
+
+const config = express.Router()
+
+config.post('/', (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}.` })
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }
+})
+
+config.get('/', wrapAsync(async (req, res) => {
+    try {
+        const config = await Config.find()
+        res.json({ config })
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    } 
+}))
+
+config.get('/:key', wrapAsync(async (req, res) => {
+    try {
+        const { key } = req.params
+        if (!key) {
+            throw Error('parameter key is mandatory.')
+        }
+        const config = await Config.find({ key })
+        res.json({ config })
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }  
+}))
+
+config.put('/', wrapAsync(async (req, res) => {
+    try{
+        const { key, value, description } = req.body
+        if (!key || !value) {
+            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 {
+            throw Error('key not found!')
+        }        
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }
+}))
+
+config.delete('/', wrapAsync(async (req, res) => {
+    try{
+        const { key } = req.body
+        if (!key) {
+            throw Error('parameter key is mandatory.')
+        }
+        const deleted = await Config.findOneAndRemove({ key })
+        if (deleted) {
+            res.json({ msg: `Successfully deleted ${key}.` })
+        } else {
+            throw Error('key not found!') 
+        }
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }
+}))
+
+export default config

+ 139 - 96
server/src/restServer/routes/swisstennis.js

@@ -3,6 +3,7 @@ import awaitFs from 'await-fs'
 import express from 'express'
 import bhttp from 'bhttp'
 import Excel from '../excel'
+import Config from '../models/config'
 import Player from '../models/player'
 import Match from '../models/match'
 import PlayerList from '../models/playerList'
@@ -15,8 +16,20 @@ const swisstennis = express.Router()
 // Use this variable for the session
 const session = bhttp.session()
 
+// Try to fetch config data for swisstennis login
+const config = {}
+Config.find({ key: { $in: ['tournament', 'tournamentId', 'tournamentPW'] } }).exec((error, results) => {
+  if (error) {
+    console.log(error.toString())
+  } else {
+    results.forEach(result => {
+      config[result.key] = result.value
+    })
+  }
+})
+
 function fileDate (date) {
-  return `${date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate()}${date.getHours() * 10000 + date.getMinutes() * 100 + date.getSeconds()}`
+  return `${(date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate()) * 1000000 + date.getHours() * 10000 + date.getMinutes() * 100 + date.getSeconds()}`
 }
 
 /*
@@ -25,27 +38,27 @@ function fileDate (date) {
 
 // Login
 swisstennis.post('/login', async (req, res) => {
-  const { username, password } = req.body
-  
-  // return, if username or password are missing
-  if (!username || !password) {
-    return res.status(400).json({ msg: 'Parameters username and password are required' })
-  }
-  
-  // assemble the login data
-  const loginData = {
-    Lang: 'D',
-    id: username,
-    pwd: password,
-    Tournament: ''
-  }
-  
   try {
+    const username = req.body.username || config.tournament
+    const password = req.body.pasword || config.tournamentPW
+    
+    // return, if username or password are missing
+    if (!username || !password) {
+      throw Error('Parameters username and password are required')
+    }
+    
+    // assemble the login data
+    const loginData = {
+      Lang: 'D',
+      id: username,
+      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')) {
-      return res.status(400).json({ msg: 'Access denied!' })
+      throw Error('Access denied!')
     }
   } catch (error) {
     return res.status(400).json({ msg: error })
@@ -55,13 +68,13 @@ swisstennis.post('/login', async (req, res) => {
 
 // Overview of tournaments
 swisstennis.get('/tournaments', async (req, res) => {
-  const tournaments = {}
   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')) {
-      return res.status(400).json({ message: 'Not logged in.' })
+      throw Error('Not logged in.')
     }
     const tournamentRegexp = /<a href=".*ProtectedDisplayTournament.*tournament=Id(\d+)">([^<]+)<\/a>/gm
     
@@ -71,26 +84,26 @@ swisstennis.get('/tournaments', async (req, res) => {
         tournaments[match[1]] = match[2]
       }
     } while (match)
+    res.json({ tournaments })
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
   }
-  res.json({ tournaments })
 })
 
 // Draws of a tournament
-swisstennis.get('/draws/:tournament', async (req, res) => {
-  const { tournament } = req.params
-  
-  if (!tournament) {
-    return res.json({ msg: 'No tournament given.' })
-  }
+swisstennis.get('/draws', async (req, res) => {
   try {
+    const tournament = req.query.tournament || config.tournamentId
+    
+    if (!tournament) {
+      throw Error('No tournament given.')
+    }
     let match
     const draws = {}
     const tournamentPage = await session.get(`https://comp.swisstennis.ch/advantage/servlet/ProtectedDisplayTournament?Lang=D&tournament=Id${tournament}`)
     const strTournamentPage = tournamentPage.body.toString()
     if (strTournamentPage.includes('Login-Zone')) {
-      return res.status(400).json({ message: 'Not logged in.' })
+      throw Error('Not logged in.')
     }
     const drawRegexp = /<a (?:class="text" )?href=".*DisplayEvent.*eventId=(\d+).*">([^<]+)<\/a>/gm
     
@@ -107,14 +120,14 @@ swisstennis.get('/draws/:tournament', async (req, res) => {
 })
 
 // Download a playerlist
-swisstennis.get('/playerlist/download/:tournament', async (req, res) => {
-  const { tournament } = req.params
-  
-  if (!tournament) {
-    return res.json({ msg: 'No tournament given.' })
-  }
+swisstennis.get('/playerlist/download', async (req, res) => {
   try {
-    const playerListFile = fs.createWriteStream(`swisstennis_files/PlayerList-${fileDate(new Date())}.xls`)
+    const tournament = req.query.tournament || config.tournamentId
+    
+    if (!tournament) {
+      throw Error('No tournament given.')
+    }
+    const playerListFile = fs.createWriteStream(`swisstennis_files/PlayerList-${tournament}-${fileDate(new Date())}.xls`)
     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.' })
@@ -128,10 +141,16 @@ 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{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.xls/).slice(1,7)
+    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 stat = fs.statSync(filePath)
 
+    const fileNewer = await PlayerList.find({ imported: { $gte: fileDate } })
+    console.log(fileNewer, fileNewer.length)
+    if (fileNewer.length) {
+      throw Error('File has to be newer.')
+    }
+
     console.log('About to read the player list.')
     const worksheets = await Excel.readWorkbook(filePath)
     const worksheet = worksheets.Players
@@ -141,29 +160,28 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
 
     const headers = worksheet.slice(3, 4)
 
-    const dbPlayers = await Player.find().lean()
-    console.log(dbPlayers.slice(0,3))
+    const dbPlayers = await Player.find()
 
     const allPlayers = worksheet.slice(4, worksheet.length).map(data => {
       const filePlayer = {
-        created: new Date(),
+        created: fileDate,
         category: normalize(data[0]),
         licenseNr: normalize(data[2]),
         name: normalize(data[5]),
         firstName: normalize(data[6]), 
         nameDP: normalize(data[24]),
         firstNameDP: normalize(data[25]),
-        birthDate: data[7],
+        birthDate: data[7] || null,
         email: normalize(data[16]),
         ranking: normalize(data[17]),
         licenseNrDP: normalize(data[23]),
         phonePrivate: normalizePhone(data[13]),
         phoneWork: normalizePhone(data[14]),
         phoneMobile: normalizePhone(data[15]),
-        birthDateDP: data[26],
+        birthDateDP: data[26] || null,
         rankingDP: normalize(data[27]),
-        confirmed: data[29],
-        paid: data[31],
+        confirmed: !!data[29],
+        paid: !!data[31],
       }
       filePlayer.gender = filePlayer.category ? 
         filePlayer.category[0] === 'M' ? 'm' : 
@@ -192,19 +210,19 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
       const dbPlayer = dbPlayers.filter(player => player.idString == filePlayer.idString).sort((a, b) => 
         a.created > b.created ? 1 : a.created == b.created ? 0 : -1
       )[0]
-      console.log('sorted:', dbPlayer)
+      //console.log('sorted:', dbPlayer)
 
-      if (filePlayer != dbPlayer) {
+      if (dbPlayer && dbPlayer.equal(filePlayer)) {
+        return dbPlayer._id
+      } else {
         const player = new Player(filePlayer)
         player.save()
         return player._id
-      } else {
-        return dbPlayer._id
       }
     })
 
     const playerList = new PlayerList({
-      imported: new Date(),
+      imported: fileDate,
       file: req.params.filename,
       fileSize: stat.size,
       players: allPlayers
@@ -218,36 +236,39 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
 })
 
 // List downloaded files
-swisstennis.get('/files/list/:tournament', async (req, res) => {
-  const { tournament } = req.params
-  
-  if (!tournament) {
-    return res.json({ msg: 'No tournament given.' })
-  }
+swisstennis.get('/files', async (req, res) => {
+  throw Error('Diini mueter!')
+  const tournament = req.query.tournament
+  const dirContent = await awaitFs.readdir('swisstennis_files')
+  const fileList = dirContent.filter(filename => {
+    return tournament ? filename.includes(tournament) : true
+  }).map(filename => {
+    const stats = fs.statSync(`swisstennis_files/${filename}`)
+    return { filename, size: stats.size, ctime: stats.ctime }
+  })
+  return res.json({ fileList })
+})
+
+swisstennis.delete('/files', async (req, res) => {
   try {
-    const dirContent = await awaitFs.readdir('swisstennis_files')
-    const fileList = dirContent.filter(filename => {
-      console.log(filename)
-      return filename.includes(tournament) || true
-    }).map(filename => {
-      console.log(filename)
-      const stats = fs.statSync(`swisstennis_files/${filename}`)
-      return { [filename]: stats.size }
+    const { filename } = req.body
+    fs.unlink(`swisstennis_files/${filename}`, (error) => {
+      if (error) throw error
+      res.json({ msg: `successfully deleted swisstennis_files/${filename}.` })
     })
-    return res.json({ fileList })
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
   }
 })
 
-swisstennis.get('/calendar/download/:tournament', async (req, res) => {
-  const { tournament } = req.params
+swisstennis.get('/calendar/download', async (req, res) => {
+  const tournament = req.query.tournament || config.tournamentId
   
   if (!tournament) {
     return res.json({ msg: 'No tournament given.' })
   }
   try {
-    const calendarFile = fs.createWriteStream(`swisstennis_files/Calendar-${fileDate(new Date())}.xls`)
+    const calendarFile = fs.createWriteStream(`swisstennis_files/Calendar-${tournament}-${fileDate(new Date())}.xls`)
     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.' })
@@ -259,45 +280,75 @@ swisstennis.get('/calendar/download/:tournament', async (req, res) => {
 swisstennis.get('/calendar/parse/:filename', async (req,res) => {
   try {
     console.log('Parsing file', req.params.filename)
-    console.log('About to read the calendar list.')
     
+    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 fileDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} ${dateElems[3]}:${dateElems[4]}:${dateElems[5]}`)
+    const stat = fs.statSync(filePath)
+
+    const fileNewer = await MatchList.find({ imported: { $gte: fileDate } })
+    if (fileNewer.length) {
+      throw Error('File has to be newer.')
+    }
+    
+    console.log('About to read the calendar list.')    
     const worksheets = await Excel.readWorkbook(`swisstennis_files/${req.params.filename}`)
     const worksheet = worksheets.Sheet1
     if (worksheet[2].length < 8 | worksheet[2].length > 9) {
       throw Error(`Wrong file structure.`)
     }
-    const calendar = await Promise.all(worksheet.slice(2, worksheet.length).map(async data => {
-      const category = normalize(data[3])
-      const player1 = await Player.findOne({idString: `${category} % ${normalize(data[4])}`})
-      const player2 = await Player.findOne({idString: `${category} % ${normalize(data[6])}`})
-      const match = new Match({
-        _created: Date.now(),
-        idString: "String",
+
+    const dbMatches = await Match.find()
+
+    const allMatches = await Promise.all(worksheet.slice(2, worksheet.length).map(async (data, key) => {
+      const fileMatch = {
+        created: fileDate,
+        fileLine: key,
+        category: normalize(data[3]),
         place: normalize(data[0]),
         date: data[1],
-        category,
-        player1: player1 ? player1._id : null,
-        player2: player2 ? player2._id : null,
-        result: normalize(data[8] || null),
-        doubles: !!category.match(/DM.*|[MW]D.*/)
-      })
+        result: normalize(data[8]),
+      }
+      fileMatch.doubles = !!fileMatch.category.match(/DM.*|[MW]D.*/)
+      const player1 = await Player.findOne({idString: `${fileMatch.category} % ${normalize(data[4])}`})
+      const player2 = await Player.findOne({idString: `${fileMatch.category} % ${normalize(data[6])}`})
+      fileMatch.idString = `${fileMatch.category} % ${player1 ? player1.idString : `${key}_1`} - ${player2 ? player2.idString : `${key}_2`}`
+      fileMatch.player1 = player1 ? player1._id : null
+      fileMatch.player2 = player2 ? player2._id : null
 
-      match.save()
-      return match
+      const dbMatch = dbMatches.filter(match => match.idString == fileMatch.idString).sort((a, b) => 
+        a.created > b.created ? 1 : a.created == b.created ? 0 : -1
+      )[0]
+
+      if (dbMatch && dbMatch.equal(fileMatch)) {
+        return dbMatch._id
+      } else {
+        const match = new Match(fileMatch)
+        match.save()
+        return match._id
+      }
     }))
-    res.json(calendar)
+    const matchList = new MatchList({
+      imported: fileDate,
+      file: req.params.filename,
+      fileSize: stat.size,
+      matches: allMatches
+    })
+    matchList.save()
+      
+    return res.json({ matchList })
   } catch (error) {
     res.status(400).json({ msg: error.toString() })
   }
 })
 
-swisstennis.get('/download/draw/:draw', async (req, res) => {
-  const { draw } = req.params
-  
-  if (!draw) {
-    return res.json({ msg: 'No draw given.' })
-  }
+swisstennis.get('/download/draw', async (req, res) => {
   try {
+    const { draw } = req.query
+    
+    if (!draw) {
+      throw Error('No draw given.')
+    }
     const fileName = `DisplayDraw${draw}-${fileDate(new Date())}.xls`
     const drawFile = fs.createWriteStream(`swisstennis_files/${fileName}`)
     const drawDisplay = await session.get(`https://comp.swisstennis.ch/advantage/servlet/DisplayDraw.xls?eventId=${draw}&lang=D`, {stream: true})
@@ -308,12 +359,4 @@ swisstennis.get('/download/draw/:draw', async (req, res) => {
   }
 })
 
-swisstennis.get('/schedule/generate/:id', async (req, res) => {
-  return res.status(400).json({ msg: 'Not implemented' })
-})
-
-swisstennis.get('/paylist/generate/:id', async (req, res) => {
-  return res.status(400).json({ msg: 'Not implemented' })
-})
-
 export default swisstennis

+ 53 - 0
server/src/restServer/routes/sztm.js

@@ -0,0 +1,53 @@
+import fs from 'fs'
+import awaitFs from 'await-fs'
+import { bulksmsConfig } from '../config/bulksms'
+import express from 'express'
+import Player from '../models/player'
+import Match from '../models/match'
+import MatchList from '../models/matchList'
+
+const sztm = express.Router()
+
+sztm.get('/schedule', async (req, res) => {
+    try{
+        const { matchListId, startDate, endDate, place, category } = req.query
+        
+        const matchList = matchListId ? 
+        await MatchList.findOne({ _id: matchListId }) : 
+        await MatchList.findOne().sort({ imported: -1 })
+        
+        const query = { _id: { $in: matchList.matches } }
+        if (place) query.place = place
+        if (category) query.category = category
+        if (startDate || endDate) { query.date = {} }
+        if (startDate) {
+            const startElems = startDate.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1, 7)
+            const startDateParsed = new Date(`${startElems[0]}-${startElems[1]}-${startElems[2]} ${startElems[3]}:${startElems[4]}:${startElems[5]}`)
+            query.date.$gte = startDateParsed
+        }
+        if (endDate) {
+            const endElems = endDate.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1, 7)
+            const endDateParsed = new Date(`${endElems[0]}-${endElems[1]}-${endElems[2]} ${endElems[3]}:${endElems[4]}:${endElems[5]}`)
+            query.date.$lte = endDateParsed
+        }
+        
+        const matches = await Match.find(query).sort({ date: 1 })
+        const playerList = []
+        matches.forEach(match => {
+            if (match.player1) playerList.push(match.player1)
+            if (match.player2) playerList.push(match.player2)
+        })
+        const players = await Player.find({ _id: { $in: playerList.players } })
+        res.json({ matches, players })
+        
+        //const filteredMatches = matchList.matches.sort().filter()
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }
+})
+
+sztm.post('/sms', async (req, res) => {
+    
+})
+
+export default sztm

BIN
server/swisstennis_files/Calendar-20180517182239.xls


BIN
server/swisstennis_files/DisplayDraw480035-20180517182217.xls


BIN
server/swisstennis_files/PlayerList-20180517182234.xls