Просмотр исходного кода

added support for draw download and parsing. not included in match-creation though.

Tomi Cvetic 6 лет назад
Родитель
Сommit
27b46f8e15
28 измененных файлов с 894 добавлено и 596 удалено
  1. 58 20
      client/package-lock.json
  2. 3 2
      client/package.json
  3. 2 2
      client/src/config/components/ConfigList.js
  4. 20 16
      client/src/config/state.js
  5. 14 0
      client/src/index.js
  6. 5 5
      client/src/swisstennis/components/Swisstennis.js
  7. 4 2
      client/src/sztm/components/SZTM.js
  8. 2 2
      client/src/users/components/UserList.js
  9. 43 0
      server/package-lock.json
  10. 1 0
      server/package.json
  11. 32 3
      server/src/restServer/api.js
  12. 2 9
      server/src/restServer/middleware/apiErrorHandler.js
  13. 46 60
      server/src/restServer/routes/config.js
  14. 0 439
      server/src/restServer/routes/swisstennis.js
  15. 4 7
      server/src/restServer/routes/sztm.js
  16. 90 0
      server/src/restServer/swisstennis/index.js
  17. 10 0
      server/src/restServer/swisstennis/models/draw.js
  18. 0 0
      server/src/restServer/swisstennis/models/match.js
  19. 0 0
      server/src/restServer/swisstennis/models/matchList.js
  20. 0 0
      server/src/restServer/swisstennis/models/player.js
  21. 0 0
      server/src/restServer/swisstennis/models/playerList.js
  22. 133 0
      server/src/restServer/swisstennis/routes/calendar.js
  23. 176 0
      server/src/restServer/swisstennis/routes/draw.js
  24. 0 0
      server/src/restServer/swisstennis/routes/excel.js
  25. 30 0
      server/src/restServer/swisstennis/routes/files.js
  26. 1 29
      server/src/restServer/swisstennis/routes/helpers.js
  27. 159 0
      server/src/restServer/swisstennis/routes/playerList.js
  28. 59 0
      server/src/restServer/swisstennis/routes/tournament.js

+ 58 - 20
client/package-lock.json

@@ -3920,7 +3920,6 @@
       "version": "0.1.6",
       "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-0.1.6.tgz",
       "integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=",
-      "dev": true,
       "requires": {
         "original": ">=0.0.5"
       }
@@ -4321,7 +4320,6 @@
       "version": "0.11.1",
       "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz",
       "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=",
-      "dev": true,
       "requires": {
         "websocket-driver": ">=0.5.1"
       }
@@ -5665,8 +5663,7 @@
     "http-parser-js": {
       "version": "0.4.13",
       "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz",
-      "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc=",
-      "dev": true
+      "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc="
     },
     "http-proxy": {
       "version": "1.17.0",
@@ -7264,8 +7261,7 @@
     "json3": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
-      "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=",
-      "dev": true
+      "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE="
     },
     "json5": {
       "version": "0.5.1",
@@ -8381,7 +8377,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/original/-/original-1.0.1.tgz",
       "integrity": "sha512-IEvtB5vM5ULvwnqMxWBLxkS13JIEXbakizMSo3yoPNPCIWzg8TG3Usn/UhXoZFM/m+FuEA20KdzPSFq/0rS+UA==",
-      "dev": true,
       "requires": {
         "url-parse": "~1.4.0"
       }
@@ -10013,8 +10008,7 @@
     "querystringify": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz",
-      "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw==",
-      "dev": true
+      "integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw=="
     },
     "raf": {
       "version": "3.4.0",
@@ -10153,6 +10147,31 @@
         "sockjs-client": "1.1.4",
         "strip-ansi": "3.0.1",
         "text-table": "0.2.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "sockjs-client": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz",
+          "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=",
+          "dev": true,
+          "requires": {
+            "debug": "^2.6.6",
+            "eventsource": "0.1.6",
+            "faye-websocket": "~0.11.0",
+            "inherits": "^2.0.1",
+            "json3": "^3.3.2",
+            "url-parse": "^1.1.8"
+          }
+        }
       }
     },
     "react-dom": {
@@ -10889,8 +10908,7 @@
     "requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
-      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
-      "dev": true
+      "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
     },
     "resolve": {
       "version": "1.6.0",
@@ -11468,10 +11486,9 @@
       }
     },
     "sockjs-client": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz",
-      "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=",
-      "dev": true,
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz",
+      "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=",
       "requires": {
         "debug": "^2.6.6",
         "eventsource": "0.1.6",
@@ -11485,7 +11502,6 @@
           "version": "2.6.9",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
           "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
-          "dev": true,
           "requires": {
             "ms": "2.0.0"
           }
@@ -12571,7 +12587,6 @@
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.1.tgz",
       "integrity": "sha512-x95Td74QcvICAA0+qERaVkRpTGKyBHHYdwL2LXZm5t/gBtCB9KQSO/0zQgSTYEV1p0WcvSg79TLNPSvd5IDJMQ==",
-      "dev": true,
       "requires": {
         "querystringify": "^2.0.0",
         "requires-port": "^1.0.0"
@@ -13055,6 +13070,31 @@
           "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
           "dev": true
         },
+        "sockjs-client": {
+          "version": "1.1.4",
+          "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.4.tgz",
+          "integrity": "sha1-W6vjhrd15M8U51IJEUUmVAFsixI=",
+          "dev": true,
+          "requires": {
+            "debug": "^2.6.6",
+            "eventsource": "0.1.6",
+            "faye-websocket": "~0.11.0",
+            "inherits": "^2.0.1",
+            "json3": "^3.3.2",
+            "url-parse": "^1.1.8"
+          },
+          "dependencies": {
+            "debug": {
+              "version": "2.6.9",
+              "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+              "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+              "dev": true,
+              "requires": {
+                "ms": "2.0.0"
+              }
+            }
+          }
+        },
         "string-width": {
           "version": "1.0.2",
           "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -13163,7 +13203,6 @@
       "version": "0.7.0",
       "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
       "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
-      "dev": true,
       "requires": {
         "http-parser-js": ">=0.4.0",
         "websocket-extensions": ">=0.1.1"
@@ -13172,8 +13211,7 @@
     "websocket-extensions": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
-      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==",
-      "dev": true
+      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg=="
     },
     "whatwg-encoding": {
       "version": "1.0.3",

+ 3 - 2
client/package.json

@@ -2,7 +2,7 @@
   "name": "sztm-excel",
   "version": "0.1.0",
   "private": true,
-  "proxy": "http://localhost:3002/",
+  "proxy": "http://restserver:3002",
   "dependencies": {
     "babel-core": "^6.25.0",
     "babel-polyfill": "^6.23.0",
@@ -27,7 +27,8 @@
     "redux": "^3.6.0",
     "redux-saga": "^0.15.4",
     "request": "^2.81.0",
-    "request-promise": "^4.2.1"
+    "request-promise": "^4.2.1",
+    "sockjs-client": "^1.1.5"
   },
   "devDependencies": {
     "eslint": "^5.0.1",

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

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

+ 20 - 16
client/src/config/state.js

@@ -27,9 +27,10 @@ export const actions = {
       data
     }
   },
-  configGetRequest: () => {
+  configGetRequest: (data) => {
     return {
-      type: 'CONFIG/GET_REQUEST'
+      type: 'CONFIG/GET_REQUEST',
+      data
     }
   },
   configGetSuccess: (data) => {
@@ -163,39 +164,42 @@ export function reducer (state = [], action) {
   }
 }
 
-function * getConfig (action) {
-    yield put(api.actions.apiRequest({
-      path: 'api/config',
-      onSuccess: actions.configGetSuccess,
-      onFailure: actions.configGetFailure
-    }))
-}
-
 function * addConfig (action) {
+  const { key, ...data } = action.data || { key: '' }
   yield put(api.actions.apiRequest({
-    path: 'api/config',
+    path: `api/config/${key}`,
     method: 'POST',
-    body: JSON.stringify(action.data),
+    body: JSON.stringify(data),
     onSuccess: actions.configAddSuccess,
     onFailure: actions.configAddFailure
   }))
 }
 
+function * getConfig (action) {
+  const { key } = action.data || { key: '' }
+    yield put(api.actions.apiRequest({
+      path: `api/config/${key}`,
+      onSuccess: actions.configGetSuccess,
+      onFailure: actions.configGetFailure
+    }))
+}
+
 function * editConfig (action) {
+  const { key, ...data } = action.data || { key: '' }
   yield put(api.actions.apiRequest({
-    path: 'api/config',
+    path: `api/config/${key}`,
     method: 'PUT',
-    body: JSON.stringify(action.data),
+    body: JSON.stringify(data),
     onSuccess: actions.configEditSuccess,
     onFailure: actions.configEditFailure
   }))
 }
 
 function * deleteConfig (action) {
+  const { key } = action.data || { key: '' }
   yield put(api.actions.apiRequest({
-    path: 'api/config',
+    path: `api/config/${key}`,
     method: 'DELETE',
-    body: JSON.stringify(action.data),
     onSuccess: actions.configDeleteSuccess,
     onFailure: actions.configDeleteFailure
   }))

+ 14 - 0
client/src/index.js

@@ -7,6 +7,8 @@ import { createStore, applyMiddleware, combineReducers, bindActionCreators, comp
 import { Provider, connect } from 'react-redux'
 import createSagaMiddleware from 'redux-saga'
 import { all } from 'redux-saga/effects'
+import SockJS from 'sockjs-client'
+import { SZTM_API } from './local-constants'
 
 // React router
 import { createBrowserHistory } from 'history'
@@ -25,6 +27,18 @@ import sms from './sms'
 import swisstennis from './swisstennis'
 import sztm from './sztm'
 
+const socketClient = new SockJS(`${SZTM_API}/socket`)
+socketClient.onopen = () => {
+  console.log('Connection open', socketClient.protocol)
+}
+socketClient.onmessage = (message) => {
+  console.log('Connection message', message.data)
+}
+socketClient.onclose = () => {
+  console.log('Connection closed')
+}
+console.log(socketClient)
+
 /** 
  * Browser History
  */

+ 5 - 5
client/src/swisstennis/components/Swisstennis.js

@@ -83,7 +83,7 @@ class Swisstennis extends React.Component {
               <td>{moment(file.mtime).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 onClick={(event) => {
+                <a href="#" onClick={(event) => {
                   event.preventDefault()
                   if (file.filename.includes('Calendar')) actions.parseCalendarRequest({filename: file.filename})
                   if (file.filename.includes('PlayerList')) actions.parsePlayerListRequest({filename: file.filename})
@@ -106,10 +106,10 @@ class Swisstennis extends React.Component {
               <td>{calendar.file}</td>
               <td>{moment(calendar.imported).format('DD.MM.YYYY HH:mm')}</td>
               <td>{state.calendar && (state.calendar._id === calendar._id) ? 'Ja' : 'Nein'}</td>
-              <td><a onClick={(event) => {
+              <td><a href="#" onClick={(event) => {
                 event.preventDefault()
                 actions.getCalendarRequest({calendarId: calendar._id})
-              }}>verwenden</a> <a onClick={(event) => {
+              }}>verwenden</a> <a href="#" onClick={(event) => {
                 event.preventDefault()
                 actions.deleteCalendarRequest({calendarId: calendar._id})
               }}>loeschen</a></td>
@@ -131,10 +131,10 @@ class Swisstennis extends React.Component {
               <td>{playerList.file}</td>
               <td>{moment(playerList.imported).format('DD.MM.YYYY HH:mm')}</td>
               <td>{state.playerList && (state.playerList._id === playerList._id) ? 'Ja' : 'Nein'}</td>
-              <td><a onClick={(event) => {
+              <td><a href="#" onClick={(event) => {
                 event.preventDefault()
                 actions.getPlayerListRequest({playerListId: playerList._id})
-              }}>verwenden</a> <a onClick={(event) => {
+              }}>verwenden</a> <a href="#" onClick={(event) => {
                 event.preventDefault()
                 actions.deletePlayerListRequest({playerlistId: playerList._id})
               }}>loeschen</a></td>

+ 4 - 2
client/src/sztm/components/SZTM.js

@@ -48,13 +48,15 @@ class SZTM extends React.Component {
     calendar && calendar.matches.forEach(match => {
       const date = moment(match.date).startOf('day').unix()
       if (!dates.includes(date)) dates.push(date)
+    })
+    calendar && calendar.matches.filter(match => filter.date ? moment(match.date).startOf('day').unix() === filter.date : true).forEach(match => {
       if (!places.includes(match.place)) places.push(match.place)
     })
     
     return (
       <div>
         <h2>Filter</h2>
-        {dates.map((date, key) => <a key={key} onClick={(event) => this.setDate(date, event)} style={filter.date === date ? {fontWeight: 'bold'} : {}}>{moment.unix(date).format('DD.MM.')}</a>)}
+        {dates.map((date, key) => <a href="#" key={key} onClick={(event) => this.setDate(date, event)} style={filter.date === date ? {fontWeight: 'bold'} : {}}>{moment.unix(date).format('DD.MM.')}</a>)}
         <p>Calendar: {calendar && <span>{calendar.file} ({moment(calendar.imported).format('DD.MM. HH:mm')})</span>}</p>
         <p>
           <button onClick={(event) => this.generateLists(places, filter.date, calendar._id, event)} disabled={!filter.date}>Listen generieren</button>
@@ -78,7 +80,7 @@ class SZTM extends React.Component {
               <td>{file.size/1024}kB</td>
               <td>{moment(file.mtime).format('DD.MM.YYYY HH:mm')}</td>
               <td>
-                <a onClick={(event) => {
+                <a href="#" onClick={(event) => {
                   event.preventDefault()
                   if (file.filename.includes('Calendar')) actions.parseCalendarRequest({filename: file.filename})
                   if (file.filename.includes('PlayerList')) actions.parsePlayerListRequest({filename: file.filename})

+ 2 - 2
client/src/users/components/UserList.js

@@ -96,8 +96,8 @@ class UserList extends React.Component {
               <td>{user.name}</td>
               <td>{user.username}</td>
               <td>
-                <a onClick={(event) => this.loadUser(key, event)}>editieren</a>
-                <a onClick={(event) => this.deleteUser(key, event)}>loeschen</a>
+                <a href="#" onClick={(event) => this.loadUser(key, event)}>editieren</a>
+                <a href="#" onClick={(event) => this.deleteUser(key, event)}>loeschen</a>
               </td>
             </tr>
             ) : ""}

+ 43 - 0
server/package-lock.json

@@ -2547,6 +2547,14 @@
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
       "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
     },
+    "faye-websocket": {
+      "version": "0.10.0",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz",
+      "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=",
+      "requires": {
+        "websocket-driver": ">=0.5.1"
+      }
+    },
     "file-saver": {
       "version": "1.3.8",
       "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz",
@@ -3593,6 +3601,11 @@
         "statuses": ">= 1.4.0 < 2"
       }
     },
+    "http-parser-js": {
+      "version": "0.4.13",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.13.tgz",
+      "integrity": "sha1-O9bW/ebjFyyTNMOzO2wZPYD+ETc="
+    },
     "http-signature": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -5281,6 +5294,22 @@
         }
       }
     },
+    "sockjs": {
+      "version": "0.3.19",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz",
+      "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==",
+      "requires": {
+        "faye-websocket": "^0.10.0",
+        "uuid": "^3.0.1"
+      },
+      "dependencies": {
+        "uuid": {
+          "version": "3.3.2",
+          "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+          "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+        }
+      }
+    },
     "source-map": {
       "version": "0.5.7",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -6080,6 +6109,20 @@
       "resolved": "https://registry.npmjs.org/voc/-/voc-1.0.0.tgz",
       "integrity": "sha512-mQwxWlK+zosxxDTqiFb9ZQBNgd794scgkhVwca7h9sEhvA52f3VzbOK+TOWeS8eSrFXnfuKrxElSPc5oLAetfw=="
     },
+    "websocket-driver": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
+      "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=",
+      "requires": {
+        "http-parser-js": ">=0.4.0",
+        "websocket-extensions": ">=0.1.1"
+      }
+    },
+    "websocket-extensions": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz",
+      "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg=="
+    },
     "which": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",

+ 1 - 0
server/package.json

@@ -28,6 +28,7 @@
     "pdfmake": "^0.1.37",
     "request": "^2.81.0",
     "request-promise": "^4.2.1",
+    "sockjs": "^0.3.19",
     "xlsx": "^0.10.4"
   },
   "devDependencies": {

+ 32 - 3
server/src/restServer/api.js

@@ -2,23 +2,46 @@ import express from 'express'
 import cors from 'cors'
 import bodyParser from 'body-parser'
 import mongoose from 'mongoose'
+import sockjs from 'sockjs'
 import configFile from './config/database'
 
 import users from './routes/users'
-import swisstennis from './routes/swisstennis'
+import swisstennis from './swisstennis'
 import sztm from './routes/sztm'
 import config from './routes/config'
+import { listConfigs } from './routes/config'
 import download from './routes/download'
 import sms from './routes/sms'
 
 import { authenticate, verify } from './routes/authenticate'
-
-import { errorHandler } from './middleware/apiErrorHandler'
+import errorHandler from './middleware/apiErrorHandler'
 
 mongoose.connect(configFile.database)
 
+export const configDb = {}
+listConfigs().then(results => {
+  results.forEach(result => {
+    configDb[result.key] = { value: result.value, description: result.description }
+  })
+}, error => {
+  console.log(error)
+})
+
+const socketServer = sockjs.createServer({disable_cors: true})
+socketServer.on('connection', connection => {
+  console.log('Connection established', connection.id)
+  connection.on('close', () => {
+    console.log('Connection closed', connection.id)
+  })
+  connection.on('data', (message) => {
+    connection.write('Connection message', connection.id, message)
+  })
+})
+
 const port = process.env.PORT || 3002
 const app = express()
+
+socketServer.installHandlers(app, { prefix: '/socket' })
 app.use(cors())
 
 app.use(bodyParser.urlencoded({ extended: false }))
@@ -28,17 +51,23 @@ app.get('/', (req, res) => {
   res.send(`Express API`)
 })
 
+// Paths outside the protected zone
 app.use('/authenticate', authenticate)
 app.use('/download', download)
 
+// Paths inside the protected zone
 const apiRoutes = express.Router()
 apiRoutes.use('/users', users)
 apiRoutes.use('/swisstennis', swisstennis)
 apiRoutes.use('/sztm', sztm)
 apiRoutes.use('/config', config)
 apiRoutes.use('/sms', sms)
+
+// Use verify to protect zone
 app.use('/api', verify)
 app.use('/api', apiRoutes)
+
+// Catch errors in the error handler
 app.use(errorHandler)
 
 app.listen(port)

+ 2 - 9
server/src/restServer/middleware/apiErrorHandler.js

@@ -1,16 +1,9 @@
 function errorHandler(err, req, res, next) {
-    console.log(err)
+    console.log(err, err.statusCode)
     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 }
+export default errorHandler

+ 46 - 60
server/src/restServer/routes/config.js

@@ -1,77 +1,63 @@
 import express from 'express'
-import { wrapAsync } from '../middleware/apiErrorHandler'
 import Config from '../models/config'
 
 const config = express.Router()
 
-config.post('/', wrapAsync(async (req, res) => {
-    try{
-        const { key, value, description } = req.body
-        if (!key || !value) {
-            throw Error('parameters key and value are mandatory.')
-        }
+config.post('/:key', async (req, res, next) => {
+    const { key } = req.params
+    const { value, description } = req.body
+    if (!value) {
+        const error = Error('Parameter value is mandatory.')
+        error.statusCode = 400
+        next(error)
+    }
+    try {
         const configPair = new Config({ key, value, description })
         await configPair.save()
         res.json(configPair)
     } catch (error) {
-        res.status(400).json({ msg: error.toString() })
+        error.statusCode = 400
+        next(error)
     }
-}))
+})
 
-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?', async (req, res) => {
+    const { key } = req.params
+    const config = key ? await Config.findOne({ key }) : await Config.find().select({ key: 1, value: 1, description: 1 })
+    res.json({ config })
+})
 
-config.get('/:key', wrapAsync(async (req, res) => {
-    try {
-        const { key } = req.params
-        if (!key) {
-            throw Error('parameter key is mandatory.')
-        }
-        const config = await Config.findOne({ 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) {
-            throw Error('key not found!')
-        }  
-        const config = await Config.findOne({ key })   
-        res.json(config)   
-    } catch (error) {
-        res.status(400).json({ msg: error.toString() })
+config.put('/:key', async (req, res) => {
+    const { key } = req.params
+    const { value, description } = req.body
+    if (!value) {
+        const error = Error('Parameter value is mandatory.')
+        error.statusCode = 400
+        next(error)
+    }
+    const config = await Config.findOneAndUpdate({ key }, { value, description })
+    if (!config) {
+        const error = Error('Key not found.')
+        error.statusCode = 404
+        next(error)
     }
-}))
+    res.json({...config, value, description})
+})
 
-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({ key })
-        } else {
-            throw Error('key not found!') 
-        }
-    } catch (error) {
-        res.status(400).json({ msg: error.toString() })
+config.delete('/:key', async (req, res, next) => {
+    const { key } = req.params
+    const deleted = await Config.findOneAndRemove({ key })
+    if (deleted) {
+        res.json({ key })
+    } else {
+        const error = Error('Key not found.')
+        error.statusCode = 404
+        next(error)
     }
-}))
+})
+
+export async function listConfigs () {
+    return await Config.find()
+}
 
 export default config

+ 0 - 439
server/src/restServer/routes/swisstennis.js

@@ -1,439 +0,0 @@
-import fs from 'fs'
-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'
-import MatchList from '../models/matchList'
-import moment from 'moment'
-import { normalize, normalizePhone } from '../helpers'
-
-// Create the router
-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
-    })
-  }
-})
-
-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
-}
-
-/*
-* Define the Routes
-*/
-
-// Login
-swisstennis.post('/login', async (req, res) => {
-  try {
-    const currentState = await checkLogin()
-    if (currentState) {
-      return res.json({ msg: 'Already logged in!' })
-    }
-
-    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 loginPage = await session.post('https://comp.swisstennis.ch/advantage/servlet/Login', loginData)
-    const strLoginPage = loginPage.body.toString()
-    if (strLoginPage.includes('Zugriff verweigert')) {
-      throw Error('Access denied!')
-    }
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-  res.json({ msg: 'Logged in successfully.' })
-})
-
-// Overview of tournaments
-swisstennis.get('/tournaments', async (req, res) => {
-  try {
-    const tournaments = {}
-    let match
-    const myTournamentsPage = await checkLogin()
-    if (!myTournamentsPage) throw Error('Not logged in!')
-
-    const tournamentRegexp = /<a href=".*ProtectedDisplayTournament.*tournament=Id(\d+)">([^<]+)<\/a>/gm
-
-    do {
-      match = tournamentRegexp.exec(myTournamentsPage)
-      if (match) {
-        tournaments[match[1]] = match[2]
-      }
-    } while (match)
-    res.json({ tournaments })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-// Draws of a tournament
-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')) {
-      throw Error('Not logged in.')
-    }
-    const drawRegexp = /<a (?:class="text" )?href=".*DisplayEvent.*eventId=(\d+).*">([^<]+)<\/a>/gm
-    
-    do {
-      match = drawRegexp.exec(strTournamentPage)
-      if (match) {
-        draws[match[1]] = match[2]
-      }
-    } while (match)
-    return res.json({ draws })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-swisstennis.get('/playerlists', async (req, res) => {
-  try {
-    const playerLists = await PlayerList.find().select({_id: 1, imported: 1, file: 1, filesize: 1})
-    return res.json(playerLists)
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-swisstennis.get('/playerlist', async (req, res) => {
-  try {
-    const key = req.query.playerlistId ? { _id: req.query.playerlistId } : {}
-    const playerList = await PlayerList.findOne(key).sort({ imported: -1 }).populate('players')
-    return res.json(playerList)
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-// Download a playerlist
-swisstennis.get('/playerlist/download', async (req, res) => {
-  try {
-    const tournament = req.query.tournament || config.tournamentId
-    
-    if (!tournament) {
-      throw Error('No tournament given.')
-    }
-    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)
-    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, mtime: stats.mtime })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
-  try {
-    console.log('Parsing file', req.params.filename)
-    
-    const filePath = `swisstennis_files/${req.params.filename}`
-    const dateString = req.params.filename.match(/(\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2})/)[1]
-    const fileDate = moment(dateString)
-    const stat = fs.statSync(filePath)
-
-    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.', filePath)
-    const worksheets = await Excel.readWorkbook(filePath)
-    const worksheet = worksheets.Players
-    if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
-      throw Error(`Wrong file structure. Length: ${worksheet[4].length} (expected 32), Column A name: ${worksheet[3][0]} (expected Konkurrenz).`)
-    }
-
-    const headers = worksheet.slice(3, 4)
-
-    const allPlayers = await Promise.all(worksheet.slice(4, worksheet.length).map(async data => {
-      const filePlayer = {
-        created: fileDate.toDate(),
-        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] || 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] || null,
-        rankingDP: normalize(data[27]),
-        confirmed: !!data[29],
-        paid: !!data[31],
-      }
-
-      filePlayer.gender = filePlayer.category ? 
-        filePlayer.category[0] === 'M' ? 'm' : 
-        filePlayer.category[0] === 'W' ? 
-          'w' : 
-          null : 
-        null,
-      filePlayer.doubles = !!filePlayer.category.match(/DM.*|[MW]D.*/)
-      filePlayer.junior = filePlayer.birthDate ? 
-        filePlayer.birthDate.getTime() >= (new Date((new Date()).getFullYear() - 19, 11, 31, 23, 59, 59, 999)).getTime() : 
-        false
-      filePlayer.fullName = filePlayer.doubles ? 
-        `${filePlayer.name} ${filePlayer.firstName} / ${filePlayer.nameDP} ${filePlayer.firstNameDP}` : 
-        `${filePlayer.name} ${filePlayer.firstName}`
-      filePlayer.idString = `${filePlayer.category} % ${filePlayer.fullName}`
-      filePlayer.phone = null
-      const reMobile = /^\+417/
-      if (filePlayer.phoneWork && filePlayer.phoneWork.match(reMobile)) {
-        filePlayer.phone = filePlayer.phoneWork
-      } else if (filePlayer.phonePrivate && filePlayer.phonePrivate.match(reMobile)) {
-        filePlayer.phone = filePlayer.phonePrivate 
-      } else if (filePlayer.phoneMobile && filePlayer.phoneMobile.match(reMobile)) {
-        filePlayer.phone = filePlayer.phoneMobile
-      }
-
-      const player = new Player(filePlayer)
-      player.save()
-      return player._id
-    }))
-
-    const playerList = new PlayerList({
-      imported: fileDate,
-      file: req.params.filename,
-      fileSize: stat.size,
-      players: allPlayers
-    })
-    await playerList.save()
-      
-    return res.json({playerList})
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-// Delete a playerlist
-swisstennis.delete('/playerlist/:playerlistId', async (req, res) => {
-  try {
-    const { playerlistId } = req.params
-    
-    if (!playerlistId) {
-      throw Error('No playerlistId given.')
-    }
-    const playerList = await PlayerList.findOne({ _id: playerlistId })
-    if (!playerList) {
-      throw Error('PlayerList not found.')
-    }
-    playerList.remove()
-    return res.json({ _id: playerlistId })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-// List downloaded files
-swisstennis.get('/files', async (req, res) => {
-  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, mtime: stats.mtime }
-  })
-  return res.json(fileList)
-})
-
-swisstennis.delete('/files', async (req, res) => {
-  try {
-    const { filename } = req.body
-    fs.unlink(`swisstennis_files/${filename}`, (error) => {
-      if (error) throw error
-      res.json({ msg: `successfully deleted swisstennis_files/${filename}.` })
-    })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-swisstennis.get('/calendars', async (req, res) => {
-  try {
-    const calendars = await MatchList.find().select({_id: 1, imported: 1, file: 1, fileSize: 1})
-    return res.json(calendars)
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-swisstennis.get('/calendar', async (req, res) => {
-  try {
-    const key = req.query.calendarId ? { _id: req.query.calendarId } : {}
-    const calendar = await MatchList.findOne(key).sort({ imported: -1 }).populate('matches')
-    await Promise.all(calendar.matches.map(async match => {
-      await Player.populate(match, 'player1')
-      await Player.populate(match, 'player2')
-    }))
-    return res.json(calendar)
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-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 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})
-    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, mtime: stats.mtime })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-swisstennis.get('/calendar/parse/:filename', async (req,res) => {
-  try {
-    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})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)
-
-    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 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],
-        result: normalize(data[8]),
-      }
-      fileMatch.doubles = !!fileMatch.category.match(/DM.*|[MW]D.*/)
-      const player1 = await Player.findOne({idString: `${fileMatch.category} % ${normalize(data[4])}`}).sort({created: -1})
-      const player2 = await Player.findOne({idString: `${fileMatch.category} % ${normalize(data[6])}`}).sort({created: -1})
-      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
-
-      const match = new Match(fileMatch)
-      await match.save()
-      return match._id
-    }))
-    const matchList = new MatchList({
-      imported: fileDate,
-      file: req.params.filename,
-      fileSize: stat.size,
-      matches: allMatches
-    })
-    await matchList.save()
-      
-    return res.json({ matchList })
-  } catch (error) {
-    res.status(400).json({ msg: error.toString() })
-  }
-})
-
-// Delete a calendar
-swisstennis.delete('/calendar/:calendarId', async (req, res) => {
-  try {
-    const { calendarId } = req.params
-    
-    if (!calendarId) {
-      throw Error('No calendarId given.')
-    }
-    const calendar = await MatchList.findOne({ _id: calendarId })
-    if (!calendar) {
-      throw Error('Calendar not found.')
-    }
-    calendar.remove()
-    return res.json({ _id: calendarId })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-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})
-    drawDisplay.pipe(drawFile)
-    return res.json({ fileName })
-  } catch (error) {
-    return res.status(400).json({ msg: error.toString() })
-  }
-})
-
-export default swisstennis

+ 4 - 7
server/src/restServer/routes/sztm.js

@@ -6,11 +6,8 @@ import pdfMake from 'pdfmake/build/pdfmake'
 import pdfFonts from 'pdfmake/build/vfs_fonts'
 import archiver from 'archiver'
 import { places } from '../config/sztm'
-import Match from '../models/match'
-import Player from '../models/player'
-import MatchList from '../models/matchList'
-import PlayerList from '../models/playerList'
-import { resolve } from 'path';
+import Match from '../swisstennis/models/match'
+import MatchList from '../swisstennis/models/matchList'
 pdfMake.vfs = pdfFonts.pdfMake.vfs
 
 const sztm = express.Router() 
@@ -78,7 +75,6 @@ sztm.post('/pdf', async (req, res) => {
         const matchList = await MatchList.findOne({ _id: matchListId })
         const dateString = date.match(/(\d{4}\d{2}\d{2})T\d{2}\d{2}\d{2}/)[1]
         const parsedDate = moment(dateString)
-        console.log('Date', parsedDate, dateString, moment(new Date()))
         const query = { 
             _id: { $in: matchList.matches },
             place,
@@ -92,6 +88,7 @@ sztm.post('/pdf', async (req, res) => {
         const players = []
         const allPlaces = []
         matches.forEach(match => {
+            console.log(match.idString, match.place)
             if (match.player1) players.push(match.player1)
             if (match.player2) players.push(match.player2)
             if (match.place && !allPlaces.includes(match.place)) allPlaces.push(match.place)
@@ -107,7 +104,7 @@ sztm.post('/pdf', async (req, res) => {
             pageOrientation: 'portrait',
             content: [
                 { text: 'Stadtzürcher Tennismeisterschaft', style: 'header' },
-                { text: `Nenngelder für den ${moment(parsedDate).format('DD.MM.YYYY')}`, style: 'subheader' },
+                { text: `Nenngelder für den ${parsedDate.subtract(1, 'day').format('DD.MM.YYYY')}`, style: 'subheader' },
                 { columns: [
                     {text: places[place]},
                     {text: '50.- oder 30.- (Junioren Jg. 2000 oder jünger)'}

+ 90 - 0
server/src/restServer/swisstennis/index.js

@@ -0,0 +1,90 @@
+import express from 'express'
+import bhttp from 'bhttp'
+
+import tournament from './routes/tournament'
+import calendar from './routes/calendar'
+import playerList from './routes/playerList'
+import draw from './routes/draw'
+import files from './routes/files'
+
+import { configDb } from '../api'
+
+// Create the router
+export const swisstennis = express.Router()
+
+// Use this variable for the session
+export const session = bhttp.session()
+
+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
+}
+
+/*
+* Define the Routes
+*/
+
+// Login
+swisstennis.post('/login', async (req, res) => {
+  try {
+    const currentState = await checkLogin()
+    if (currentState) {
+      return res.json({ msg: 'Already logged in!' })
+    }
+    const username = req.body.username || configDb.tournament.value
+    const password = req.body.password || configDb.tournamentPW.value
+    
+    // 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 loginPage = await session.post('https://comp.swisstennis.ch/advantage/servlet/Login', loginData)
+    const strLoginPage = loginPage.body.toString()
+    if (strLoginPage.includes('Zugriff verweigert')) {
+      throw Error('Access denied!')
+    }
+  } catch (error) {
+    return res.status(400).json({ msg: error.toString() })
+  }
+  res.json({ msg: 'Logged in successfully.' })
+})
+
+// Tournament actions
+swisstennis.get('/tournaments', tournament.getTournaments)
+swisstennis.get('/tournament/draws', tournament.getTournamentDraws)
+
+// Calendar actions
+swisstennis.get('/calendars', calendar.getCalendars)
+swisstennis.get('/calendar', calendar.getCalendar)
+swisstennis.get('/calendar/download', calendar.downloadCalendar)
+swisstennis.get('/calendar/parse/:filename', calendar.parseCalendar)
+swisstennis.delete('/calendar/:calendarId', calendar.deleteCalendar)
+
+// PlayerList actions
+swisstennis.get('/playerlist/download', playerList.downloadPlayerList)
+swisstennis.get('/playerlists', playerList.getPlayerLists)
+swisstennis.get('/playerlist', playerList.getPlayerList)
+swisstennis.get('/playerlist/parse/:filename', playerList.parsePlayerList)
+swisstennis.delete('/playerlist/:playerlistId', playerList.deletePlayerList)
+
+// Draw actions
+swisstennis.get('/draws', draw.getDraws)
+swisstennis.get('/draw', draw.getDraw)
+swisstennis.get('/draw/download/:drawId', draw.downloadDraw)
+swisstennis.get('/draw/parse/:filename', draw.parseDraw)
+swisstennis.delete('/draw/:drawId', draw.deleteDraw)
+
+// File actions
+swisstennis.get('/files', files.getFiles)
+swisstennis.delete('/file', files.deleteFile)
+
+export default swisstennis

+ 10 - 0
server/src/restServer/swisstennis/models/draw.js

@@ -0,0 +1,10 @@
+import mongoose from 'mongoose'
+
+const DrawSchema = new mongoose.Schema({
+  imported: Date,
+  matchList: { type: mongoose.Schema.Types.ObjectId, ref: 'MatchList' },
+})
+
+const Draw = mongoose.model('Draw', DrawSchema)
+
+export default Draw

+ 0 - 0
server/src/restServer/models/match.js → server/src/restServer/swisstennis/models/match.js


+ 0 - 0
server/src/restServer/models/matchList.js → server/src/restServer/swisstennis/models/matchList.js


+ 0 - 0
server/src/restServer/models/player.js → server/src/restServer/swisstennis/models/player.js


+ 0 - 0
server/src/restServer/models/playerList.js → server/src/restServer/swisstennis/models/playerList.js


+ 133 - 0
server/src/restServer/swisstennis/routes/calendar.js

@@ -0,0 +1,133 @@
+import fs from 'fs'
+import Excel from './excel'
+import Player from '../models/player'
+import Match from '../models/match'
+import MatchList from '../models/matchList'
+import moment from 'moment'
+import { normalize } from './helpers'
+import { session } from '../index'
+import { configDb } from '../../api'
+
+
+async function getCalendars (req, res) {
+    try {
+        // Calendar list from database. Unly query certain fields.
+        const calendars = await MatchList.find().select({_id: 1, imported: 1, file: 1, fileSize: 1})
+        return res.json(calendars)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function getCalendar (req, res) {
+    try {
+        const key = req.query.calendarId ? { _id: req.query.calendarId } : {}
+        const calendar = await MatchList.findOne(key).sort({ imported: -1 }).populate('matches')
+        await Promise.all(calendar.matches.map(async match => {
+            await Player.populate(match, 'player1')
+            await Player.populate(match, 'player2')
+        }))
+        return res.json(calendar)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function downloadCalendar (req, res) {
+    const tournament = req.query.tournament || configDb.tournamentId.value
+    
+    if (!tournament) {
+        return res.json({ msg: 'No tournament given.' })
+    }
+    try {
+        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})
+        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, mtime: stats.mtime })
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function parseCalendar (req,res) {
+    try {
+        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})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)
+        
+        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 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],
+                result: normalize(data[8]),
+            }
+        fileMatch.doubles = !!fileMatch.category.match(/DM.*|[MW]D.*/)
+        const player1 = await Player.findOne({idString: `${fileMatch.category} % ${normalize(data[4])}`}).sort({created: -1})
+        const player2 = await Player.findOne({idString: `${fileMatch.category} % ${normalize(data[6])}`}).sort({created: -1})
+        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
+        
+        const match = new Match(fileMatch)
+        await match.save()
+        return match._id
+    }))
+    const matchList = new MatchList({
+        imported: fileDate,
+        file: req.params.filename,
+        fileSize: stat.size,
+        matches: allMatches
+    })
+    await matchList.save()
+    
+    return res.json({ matchList })
+} catch (error) {
+    res.status(400).json({ msg: error.toString() })
+}
+}
+
+// Delete a calendar
+async function deleteCalendar (req, res) {
+    try {
+        const { calendarId } = req.params
+        
+        if (!calendarId) {
+            throw Error('No calendarId given.')
+        }
+        const calendar = await MatchList.findOne({ _id: calendarId })
+        if (!calendar) {
+            throw Error('Calendar not found.')
+        }
+        calendar.remove()
+        return res.json({ _id: calendarId })
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+export default { getCalendars, getCalendar, downloadCalendar, parseCalendar, deleteCalendar }

+ 176 - 0
server/src/restServer/swisstennis/routes/draw.js

@@ -0,0 +1,176 @@
+import fs from 'fs'
+import Excel from './excel'
+import Draw from '../models/draw'
+import MatchList from '../models/matchList'
+import PlayerList from '../models/playerList'
+import moment from 'moment'
+import { normalize } from './helpers'
+import { session } from '../index'
+
+
+function initials (player) {
+    const fnInitials = player.firstName
+        .split('-')
+        .map(element=>element
+            .split(' ')
+            .map(element => element[0]+'.')
+            .join(' ')
+        ).join('-')
+    const fnInitialsDP = player.firstNameDP
+        .split('-')
+        .map(element=>element
+            .split(' ')
+            .map(element => element[0]+'.')
+            .join(' ')
+        ).join('-')
+    return player.doubles ? `${player.name} ${fnInitials} / ${player.nameDP} ${fnInitialsDP}` : `${name} ${fnInitials}`
+}
+
+async function getDraws (req, res) {
+    try {
+        const draws = await Draw.find()
+        return res.json(draws)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function getDraw (req, res) {
+    try {
+        const key = req.query.drawId ? { _id: req.query.drawId } : {}
+        const draw = await MatchList.findOne(key).sort({ imported: -1 })
+        return res.json(draw)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function downloadDraw (req, res) {
+    try {
+        const { drawId } = req.params
+        
+        const filename = `DisplayDraw-${drawId}-${moment().format('YYYYMMDDTHHmmss')}.xls`
+        const drawFile = fs.createWriteStream(`swisstennis_files/${filename}`)
+        const drawDisplay = await session.get(`https://comp.swisstennis.ch/advantage/servlet/DisplayDraw.xls?eventId=${drawId}&lang=D`, {stream: true})
+        await drawDisplay.pipe(drawFile)
+        const streamDone = new Promise((resolve, reject) => {
+            drawFile.on('finish', resolve)
+            drawFile.on('error', reject)
+        })
+        await streamDone
+        const stats = fs.statSync(`swisstennis_files/${filename}`)
+        return res.json({ filename, size: stats.size, mtime: stats.mtime })
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function parseDraw (req,res) {
+    try {
+        console.log('Parsing file', req.params.filename)
+        
+        const filePath = `swisstennis_files/${req.params.filename}`
+        const dateString = req.params.filename.match(/(\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2})/)[1]
+        const fileDate = moment(dateString)
+        const stat = fs.statSync(filePath)
+        
+        const fileNewer = await Draw.find({ imported: { $gte: fileDate } })
+        if (fileNewer.length) {
+            throw Error('File has to be newer.')
+        }
+        
+        console.log('About to read the draw.')
+        const worksheets = await Excel.readWorkbook(`swisstennis_files/${req.params.filename}`)
+        const worksheet = worksheets.Tableaux
+        if (worksheet[0].length !== 1 || worksheet[1].length !== 1 || worksheet[2].length !== 1 || worksheet[3].length !== 0) {
+            throw Error(`Wrong file structure.`)
+        }
+
+        const category = normalize(worksheet[2][0])
+        const doubles = !!category.match(/DM.*|[MW]D.*/)
+        const drawSize = ( worksheet.length - 4 + 1 ) / 2 // 4 header lines, 1 empty line between each player (n-1)
+        const draw = worksheet.slice(4)
+        const rounds = Math.log2(drawSize)
+
+        const playerList = await PlayerList.findOne().sort({imported: -1}).populate('players')
+        const players = playerList.players.filter(player => player.category === category)
+        const matchList = await MatchList.findOne().sort({imported: -1}).populate('matches')
+        const matches = matchList.matches
+        
+        const drawMatches = []
+        for (var round = 0; round < rounds; round += 1) {
+            const matchPositionOffset = Math.pow(2, round)
+            const playerOffset = 2 * matchPositionOffset
+            const roundOffset = playerOffset/2 - 1 
+            const matchIncrement = 2 * playerOffset
+
+            for (var seq = 0; seq < drawSize / Math.pow(2, round + 1); seq += 1) {
+                const matchPosition = roundOffset + seq * matchIncrement
+
+                // Data from spreadsheet cells
+                const player1String = normalize(draw[matchPosition][round])
+                const player2String = normalize(draw[matchPosition + playerOffset][round])
+                const winnerString = normalize(draw[matchPositionOffset + matchPosition][round + 1])
+                const timePlaceResultString = normalize(draw[matchPositionOffset + matchPosition + 1][round + 1])
+
+                const drawPlayerRegex = /^(?:\((\d+)\))?\s*\(([RN]\d\s?(?:\d+)?|NC)\) (.+)$/
+                const drawDoubleRegex = /^(?:\((\d+)\))?\s*\(([RN]\d\s?(?:\d+)?|NC)\/([RN]\d\s?(?:\d+)?|NC)\) ([^\/]+)\s*\/\s*([^\/]+)$/
+                
+                const player1Match = doubles ? player1String.match(drawDoubleRegex) : player1String.match(drawDoubleRegex)
+                const player2Match = doubles ? player2String.match(drawDoubleRegex) : player2String.match(drawDoubleRegex)
+                
+                console.log(player1Match, player2Match)
+                const player1 = (round === 0) ? 
+                    players.find(player => player1Match && (category === player.category) && (player1Match[3] === player.fullName)) : 
+                    players.find(player => (category === player.category) && (player1String === initials(player)))
+                
+                const player2 = (round === 0) ? 
+                    players.find(player => player2Match && (category === player.category) && (player2Match[3] === player.fullName)) : 
+                    players.find(player => (category === player.category) && (player2String === initials(player)))
+                    
+                const timePlace = timePlaceResultString.match(/^(\d{2}\/\d{2}\/\d{2})?\s+(\d{2}:\d{2})?\s*\(([^\)]+)\)?$/)
+                const result = normalize(draw[matchPosition + playerOffset/2 + 1][round + 1]).match(/^((?:WO)?\s*(?:\d+\/\d+\s*)*)$/)
+                const winner = winnerString
+                const idString = (player1 && player2)
+                const match = {
+                    created: new Date(),
+                    idString: '',
+                    category,
+                    place: timePlace && timePlace[3] || null,
+                    date: timePlace && timePlace[1] && timePlace[2] && moment(`${timePlace[1]} ${timePlace[2]}`, 'DD/MM/YY HH:mm') || null,
+                    round: round + 1,
+                    player1: player1 && player1._id || null,
+                    player2: player2 && player2._id || null,
+                    winner: winner || null,
+                    result: result && result[0] || null,
+                    doubles
+                }
+                drawMatches.push(match)
+            }
+        }
+        console.log('matches:', drawMatches.length)
+        return res.json({ drawMatches })
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function deleteDraw (req, res) {
+    try {
+        const { drawId } = req.params
+        
+        if (!drawId) {
+            throw Error('No drawId given.')
+        }
+        const draw = await MatchList.findOne({ _id: drawId })
+        if (!draw) {
+            throw Error('Draw not found.')
+        }
+        draw.remove()
+        return res.json({ _id: drawId })
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+export default { getDraws, getDraw, downloadDraw, parseDraw, deleteDraw }

+ 0 - 0
server/src/restServer/excel.js → server/src/restServer/swisstennis/routes/excel.js


+ 30 - 0
server/src/restServer/swisstennis/routes/files.js

@@ -0,0 +1,30 @@
+import fs from 'fs'
+import awaitFs from 'await-fs'
+
+
+// List downloaded files
+async function getFiles(req, res) {
+    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, mtime: stats.mtime }
+    })
+    return res.json(fileList)
+  }
+  
+ async function deleteFile (req, res) {
+    try {
+      const { filename } = req.body
+      fs.unlink(`swisstennis_files/${filename}`, (error) => {
+        if (error) throw error
+        res.json({ msg: `successfully deleted swisstennis_files/${filename}.` })
+      })
+    } catch (error) {
+      return res.status(400).json({ msg: error.toString() })
+    }
+  }
+
+  export default { getFiles, deleteFile }

+ 1 - 29
server/src/restServer/helpers.js → server/src/restServer/swisstennis/routes/helpers.js

@@ -1,35 +1,7 @@
-import moment from 'moment'
-
-function date2s (date) {
-  return moment(date).format('DD.MM.')
-}
-
-function time2s (date) {
-  return moment(date).format('HH:mm')
-}
-
-function datetime2s (date) {
-  return moment(date).format('DD.MM. HH:mm')
-}
-
 function normalize (item) {
   return item ? String(item).replace(/\s+/g, ' ').trim() : ''
 }
 
-function fileSize (int) {
-  let value
-  if (int > Math.pow(2, 10)) {
-    value = `${int / Math.pow(2, 10)}kB`
-  }
-  if (int > Math.pow(2, 20)) {
-    value = `${int / Math.pow(2, 20)}MB`
-  }
-  if (int > Math.pow(2, 30)) {
-    value = `${int / Math.pow(2, 30)}GB`
-  }
-  return value
-} 
-
 function normalizePhone (item) {
   const phone = item ? String(item).replace(/\s|\+|\/|,|-|'/g, '').replace(/\(0\)/, '').replace(/^0+/, '') : ''
   if (phone.match(/[^\d*]/)) {
@@ -68,4 +40,4 @@ function filterExact (item, filter) {
   return item === filter
 }
 
-export { date2s, time2s, datetime2s, normalize, normalizePhone, fileSize, filterFuzzy, filterExact }
+export { normalize, normalizePhone, filterFuzzy, filterExact }

+ 159 - 0
server/src/restServer/swisstennis/routes/playerList.js

@@ -0,0 +1,159 @@
+import fs from 'fs'
+import Excel from './excel'
+import Player from '../models/player'
+import PlayerList from '../models/playerList'
+import moment from 'moment'
+import { normalize, normalizePhone } from './helpers'
+import { session } from '../index'
+import { configDb } from '../../api'
+
+
+async function getPlayerLists (req, res) {
+    try {
+        const playerLists = await PlayerList.find().select({_id: 1, imported: 1, file: 1, filesize: 1})
+        return res.json(playerLists)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function getPlayerList (req, res) {
+    try {
+        const key = req.query.playerlistId ? { _id: req.query.playerlistId } : {}
+        const playerList = await PlayerList.findOne(key).sort({ imported: -1 }).populate('players')
+        return res.json(playerList)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function downloadPlayerList (req, res) {
+    try {
+        const tournament = req.query.tournament || configDb.tournamentId.value
+        
+        if (!tournament) {
+            throw Error('No tournament given.')
+        }
+        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)
+        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, mtime: stats.mtime })
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+async function parsePlayerList (req, res) {
+    try {
+        console.log('Parsing file', req.params.filename)
+        
+        const filePath = `swisstennis_files/${req.params.filename}`
+        const dateString = req.params.filename.match(/(\d{4}\d{2}\d{2}T\d{2}\d{2}\d{2})/)[1]
+        const fileDate = moment(dateString)
+        const stat = fs.statSync(filePath)
+        
+        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.', filePath)
+        const worksheets = await Excel.readWorkbook(filePath)
+        const worksheet = worksheets.Players
+        if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
+            throw Error(`Wrong file structure. Length: ${worksheet[4].length} (expected 32), Column A name: ${worksheet[3][0]} (expected Konkurrenz).`)
+        }
+        
+        const headers = worksheet.slice(3, 4)
+        
+        const allPlayers = await Promise.all(worksheet.slice(4, worksheet.length).map(async data => {
+            const filePlayer = {
+                created: fileDate.toDate(),
+                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] || 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] || null,
+                rankingDP: normalize(data[27]),
+                confirmed: !!data[29],
+                paid: !!data[31],
+            }
+            
+            filePlayer.gender = filePlayer.category ? 
+            filePlayer.category[0] === 'M' ? 'm' : 
+            filePlayer.category[0] === 'W' ? 
+            'w' : 
+            null : 
+            null,
+        filePlayer.doubles = !!filePlayer.category.match(/DM.*|[MW]D.*/)
+        filePlayer.junior = filePlayer.birthDate ? 
+        filePlayer.birthDate.getTime() >= (new Date((new Date()).getFullYear() - 19, 11, 31, 23, 59, 59, 999)).getTime() : 
+        false
+        filePlayer.fullName = filePlayer.doubles ? 
+        `${filePlayer.name} ${filePlayer.firstName} / ${filePlayer.nameDP} ${filePlayer.firstNameDP}` : 
+        `${filePlayer.name} ${filePlayer.firstName}`
+        filePlayer.idString = `${filePlayer.category} % ${filePlayer.fullName}`
+        filePlayer.phone = null
+        const reMobile = /^\+417/
+        if (filePlayer.phoneWork && filePlayer.phoneWork.match(reMobile)) {
+            filePlayer.phone = filePlayer.phoneWork
+        } else if (filePlayer.phonePrivate && filePlayer.phonePrivate.match(reMobile)) {
+            filePlayer.phone = filePlayer.phonePrivate 
+        } else if (filePlayer.phoneMobile && filePlayer.phoneMobile.match(reMobile)) {
+            filePlayer.phone = filePlayer.phoneMobile
+        }
+        
+        const player = new Player(filePlayer)
+        player.save()
+        return player._id
+    }))
+    
+    const playerList = new PlayerList({
+        imported: fileDate,
+        file: req.params.filename,
+        fileSize: stat.size,
+        players: allPlayers
+    })
+    await playerList.save()
+    
+    return res.json({playerList})
+} catch (error) {
+    return res.status(400).json({ msg: error.toString() })
+}
+}
+
+async function deletePlayerList (req, res) {
+    try {
+        const { playerlistId } = req.params
+        
+        if (!playerlistId) {
+            throw Error('No playerlistId given.')
+        }
+        const playerList = await PlayerList.findOne({ _id: playerlistId })
+        if (!playerList) {
+            throw Error('PlayerList not found.')
+        }
+        playerList.remove()
+        return res.json({ _id: playerlistId })
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+}
+
+export default { getPlayerLists, getPlayerList, downloadPlayerList, parsePlayerList, deletePlayerList }

+ 59 - 0
server/src/restServer/swisstennis/routes/tournament.js

@@ -0,0 +1,59 @@
+import { session } from '../index'
+
+
+// Overview of tournaments
+async function getTournaments (req, res) {
+    try {
+      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 tournamentRegexp = /<a href=".*ProtectedDisplayTournament.*tournament=Id(\d+)">([^<]+)<\/a>/gm
+  
+      const tournaments = {}
+      let match
+      
+      do {
+        match = tournamentRegexp.exec(strMyTournamentsPage)
+        if (match) {
+          tournaments[match[1]] = match[2]
+        }
+      } while (match)
+      res.json({ tournaments })
+    } catch (error) {
+      return res.status(400).json({ msg: error.toString() })
+    }
+  }
+  
+  // Draws of a tournament
+  async function getTournamentDraws (req, res) {
+    try {
+      const tournament = req.params.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')) {
+        throw Error('Not logged in.')
+      }
+      const drawRegexp = /<a (?:class="text" )?href=".*DisplayEvent.*eventId=(\d+).*">([^<]+)<\/a>/gm
+      
+      do {
+        match = drawRegexp.exec(strTournamentPage)
+        if (match) {
+          draws[match[1]] = match[2]
+        }
+      } while (match)
+      return res.json({ draws })
+    } catch (error) {
+      return res.status(400).json({ msg: error.toString() })
+    }
+  }
+
+  export default { getTournaments, getTournamentDraws }