Browse Source

worked on server and client.

Tomi Cvetic 6 years ago
parent
commit
9ca431924d
78 changed files with 1302 additions and 2969 deletions
  1. 1 1
      client/.env
  2. 1 1
      client/src/.env
  3. 1 1
      client/src/alerts/components/AlertList.js
  4. 0 20
      client/src/calendar/components/MatchDisp.js
  5. 0 62
      client/src/calendar/components/MatchForm.js
  6. 0 17
      client/src/calendar/components/MatchList.js
  7. 0 53
      client/src/calendar/components/MatchTable.js
  8. 0 7
      client/src/calendar/components/index.js
  9. 0 20
      client/src/calendar/functions.js
  10. 0 120
      client/src/calendar/state.js
  11. 0 24
      client/src/classes/match.js
  12. 0 16
      client/src/classes/match.test.js
  13. 0 64
      client/src/classes/player.js
  14. 7 6
      client/src/config/state.js
  15. 5 0
      client/src/config/static.js
  16. 0 641
      client/src/example_excel_write.js
  17. 0 74
      client/src/excel/index.js
  18. 20 27
      client/src/index.js
  19. 0 131
      client/src/macros/SZTM_Spielliste.bas
  20. 0 202
      client/src/macros/SZTM_Zahlliste.bas
  21. 92 0
      client/src/matches/components/Matches.js
  22. 84 0
      client/src/matches/components/Players.js
  23. 4 0
      client/src/matches/components/index.js
  24. 0 0
      client/src/matches/index.js
  25. 204 0
      client/src/matches/state.js
  26. 0 167
      client/src/module/container.js
  27. 0 5
      client/src/module/index.js
  28. 0 34
      client/src/module/module.js
  29. 0 14
      client/src/playerList/components/PlayerActions.js
  30. 0 43
      client/src/playerList/components/PlayerFilter.js
  31. 0 61
      client/src/playerList/components/PlayerForm.js
  32. 0 20
      client/src/playerList/components/PlayerList.js
  33. 0 55
      client/src/playerList/components/PlayerTable.js
  34. 0 8
      client/src/playerList/components/index.js
  35. 0 20
      client/src/playerList/functions.js
  36. 0 10
      client/src/playerList/index.js
  37. 0 121
      client/src/playerList/state.js
  38. 8 0
      client/src/routes.js
  39. 0 87
      client/src/scraper/components/ScraperInterface.js
  40. 0 4
      client/src/scraper/components/index.js
  41. 0 169
      client/src/scraper/state.js
  42. 0 86
      client/src/scraper/test.js
  43. 0 8
      client/src/settings/index.js
  44. 0 46
      client/src/settings/state.js
  45. 68 0
      client/src/sms/components/SMS.js
  46. 4 0
      client/src/sms/components/index.js
  47. 0 0
      client/src/sms/index.js
  48. 123 0
      client/src/sms/state.js
  49. 0 100
      client/src/startPage/components/FileImport.js
  50. 0 20
      client/src/startPage/components/StartPage.js
  51. 0 4
      client/src/startPage/components/index.js
  52. 0 8
      client/src/startPage/index.js
  53. 0 27
      client/src/startPage/state.js
  54. 0 81
      client/src/swisstennis/components/ConfigList.js
  55. 100 0
      client/src/swisstennis/components/Swisstennis.js
  56. 3 3
      client/src/swisstennis/components/index.js
  57. 189 113
      client/src/swisstennis/state.js
  58. 0 56
      client/src/tables/components/Table.js
  59. 0 8
      client/src/tables/index.js
  60. 0 55
      client/src/tables/state.js
  61. 0 0
      client/src/users/components/EditUser.js
  62. 8 7
      client/src/users/state.js
  63. 10 0
      server/package-lock.json
  64. 2 0
      server/package.json
  65. 8 3
      server/src/restServer/api.js
  66. 0 5
      server/src/restServer/config/swisstennis.js
  67. 21 0
      server/src/restServer/config/sztm.js
  68. 2 1
      server/src/restServer/models/match.js
  69. 1 0
      server/src/restServer/models/matchList.js
  70. 2 0
      server/src/restServer/models/player.js
  71. 1 0
      server/src/restServer/models/playerList.js
  72. 13 0
      server/src/restServer/models/sms.js
  73. 1 1
      server/src/restServer/routes/authenticate.js
  74. 12 0
      server/src/restServer/routes/download.js
  75. 66 0
      server/src/restServer/routes/sms.js
  76. 29 2
      server/src/restServer/routes/swisstennis.js
  77. 211 29
      server/src/restServer/routes/sztm.js
  78. 1 1
      server/src/restServer/routes/users.js

+ 1 - 1
client/.env

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

+ 1 - 1
client/src/.env

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

+ 1 - 1
client/src/alerts/components/AlertList.js

@@ -47,7 +47,7 @@ class AlertList extends React.Component {
     } else {
       return null
     }
-  }
+  } 
 }
 
 export default AlertList

+ 0 - 20
client/src/calendar/components/MatchDisp.js

@@ -1,20 +0,0 @@
-import React from 'react'
-import { date2s, time2s } from '../../helpers.js'
-
-class MatchDisp extends React.Component {
-  render () {
-    const match = this.props.match
-    return (
-      <tr>
-        <td>{match.Ort || <strong>Kein Platz zugeteilt</strong>}</td>
-        <td>{match.Datum ? date2s(match.Datum) : <strong>Kein Datum zugeteilt</strong>}</td>
-        <td>{match.Datum ? time2s(match.Datum) : <strong>Keine Zeit zugeteilt</strong>}</td>
-        <td>{match.Konkurrenz}</td>
-        <td>{match.Spieler1}</td>
-        <td>{match.Spieler2}</td>
-      </tr>
-    )
-  }
-}
-
-export default MatchDisp

+ 0 - 62
client/src/calendar/components/MatchForm.js

@@ -1,62 +0,0 @@
-import React from 'react'
-import { FormGroup, ControlLabel, FormControl, HelpBlock } from 'react-bootstrap'
-import { fileSize, date2s, time2s } from '../../helpers'
-
-function FieldGroup ({ id, label, help, file, ...props }) {
-  return (
-    <FormGroup controlId={id}>
-      <ControlLabel>{label}</ControlLabel>
-      <FormControl {...props} />
-      {help && <HelpBlock>{help}</HelpBlock>}
-      {file && <div>{fileSize(file.size)} {date2s(file.lastModified)} {time2s(file.lastModified)}</div>}
-    </FormGroup>
-  )
-}
-
-class MatchForm extends React.Component {
-  constructor () {
-    super()
-    this.handleFileUpload = this.handleFileUpload.bind(this)
-  }
-
-  handleFileUpload (event) {
-    event.preventDefault()
-    const { fileUploadStart } = this.props.actions
-    const { files } = this.calendarFile
-    // if (files.length === 0) {
-    //   alertAdd({ type: 'info', text: 'Datei entfernt' })
-    //   return
-    // }
-    // if (files.length > 1) {
-    //   alertAdd({ type: 'warning', text: 'Mehrere Dateien gesendet. Nur die erste wird verarbeitet.' })
-    // }
-    const file = files[0]
-    fileUploadStart(file)
-  }
-
-  render () {
-    const { fileUpload, file, allMatches } = this.props.state
-
-    return (
-      <div>
-        <form>
-          <FieldGroup
-            id='calendarFile'
-            label='Calendar.xls File'
-            type='file'
-            file={file}
-            inputRef={input => { this.calendarFile = input }}
-            onChange={this.handleFileUpload}
-            disabled={(fileUpload === 'started')}
-          />
-        </form>
-        <div>{file
-          ? `${fileSize(file.size)} ${date2s(file.lastModified)} ${time2s(file.lastModified)} ${allMatches.length} Matches`
-          : 'Datei noch nicht geladen...'}
-        </div>
-      </div>
-    )
-  }
-}
-
-export default MatchForm

+ 0 - 17
client/src/calendar/components/MatchList.js

@@ -1,17 +0,0 @@
-import React from 'react'
-import MatchTable from './MatchTable'
-
-class MatchList extends React.Component {
-  render () {
-    const { state, actions } = this.props
-
-    return (
-      <div>
-        <h1>Matchliste</h1>
-        <MatchTable state={state} actions={actions} />
-      </div>
-    )
-  }
-}
-
-export default MatchList

+ 0 - 53
client/src/calendar/components/MatchTable.js

@@ -1,53 +0,0 @@
-import React from 'react'
-import { date2s, time2s } from '../../helpers'
-
-class MatchRow extends React.Component {
-  render () {
-    const match = this.props.match
-    return (
-      <tr>
-        <td>{match.Ort}</td>
-        <td>{date2s(match.Datum)}</td>
-        <td>{match.Konkurrenz}</td>
-        <td>{match.Spieler1}</td>
-        <td>{match.Spieler1Klassierung}</td>
-        <td>{match.Spieler2}</td>
-        <td>{match.Spieler2Klassierung}</td>
-        <td>{match.Resultat}</td>
-      </tr>
-    )
-  }
-}
-
-class MatchTable extends React.Component {
-  render () {
-    const { allMatches, filteredMatches } = this.props.state || { allMatches: [], filteredMatches: [] }
-
-    return (
-      <div>
-        <h2>Spielerliste ({filteredMatches.length}/{allMatches.length})</h2>
-        <table className='table table-bordered table-striped'>
-          <thead>
-            <tr>
-              <th>Ort</th>
-              <th>Datum</th>
-              <th>Konkurrenz</th>
-              <th>Spieler1</th>
-              <th>Spieler1Klassierung</th>
-              <th>Spieler2</th>
-              <th>Spieler2Klassierung</th>
-              <th>Resultat</th>
-            </tr>
-          </thead>
-          <tbody>
-            {filteredMatches.map((match, key) =>
-              <MatchRow key={key} match={match} />
-            )}
-          </tbody>
-        </table>
-      </div>
-    )
-  }
-}
-
-export default MatchTable

+ 0 - 7
client/src/calendar/components/index.js

@@ -1,7 +0,0 @@
-import MatchDisp from './MatchDisp'
-import MatchList from './MatchList'
-import MatchForm from './MatchForm'
-import MatchTable from './MatchTable'
-
-export { MatchDisp, MatchList, MatchForm, MatchTable }
-export default { MatchDisp, MatchList, MatchForm, MatchTable }

+ 0 - 20
client/src/calendar/functions.js

@@ -1,20 +0,0 @@
-import Excel from '../excel'         // Helper files to create Excel files
-import Match from '../classes/match'
-
-export function generateCalendar (file) {
-  return new Promise((resolve, reject) => {
-    console.log('About to read the calendar.')
-    Excel.readWorkbook(file).then(worksheets => {
-      console.log('got worksheets', worksheets)
-      const worksheet = worksheets.Sheet1
-      if (worksheet[2].length < 8 | worksheet[2].length > 9) {
-        reject(Error('Wrong file structure.'))
-      }
-      const calendar = worksheet.slice(2, worksheet.length).map((matchData) => new Match.Match(matchData))
-      console.log('State after generating calendar:', calendar)
-      resolve(calendar)
-    }).catch(error => {
-      reject(Error(error))
-    })
-  })
-}

+ 0 - 120
client/src/calendar/state.js

@@ -1,120 +0,0 @@
-/** @module player/state */
-import { call, put, takeEvery } from 'redux-saga/effects'
-import { generateCalendar } from './functions'
-
-/**
- * state.js
- *
- * Collection of everything which has to do with state changes.
- **/
-
-/** actionTypes define what actions are handeled by the reducer. */
-export const actions = {
-  fileUploadStart: file => {
-    return {
-      type: 'CALENDAR_FILE_UPLOAD_START',
-      file
-    }
-  },
-  fileUploadSuccess: allMatches => {
-    return {
-      type: 'CALENDAR_FILE_UPLOAD_SUCCESS',
-      allMatches
-    }
-  },
-  fileUploadFailure: error => {
-    return {
-      type: 'CALENDAR_FILE_UPLOAD_FAILURE',
-      alert: { type: 'warning', text: error.toString() }
-    }
-  },
-  filterSortMatches: () => {
-    return {
-      type: 'CALENDAR_FILTER_SORT'
-    }
-  }
-}
-console.log('State actions', actions)
-
-/** state definition */
-export const state = {
-  allMatches: [],
-  filteredMatches: [],
-  filters: [
-    {
-      filterFunction: (match, filterValue) => {
-        return match.Konkurrenz === filterValue
-      },
-      filterValue: 'MS R6/R7'
-    }
-  ],
-  sorting: [
-    (match1, match2) => {
-      if (match1.Datum > match2.Datum) {
-        return 1
-      }
-      if (match1.Datum === match2.Datum) {
-        return 0
-      }
-      return -1
-    },
-    (match1, match2) => {
-      if (match1.Ort > match2.Ort) {
-        return 1
-      }
-      if (match1.Ort === match2.Ort) {
-        return 0
-      }
-      return -1
-    }
-  ],
-  fileUpload: 'idle',
-  file: null
-}
-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 'CALENDAR_FILE_UPLOAD_START':
-      return { ...state, fileUpload: 'started', file: action.file }
-    case 'CALENDAR_FILE_UPLOAD_SUCCESS':
-      return { ...state, fileUpload: 'finished', allMatches: action.allMatches }
-    case 'CALENDAR_FILE_UPLOAD_FAILURE':
-      return { ...state, fileUpload: 'failure' }
-    case 'CALENDAR_FILTER_SORT':
-      const { allMatches, filters, sorting } = state
-      const sortedMatches = allMatches.sort((match1, match2) => {
-        return sorting.length ? sorting.reduce((acc, sortFun) => {
-          return acc === 0 ? sortFun(match1, match2) : acc
-        }, 0) : 0
-      })
-      console.log('Sorted matches', sortedMatches)
-      const filteredMatches = sortedMatches.filter(match => {
-        return filters.length ? filters.map(filter => filter.filterFunction(match, filter.filterValue)).every(value => !!value) : true
-      })
-      console.log('Filtered matches', filteredMatches)
-      return { ...state, filteredMatches }
-    default:
-      return state
-  }
-}
-
-function * uploadFile (action) {
-  try {
-    console.log('Calendar uploadFile', action.file)
-    const allMatches = yield call(generateCalendar, action.file)
-    console.log('Calendar success!', actions.fileUploadSuccess(allMatches))
-    yield put(actions.fileUploadSuccess(allMatches))
-    yield put(actions.filterSortMatches())
-  } catch (error) {
-    console.log('Calendar failure!', actions.fileUploadFailure(error))
-    yield put(actions.fileUploadFailure(error))
-  }
-}
-
-/** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {
-  console.log('Calendar saga started.')
-  yield takeEvery('CALENDAR_FILE_UPLOAD_START', uploadFile)
-}

+ 0 - 24
client/src/classes/match.js

@@ -1,24 +0,0 @@
-import { normalize } from '../helpers.js'
-
-/** Class representing a match */
-class Match {
-  /**
-   * Create a match
-   * A match data item in the Swisstennis Calendar.xlsx file has the following columns
-   * Ort | Datum | Zeit | Konkurrenz | Spieler 1 | Spieler 1 Klassierung | Spieler 2 |
-   * Spieler 2 Klassierung | [Resultat]
-   */
-  constructor (data) {
-    this.Ort = normalize(data[0])
-    this.Datum = data[1]
-    this.Konkurrenz = normalize(data[3])
-    this.Spieler1 = normalize(data[4])
-    this.Spieler1Klassierung = normalize(data[5])
-    this.Spieler2 = normalize(data[6])
-    this.Spieler2Klassierung = normalize(data[7])
-    this.Resultat = normalize(data[8] || null)
-    this.isDoubles = this.Konkurrenz.match(/DM.*|[MW]D.*/)
-  }
-}
-
-export default { Match }

+ 0 - 16
client/src/classes/match.test.js

@@ -1,16 +0,0 @@
-import Match from './match'
-
-it('evaluates a valid user data array', () => {
-  const validData = [
-    'AU',                                   // Ort
-    (new Date(2017, 6, 5, 4, 3, 2)),   // Datum
-    (new Date(2017, 6, 5, 4, 3, 2)),   // Zeit
-    'MS 45+',                               // Konkurrenz
-    'Bobo DJ',                              // Spieler 1
-    'R6',                                   // Spieler 1 Klassierung
-    'Hofer Polo',                           // Spieler 2
-    'N4 (75)',                              // Spieler 2 Klassierung
-    'WO'                                    // [Resultat]
-  ]
-  new Match(validData)
-})

+ 0 - 64
client/src/classes/player.js

@@ -1,64 +0,0 @@
-import { normalize, normalizePhone } from '../helpers.js'
-
-/** Regular expression for cellphone numbers. Cellphone numbers start with +417 */
-const reMobile = /^\+417/
-
-/** Class representing a player */
-class Player {
-  /**
-   * Create a player
-   * A player data item in the Swisstennis PlayerList.xlsx file has the following columns
-   * Konkurrenz | Anmeldedatum | Lizenznummer | Klub | Klub Name | Name | Vorname | Geburtsdatum | Adresse | c/o | PLZ | Ort |
-   * Land | Tel P | Tel G | Mobile | Email | Klassierung | Klass. Wert | Gesetzte | Kommentar | Einschränkungen | Kommentar |
-   * Lizenz Nr.Doppelpartner | Name Doppelpartner | Vorname Doppelpartner | Geburtsdatum Doppelpartner | Klassierung Doppelpartner |
-   * Klass. Wert Doppelpartner | bestätigt | On-line Anmeldung | bezahlt
-   */
-  constructor (data) {
-    this.Konkurrenz = normalize(data[0])
-    this.Lizenz = normalize(data[2])
-    this.geschlecht = null
-    if (this.Konkurrenz) {
-      if (this.Konkurrenz[0] === 'M') {
-        this.geschlecht = 'm'
-      } else if (this.Konkurrenz[0] === 'W') {
-        this.geschlecht = 'w'
-      }
-    }
-    this.Name = normalize(data[5])
-    this.Vorname = normalize(data[6])
-    this.Geburtsdatum = data[7]
-    this.TelP = normalizePhone(data[13])
-    this.TelG = normalizePhone(data[14])
-    this.Mobile = normalizePhone(data[15])
-    this.Email = normalize(data[16])
-    this.Klassierung = normalize(data[17])
-    this.LizenzDP = normalize(data[23])
-    this.NameDP = normalize(data[24])
-    this.VornameDP = normalize(data[25])
-    this.GeburtsdatumDP = data[26]
-    this.KlassierungDP = normalize(data[27])
-    this.Bestaetigt = data[29]
-    this.Bezahlt = data[31]
-    this.BezahltAm = null
-    this.Matches = []
-    this.isDoubles = !!this.Konkurrenz.match(/DM.*|[MW]D.*/)
-    this.isJunior = (this.Geburtsdatum) ? this.Geburtsdatum.getTime() >= (new Date((new Date()).getFullYear() - 19, 11, 31, 23, 59, 59, 999)).getTime() : false
-    this.isJuniorDP = (this.isDoubles && this.GeburtsdatumDP) ? this.GeburtsdatumDP.getTime() >= (new Date((new Date()).getFullYear() - 19, 11, 31, 23, 59, 59, 999)).getTime() : false
-    this.name = this.isDoubles ? `${this.Name} ${this.Vorname} / ${this.NameDP} ${this.VornameDP}` : `${this.Name} ${this.Vorname}`
-    if (this.Mobile && this.Mobile.match(reMobile)) {
-      this.phone = this.Mobile
-    } else if (this.TelP && this.TelP.match(reMobile)) {
-      this.phone = this.TelP
-    } else if (this.TelG && this.TelG.match(reMobile)) {
-      this.phone = this.TelG
-    } else if (this.Mobile && this.Mobile.match(/FEHLER/)) {
-      this.phone = this.Mobile
-    } else if (this.TelP && this.TelP.match(/FEHLER/)) {
-      this.phone = this.TelP
-    } else if (this.TelG && this.TelG.match(/FEHLER/)) {
-      this.phone = this.TelG
-    }
-  }
-}
-
-export default { Player }

+ 7 - 6
client/src/config/state.js

@@ -1,5 +1,6 @@
 /** @module config/state */
 import { call, put, takeEvery, select } from 'redux-saga/effects'
+import { SZTM_API } from '../config/static'
 
 /**
  * state.js
@@ -164,7 +165,7 @@ 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', {
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
       method: 'GET',
       headers: {
         'Content-Type': 'application/json',
@@ -179,8 +180,8 @@ function * getConfig (action) {
       yield put(actions.configGetSuccess(responseJson))
     }
   } catch (error) {
-    console.log('Config failure!', actions.configAddFailure(error))
-    yield put(actions.configAddFailure(error))
+    console.log('Config failure!', actions.configGetFailure(error))
+    yield put(actions.configGetFailure(error))
   }
 }
 
@@ -188,7 +189,7 @@ 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', {
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
@@ -212,7 +213,7 @@ 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', {
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
       method: 'PUT',
       headers: {
         'Content-Type': 'application/json',
@@ -238,7 +239,7 @@ function * deleteConfig (action) {
     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', {
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
       method: 'DELETE',
       headers: {
         'Content-Type': 'application/json',

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

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

+ 0 - 641
client/src/example_excel_write.js

@@ -1,641 +0,0 @@
-var XLSX = require('xlsx');
-var OUTFILE = '/tmp/example-style.xlsx';
-
-function JSDateToExcelDate(inDate) {
-  return 25569.0 + ((inDate.getTime() - (inDate.getTimezoneOffset() * 60 * 1000)) / (1000 * 60 * 60 * 24));
-}
-
-var defaultCellStyle = { font: { name: "Verdana", sz: 11, color: "FF00FF88"}, fill: {fgColor: {rgb: "FFFFAA00"}}};
-
-// test to see if everything on the left equals its counterpart on the right
-// but the right hand object may have other attributes which we might not care about
-function basicallyEquals(left, right) {
-  if (Array.isArray(left) && Array.isArray(right)) {
-    for (var i = 0; i < left.length; i++) {
-      if (!basicallyEquals(left[i], right[i])) {
-        return false;
-      }
-    }
-    return true;
-  }
-  else if (typeof left == 'object' && typeof right == 'object') {
-    for (var key in left) {
-      if (key != 'bgColor') {
-        if (!basicallyEquals(left[key], right[key])) {
-          if (JSON.stringify(left[key]) == "{}" && right[key] == undefined) return true;
-          if (JSON.stringify(right[key]) == "{}" && left[key] == undefined) return true;
-          return false;
-        }
-      }
-    }
-    return true;
-  }
-  else {
-    if (left != right) {
-      return false;
-    }
-    return true;
-  }
-}
-
-
-var workbook, wbout, wbin;
-
-workbook = {
-  "SheetNames": [
-    "Main"
-  ],
-  "Sheets": {
-    "Main": {
-      "!merges": [
-        {
-          "s": {
-            "c": 0,
-            "r": 0
-          },
-          "e": {
-            "c": 2,
-            "r": 0
-          }
-        }
-      ],
-      "A1": {
-        "v": "This is a submerged cell",
-        "s": {
-          "border": {
-            "left": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            },
-            "top": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            },
-            "bottom": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            }
-          }
-        },
-        "t": "s"
-      },
-      "B1": {
-        "v": "Pirate ship",
-        "s": {
-          "border": {
-            "top": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            },
-            "bottom": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            }
-          }
-        },
-        "t": "s"
-      },
-      "C1": {
-        "v": "Sunken treasure",
-        "s": {
-          "border": {
-            "right": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            },
-            "top": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            },
-            "bottom": {
-              "style": "thick",
-              "color": {
-                "auto": 1
-              }
-            }
-          }
-        },
-        "t": "s"
-      },
-      "A2": {
-        "v": "Blank",
-        "t": "s"
-      },
-      "B2": {
-        "v": "Red",
-        "s": {
-          "fill": {
-            "fgColor": {
-              "rgb": "FFFF0000"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "C2": {
-        "v": "Green",
-        "s": {
-          "fill": {
-            "fgColor": {
-              "rgb": "FF00FF00"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "D2": {
-        "v": "Blue",
-        "s": {
-          "fill": {
-            "fgColor": {
-              "rgb": "FF0000FF"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "E2": {
-        "v": "Theme 5",
-        "s": {
-          "fill": {
-            "fgColor": {
-              "theme": 5
-            }
-          }
-        },
-        "t": "s"
-      },
-      "F2": {
-        "v": "Theme 5 Tint -0.5",
-        "s": {
-          "fill": {
-            "fgColor": {
-              "theme": 5,
-              "tint": -0.5
-            }
-          }
-        },
-        "t": "s"
-      },
-      "A3": {
-        "v": "Default",
-        "t": "s"
-      },
-      "B3": {
-        "v": "Arial",
-        "s": {
-          "font": {
-            "name": "Arial",
-            "sz": 24,
-            "color": {
-              "theme": "5"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "C3": {
-        "v": "Times New Roman",
-        "s": {
-          "font": {
-            "name": "Times New Roman",
-            bold: true,
-            underline: true,
-            italic: true,
-            strike: true,
-            outline: true,
-            shadow: true,
-            vertAlign: "superscript",
-            "sz": 16,
-            "color": {
-              "rgb": "FF2222FF"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "D3": {
-        "v": "Courier New",
-        "s": {
-          "font": {
-            "name": "Courier New",
-            "sz": 14
-          }
-        },
-        "t": "s"
-      },
-      "A4": {
-        "v": 0.618033989,
-        "t": "n"
-      },
-      "B4": {
-        "v": 0.618033989,
-        "t": "n"
-      },
-      "C4": {
-        "v": 0.618033989,
-        "t": "n"
-      },
-      "D4": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0.00%"
-        }
-      },
-      "E4": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0.00%",
-          "fill": {
-            "fgColor": {
-              "rgb": "FFFFCC00"
-            }
-          }
-        }
-      },
-      "A5": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0%"
-        }
-      },
-      "B5": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0.0%"
-        }
-      },
-      "C5": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0.00%"
-        }
-      },
-      "D5": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0.000%"
-        }
-      },
-      "E5": {
-        "v": 0.618033989,
-        "t": "n",
-        "s": {
-          "numFmt": "0.0000%"
-        }
-      },
-      "F5": {
-        "v": 0,
-        "t": "n",
-        "s": {
-          "numFmt": "0.00%;\\(0.00%\\);\\-;@",
-          "fill": {
-            "fgColor": {
-              "rgb": "FFFFCC00"
-            }
-          }
-        }
-      },
-      "A6": {
-        "v": "Sat Mar 21 2015 23:47:34 GMT-0400 (EDT)",
-        "t": "s"
-      },
-      "B6": {
-        "v": 42084.99137416667,
-        "t": "n"
-      },
-      "C6": {
-        "v": 42084.99137416667,
-        "s": {
-          "numFmt": "d-mmm-yy"
-        },
-        "t": "n"
-      },
-      "A7": {
-        "v": "left",
-        "s": {
-          "alignment": {
-            "horizontal": "left"
-          }
-        },
-        "t": "s"
-      },
-      "B7": {
-        "v": "center",
-        "s": {
-          "alignment": {
-            "horizontal": "center"
-          }
-        },
-        "t": "s"
-      },
-      "C7": {
-        "v": "right",
-        "s": {
-          "alignment": {
-            "horizontal": "right"
-          }
-        },
-        "t": "s"
-      },
-      "A8": {
-        "v": "vertical",
-        "s": {
-          "alignment": {
-            "vertical": "top"
-          }
-        },
-        "t": "s"
-      },
-      "B8": {
-        "v": "vertical",
-        "s": {
-          "alignment": {
-            "vertical": "center"
-          }
-        },
-        "t": "s"
-      },
-      "C8": {
-        "v": "vertical",
-        "s": {
-          "alignment": {
-            "vertical": "bottom"
-          }
-        },
-        "t": "s"
-      },
-      "A9": {
-        "v": "indent",
-        "s": {
-          "alignment": {
-            "indent": "1"
-          }
-        },
-        "t": "s"
-      },
-      "B9": {
-        "v": "indent",
-        "s": {
-          "alignment": {
-            "indent": "2"
-          }
-        },
-        "t": "s"
-      },
-      "C9": {
-        "v": "indent",
-        "s": {
-          "alignment": {
-            "indent": "3"
-          }
-        },
-        "t": "s"
-      },
-      "A10": {
-        "v": "In publishing and graphic design, lorem ipsum is a filler text commonly used to demonstrate the graphic elements of a document or visual presentation. ",
-        "s": {
-          "alignment": {
-            "wrapText": 1,
-            "horizontal": "right",
-            "vertical": "center",
-            "indent": 1
-          }
-        },
-        "t": "s"
-      },
-      "A11": {
-        "v": 41684.35264774306,
-        "s": {
-          "numFmt": "m/d/yy"
-        },
-        "t": "n"
-      },
-      "B11": {
-        "v": 41684.35264774306,
-        "s": {
-          "numFmt": "d-mmm-yy"
-        },
-        "t": "n"
-      },
-      "C11": {
-        "v": 41684.35264774306,
-        "s": {
-          "numFmt": "h:mm:ss AM/PM"
-        },
-        "t": "n"
-      },
-      "D11": {
-        "v": 42084.99137416667,
-        "s": {
-          "numFmt": "m/d/yy"
-        },
-        "t": "n"
-      },
-      "E11": {
-        "v": 42065.02247239584,
-        "s": {
-          "numFmt": "m/d/yy"
-        },
-        "t": "n"
-      },
-      "F11": {
-        "v": 42084.99137416667,
-        "s": {
-          "numFmt": "m/d/yy h:mm:ss AM/PM"
-        },
-        "t": "n"
-      },
-      "A12": {
-        "v": "Apple",
-        "s": {
-          "border": {
-            "top": {
-              "style": "thin"
-            },
-            "left": {
-              "style": "thin"
-            },
-            "right": {
-              "style": "thin"
-            },
-            "bottom": {
-              "style": "thin"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "C12": {
-        "v": "Apple",
-        "s": {
-          "border": {
-            "diagonalUp": 1,
-            "diagonalDown": 1,
-            "top": {
-              "style": "dashed",
-              "color": {
-                "auto": 1
-              }
-            },
-            "right": {
-              "style": "medium",
-              "color": {
-                "theme": "5"
-              }
-            },
-            "bottom": {
-              "style": "hair",
-              "color": {
-                "theme": 5,
-                "tint": "-0.3"
-              }
-            },
-            "left": {
-              "style": "thin",
-              "color": {
-                "rgb": "FFFFAA00"
-              }
-            },
-            "diagonal": {
-              "style": "dotted",
-              "color": {
-                "auto": 1
-              }
-            }
-          }
-        },
-        "t": "s"
-      },
-      "E12": {
-        "v": "Pear",
-        "s": {
-          "border": {
-            "diagonalUp": 1,
-            "diagonalDown": 1,
-            "top": {
-              "style": "dashed",
-              "color": {
-                "auto": 1
-              }
-            },
-            "right": {
-              "style": "dotted",
-              "color": {
-                "theme": "5"
-              }
-            },
-            "bottom": {
-              "style": "mediumDashed",
-              "color": {
-                "theme": 5,
-                "tint": "-0.3"
-              }
-            },
-            "left": {
-              "style": "double",
-              "color": {
-                "rgb": "FFFFAA00"
-              }
-            },
-            "diagonal": {
-              "style": "hair",
-              "color": {
-                "auto": 1
-              }
-            }
-          }
-        },
-        "t": "s"
-      },
-      "A13": {
-        "v": "Up 90",
-        "s": {
-          "alignment": {
-            "textRotation": 90
-          }
-        },
-        "t": "s"
-      },
-      "B13": {
-        "v": "Up 45",
-        "s": {
-          "alignment": {
-            "textRotation": 45
-          }
-        },
-        "t": "s"
-      },
-      "C13": {
-        "v": "Horizontal",
-        "s": {
-          "alignment": {
-            "textRotation": 0
-          }
-        },
-        "t": "s"
-      },
-      "D13": {
-        "v": "Down 45",
-        "s": {
-          "alignment": {
-            "textRotation": 135
-          }
-        },
-        "t": "s"
-      },
-      "E13": {
-        "v": "Down 90",
-        "s": {
-          "alignment": {
-            "textRotation": 180
-          }
-        },
-        "t": "s"
-      },
-      "F13": {
-        "v": "Vertical",
-        "s": {
-          "alignment": {
-            "textRotation": 255
-          }
-        },
-        "t": "s"
-      },
-      "A14": {
-        "v": "Font color test",
-        "s": {
-          "font": {
-            "color": {
-              "rgb": "FFC6EFCE"
-            }
-          }
-        },
-        "t": "s"
-      },
-      "!ref": "A1:F14"
-    }
-  }
-}
-XLSX.writeFile(workbook, OUTFILE, { defaultCellStyle: defaultCellStyle });
-console.log("open " + OUTFILE)

+ 0 - 74
client/src/excel/index.js

@@ -1,74 +0,0 @@
-import XLSX from 'xlsx'
-import FileSaver from 'file-saver'
-import Blob from 'blob'
-
-function datenum (v, date1904) {
-  if (date1904) v += 1462
-  var epoch = Date.parse(v)
-  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000)
-}
-
-function SheetFromArray (data, opts) {
-  var ws = {}
-  var range = {s: {c: 10000000, r: 10000000}, e: {c: 0, r: 0 }}
-  for (var R = 0; R !== data.length; ++R) {
-    for (var C = 0; C !== data[R].length; ++C) {
-      if (range.s.r > R) range.s.r = R
-      if (range.s.c > C) range.s.c = C
-      if (range.e.r < R) range.e.r = R
-      if (range.e.c < C) range.e.c = C
-      var cell = {v: data[R][C] }
-      if (cell.v == null) continue
-      var cell_ref = XLSX.utils.encode_cell({c: C, r: R})
-
-      if (typeof cell.v === 'number') cell.t = 'n'
-      else if (typeof cell.v === 'boolean') cell.t = 'b'
-      else if (cell.v instanceof Date) {
-        cell.t = 'n'; cell.z = XLSX.SSF._table[14]
-        cell.v = datenum(cell.v)
-      } else cell.t = 's'
-
-      ws[cell_ref] = cell
-    }
-  }
-  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range)
-  return ws
-}
-
-function Workbook () {
-  if (!(this instanceof Workbook)) return new Workbook()
-  this.SheetNames = []
-  this.Sheets = {}
-}
-
-function s2ab (s) {
-  var buf = new ArrayBuffer(s.length)
-  var view = new Uint8Array(buf)
-  for (var i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF
-  return buf
-}
-
-function saveAs (workbook, filename) {
-  const wopts = {bookType: 'xlsx', bookSST: false, type: 'binary'}
-  const wbout = XLSX.write(workbook, wopts)
-  FileSaver.saveAs(new Blob([s2ab(wbout)], {type: ''}), filename)
-}
-
-function readWorkbook (file) {
-  return new Promise((resolve, reject) => {
-    const reader = new FileReader()
-    reader.onload = (e) => {
-      const data = e.target.result
-      const workbook = XLSX.read(data, {type: 'binary', cellDates: true})
-      console.log('Workbook after read:', workbook)
-      const worksheets = {}
-      Object.keys(workbook.Sheets).forEach(sheetName => {
-        worksheets[sheetName] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { header: 1, raw: true })
-      })
-      resolve(worksheets)
-    }
-    reader.readAsBinaryString(file)
-  })
-}
-
-export default { Workbook, SheetFromArray, saveAs, readWorkbook }

+ 20 - 27
client/src/index.js

@@ -9,21 +9,20 @@ import createSagaMiddleware from 'redux-saga'
 import { all } from 'redux-saga/effects'
 
 // React router
-import { Route, Switch, Link } from 'react-router'
 import { createBrowserHistory } from 'history'
 import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router'
-
+ 
 // Import the main app
 import Main from './Main'
 
 // Import the submodules
-import playerList from './playerList'
-import calendar from './calendar'
+import matches from './matches'
 import layout from './layout'
-import scraper from './scraper'
 import alerts from './alerts'
 import users from './users'
 import config from './config'
+import sms from './sms'
+import swisstennis from './swisstennis'
 
 /** 
  * Browser History
@@ -37,25 +36,25 @@ console.log('history:', history)
 
 /** The root reducer is combined from all sub-module reducers */
 const rootReducer = combineReducers({
-  playerList: playerList.reducer,
-  calendar: calendar.reducer,
+  matches: matches.reducer,
   layout: layout.reducer,
-  scraper: scraper.reducer,
   alerts: alerts.reducer,
   users: users.reducer,
-  config: config.reducer
+  config: config.reducer,
+  swisstennis: swisstennis.reducer,
+  sms: sms.reducer
 })
 console.log('Root reducer:', rootReducer)
 
 /** The default state is combined from all sub-module states */
 const defaultState = {
-  playerList: playerList.state,
-  calendar: calendar.state,
+  matches: matches.state,
   layout: layout.state,
-  scraper: scraper.state,
   alerts: alerts.state,
   users: users.state,
-  config: config.state
+  config: config.state,
+  swisstennis: swisstennis.state,
+  sms: sms.state
 }
 console.log('Default state:', defaultState)
 
@@ -63,13 +62,13 @@ console.log('Default state:', defaultState)
 function * rootSaga () {
   console.log('rootSaga called')
   yield all([
-    playerList.saga(),
-    calendar.saga(),
+    matches.saga(),
     layout.saga(),
-    scraper.saga(),
     alerts.saga(),
     users.saga(),
-    config.saga()
+    config.saga(),
+    swisstennis.saga(),
+    sms.saga()
   ])
 }
 
@@ -127,13 +126,13 @@ sagaMiddleware.run(rootSaga)
 
 /** Collect the action creators from all modules in actionCreators */
 const actionCreators = {
-  playerList: playerList.actions,
-  calendar: calendar.actions,
+  matches: matches.actions,
   layout: layout.actions,
-  scraper: scraper.actions,
   alerts: alerts.actions,
   users: users.actions,
-  config: config.actions
+  config: config.actions,
+  swisstennis: swisstennis.actions,
+  sms: sms.actions
 }
 
 /** Creates a function  */
@@ -171,12 +170,6 @@ const router = (
   </Provider>
 )
 
-const provider = (
-  <Provider store={store}>
-    <App />
-  </Provider>
-)
-
 /**
  * Render the app
  **/

+ 0 - 131
client/src/macros/SZTM_Spielliste.bas

@@ -1,131 +0,0 @@
-Attribute VB_Name = "Module2"
-Sub SZTM_Spielliste()
-Attribute SZTM_Spielliste.VB_Description = "Formatiert die Spielliste"
-Attribute SZTM_Spielliste.VB_ProcData.VB_Invoke_Func = "S\n14"
-'
-' SZTM_Spielliste Macro
-' Formatiert die Spielliste
-'
-' Keyboard Shortcut: Ctrl+Shift+X
-'
-
-
-    Dim ws As Worksheet
-    For Each ws In ActiveWorkbook.Sheets
-        With ws
-        
-            ws.Activate
-            ws.PageSetup.Orientation = xlLandscape
-            
-            'Select first row, change font size, make bold and merge
-            Range("A1:K1").Select
-            With Selection
-                .HorizontalAlignment = xlLeft
-                .VerticalAlignment = xlBottom
-                .WrapText = False
-                .Orientation = 0
-                .AddIndent = False
-                .IndentLevel = 0
-                .ShrinkToFit = False
-                .ReadingOrder = xlContext
-                .MergeCells = True
-            End With
-            Selection.Merge
-            With Selection.Font
-                .Name = "Calibri"
-                .Size = 14
-                .Strikethrough = False
-                .Superscript = False
-                .Subscript = False
-                .OutlineFont = False
-                .Shadow = False
-                .Underline = xlUnderlineStyleNone
-                .ThemeColor = xlThemeColorLight1
-                .TintAndShade = 0
-                .ThemeFont = xlThemeFontMinor
-            End With
-            Selection.Font.Bold = True
-            
-            'Select second row, make bold and merge
-            Range("A2:K2").Select
-            With Selection
-                .HorizontalAlignment = xlLeft
-                .VerticalAlignment = xlBottom
-                .WrapText = False
-                .Orientation = 0
-                .AddIndent = False
-                .IndentLevel = 0
-                .ShrinkToFit = False
-                .ReadingOrder = xlContext
-                .MergeCells = True
-            End With
-            Selection.Merge
-            Selection.Font.Bold = True
-                        
-            'Select all data cells, activate grid, fit width, increase height and add header.
-            Range("A4:K4").Select
-            With Selection.Interior
-                .Pattern = xlSolid
-                .PatternColorIndex = xlAutomatic
-                .ThemeColor = xlThemeColorDark1
-                .TintAndShade = -0.149998474074526
-                .PatternTintAndShade = 0
-            End With
-            Selection.Font.Bold = True
-            ActiveWindow.SmallScroll Down:=-3
-            Range("A4").Select
-            Range(Selection, ActiveCell.SpecialCells(xlLastCell)).Select
-            Selection.Borders(xlDiagonalDown).LineStyle = xlNone
-            Selection.Borders(xlDiagonalUp).LineStyle = xlNone
-            With Selection.Borders(xlEdgeLeft)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeTop)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeBottom)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeRight)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlInsideVertical)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlInsideHorizontal)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            Selection.RowHeight = 24
-            'Selection.EntireColumn.AutoFit
-            Columns("A:B").ColumnWidth = 5.63
-            Columns("C:C").ColumnWidth = 11.88
-            Columns("D:D").ColumnWidth = 20.63
-            Columns("E:E").ColumnWidth = 7.5
-            Columns("F:F").ColumnWidth = 20.63
-            Columns("G:G").ColumnWidth = 7.5
-            Columns("H:J").ColumnWidth = 6.25
-            Columns("K:K").ColumnWidth = 15
-            Rows(4).RowHeight = 15.75
-            
-        End With
-    Next
-
-End Sub

+ 0 - 202
client/src/macros/SZTM_Zahlliste.bas

@@ -1,202 +0,0 @@
-Attribute VB_Name = "Module1"
-Sub SZTM_Zahlliste()
-Attribute SZTM_Zahlliste.VB_Description = "Formatiert die Zahlliste"
-Attribute SZTM_Zahlliste.VB_ProcData.VB_Invoke_Func = "Z\n14"
-'
-' SZTM_Zahlliste Macro
-' Formatiert die Zahlliste
-'
-' Keyboard Shortcut: Ctrl+Shift+F
-'
-    Dim ws As Worksheet
-    For Each ws In ActiveWorkbook.Sheets
-        With ws
-        
-            ws.Activate
-            
-            'Select first row, change font size, make bold and merge
-            Range("A1:F1").Select
-            With Selection
-                .HorizontalAlignment = xlLeft
-                .VerticalAlignment = xlBottom
-                .WrapText = False
-                .Orientation = 0
-                .AddIndent = False
-                .IndentLevel = 0
-                .ShrinkToFit = False
-                .ReadingOrder = xlContext
-                .MergeCells = True
-            End With
-            Selection.Merge
-            With Selection.Font
-                .Name = "Calibri"
-                .Size = 14
-                .Strikethrough = False
-                .Superscript = False
-                .Subscript = False
-                .OutlineFont = False
-                .Shadow = False
-                .Underline = xlUnderlineStyleNone
-                .ThemeColor = xlThemeColorLight1
-                .TintAndShade = 0
-                .ThemeFont = xlThemeFontMinor
-            End With
-            Selection.Font.Bold = True
-            
-            'Select second row, make bold and merge
-            Range("A2:F2").Select
-            With Selection
-                .HorizontalAlignment = xlLeft
-                .VerticalAlignment = xlBottom
-                .WrapText = False
-                .Orientation = 0
-                .AddIndent = False
-                .IndentLevel = 0
-                .ShrinkToFit = False
-                .ReadingOrder = xlContext
-                .MergeCells = True
-            End With
-            Selection.Merge
-            Selection.Font.Bold = True
-            
-            'Select price cells and merge
-            Range("C4:F4").Select
-            Selection.Merge
-            
-            'Select last cell
-            ActiveCell.SpecialCells(xlLastCell).Select
-            Selection.RowHeight = 27.75
-            Selection.Offset(0, -2).Select
-            Selection.Resize(Selection.Rows.Count + 1, Selection.Columns.Count + 2).Select
-            Selection.Merge
-            
-            Selection.Offset(0, -3).Select
-            Selection.Resize(Selection.Rows.Count + 1, Selection.Columns.Count + 2).Select
-            Selection.Merge
-            
-            Selection.Offset(-1, 0).Select
-            Selection.Font.Bold = True
-            Selection.Offset(0, 1).Select
-            Selection.Resize(Selection.Rows.Count, Selection.Columns.Count + 1).Select
-            Selection.Merge
-            Selection.Offset(0, 1).Select
-            Selection.Font.Bold = True
-            Selection.Offset(0, 1).Select
-            Selection.Resize(Selection.Rows.Count, Selection.Columns.Count + 1).Select
-            Selection.Merge
-            
-            Selection.Offset(-1, -4).Select
-            Selection.Font.Bold = True
-            Selection.Offset(0, 1).Select
-            Selection.Resize(Selection.Rows.Count, Selection.Columns.Count + 4).Select
-            Selection.Merge
-            
-            Selection.Offset(0, -1).Select
-            Range(Selection, ActiveCell.SpecialCells(xlLastCell)).Select
-            Selection.Resize(Selection.Rows.Count - 1, Selection.Columns.Count).Select
-            Selection.Borders(xlDiagonalDown).LineStyle = xlNone
-            Selection.Borders(xlDiagonalUp).LineStyle = xlNone
-            With Selection.Borders(xlEdgeLeft)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeTop)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeBottom)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeRight)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlInsideVertical)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlInsideHorizontal)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            
-                
-            'Select all data cells, activate grid, fit width, increase height and add header.
-            Range("A6:F6").Select
-            With Selection.Interior
-                .Pattern = xlSolid
-                .PatternColorIndex = xlAutomatic
-                .ThemeColor = xlThemeColorDark1
-                .TintAndShade = -0.149998474074526
-                .PatternTintAndShade = 0
-            End With
-            Selection.Font.Bold = True
-            ActiveWindow.SmallScroll Down:=-3
-            Range("A6").Select
-            Range(Selection, ActiveCell.SpecialCells(xlLastCell)).Select
-            Selection.Resize(Selection.Rows.Count - 6, Selection.Columns.Count).Select
-            Selection.Borders(xlDiagonalDown).LineStyle = xlNone
-            Selection.Borders(xlDiagonalUp).LineStyle = xlNone
-            With Selection.Borders(xlEdgeLeft)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeTop)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeBottom)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlEdgeRight)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlInsideVertical)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            With Selection.Borders(xlInsideHorizontal)
-                .LineStyle = xlContinuous
-                .ColorIndex = 0
-                .TintAndShade = 0
-                .Weight = xlThin
-            End With
-            Selection.RowHeight = 24
-            'Selection.EntireColumn.AutoFit
-            Columns("A:A").ColumnWidth = 11
-            Columns("B:B").ColumnWidth = 11.88
-            Columns("C:C").ColumnWidth = 5.63
-            Columns("D:D").ColumnWidth = 30
-            Columns("E:E").ColumnWidth = 9.38
-            Columns("F:F").ColumnWidth = 8
-            Rows(6).RowHeight = 15.75
-            
-        End With
-    Next
-
-End Sub

+ 92 - 0
client/src/matches/components/Matches.js

@@ -0,0 +1,92 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+import moment from 'moment'
+
+class Matches extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+  }
+
+  componentDidMount () {
+    console.log('Matches did mount', this)
+    const { matchesGetMatchesRequest } = this.props.matchesActions
+    matchesGetMatchesRequest()
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+    const { matchesSetFilter } = this.props.matchesActions
+    const nextFilter = {
+      category: this.category.value,
+      date: this.date.value,
+      time: this.time.value,
+      place: this.place.value,
+      player: this.player.value,
+      result: this.result.value
+    }
+    matchesSetFilter(nextFilter)
+  }
+
+  render () {
+    const state = this.props.matches
+    const actions = this.props.matchesActions
+    const { setRecipients } = this.props.smsActions
+    const { matches, filteredMatches, filter } = state
+    const participatingPlayers = []
+    filteredMatches.forEach(match => {
+      if (match.player1) participatingPlayers.push(match.player1)
+      if (match.player2) participatingPlayers.push(match.player2)
+    })
+
+    return (
+      <div>
+        <p>{filteredMatches.length}/{matches.length} Spiele, {participatingPlayers.length} Spieler > <Link to="/sms" onClick={() => setRecipients(participatingPlayers)}>SMS</Link></p>
+        <form>
+          <table className='table table-bordered table-striped'>
+            <thead>
+              <tr>
+                <th>Kategorie</th><th>Datum</th><th>Zeit</th><th>Ort</th><th>Spieler 1</th><th>Spieler 2</th><th>Resultat</th>
+              </tr>
+              <tr>
+                <td>
+                  <input type="input" ref={(input) => {this.category = input}} id="category" value={filter.category} placeholder="Kategorie" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                  <input type="input" ref={(input) => {this.date = input}} id="date" value={filter.date} placeholder="Datum" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                  <input type="input" ref={(input) => {this.time = input}} id="time" value={filter.time} placeholder="Zeit" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                  <input type="input" ref={(input) => {this.place = input}} id="place" value={filter.place} placeholder="Ort" onChange={this.handleChange}></input>
+                </td>
+                <td colSpan={2}>
+                  <input type="input" ref={(input) => {this.player = input}} id="player" value={filter.player} placeholder="Spieler" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                  <input type="input" ref={(input) => {this.result = input}} id="result" value={filter.result} placeholder="Resultat" onChange={this.handleChange}></input>
+                </td>
+              </tr>
+            </thead>
+            <tbody>
+              {filteredMatches.map((match, key) => 
+              <tr key={key}>
+                <td>{match.category}</td>
+                <td>{moment(match.date).format('DD.MM.YYYY')}</td>
+                <td>{moment(match.date).format('HH:mm')}</td>
+                <td>{match.place}</td>
+                <td>{match.player1 ? match.player1.fullName : ''}</td>
+                <td>{match.player2 ? match.player2.fullName : ''}</td>
+                <td>{match.result}</td>
+              </tr>
+              )}
+            </tbody>
+          </table>
+        </form>
+      </div>
+    )
+  }
+}
+
+export default Matches

+ 84 - 0
client/src/matches/components/Players.js

@@ -0,0 +1,84 @@
+import React from 'react'
+import moment from 'moment'
+
+class Players extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+  }
+
+  componentDidMount () {
+    console.log('Players did mount', this)
+    const { matchesGetPlayersRequest } = this.props.matchesActions
+    matchesGetPlayersRequest()
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+    const { matchesSetFilter } = this.props.matchesActions
+    const nextFilter = {
+      category: this.category.value,
+      player: this.player.value,
+      junior: this.junior.checked,
+      paid: this.paid.checked,
+      matchfilter: this.matchfilter.checked
+    }
+    matchesSetFilter(nextFilter)
+  }
+
+  render () {
+    const state = this.props.matches
+    console.log('sali', state)
+    const actions = this.props.matchesActions
+    const { players, filteredPlayers, filter } = state
+
+    return (
+      <div>
+        <form>
+          <p>{filteredPlayers.length}/{players.length} Spieler, {}/{} Kategorien, {}/{} Daten, {}/{} Zeiten, {}/{} Orte</p>
+          <label for="matchfilter">Match Filter benutzen</label>
+          <input type="checkbox" ref={(input) => {this.matchfilter = input}} id="matchfilter" checked={filter.matchfilter} onChange={this.handleChange} />
+          <table className='table table-bordered table-striped'>
+            <thead>
+              <tr>
+                <th>Name</th><th>Kategorie</th><th>Telefon</th><th>E-Mail</th><th>Junior</th><th>Bezahlt</th>
+              </tr>
+              <tr>
+                <td>
+                  <input type="input" ref={(input) => {this.player = input}} id="player" value={filter.player} placeholder="Name" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                  <input type="input" ref={(input) => {this.category = input}} id="category" value={filter.category} placeholder="Datum" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                </td>
+                <td>
+                </td>
+                <td>
+                  <input type="checkbox" ref={(input) => {this.junior = input}} id="junior" checked={filter.junior} placeholder="Spieler" onChange={this.handleChange}></input>
+                </td>
+                <td>
+                  <input type="checkbox" ref={(input) => {this.paid = input}} id="paid" checked={filter.paid} placeholder="Resultat" onChange={this.handleChange}></input>
+                </td>
+              </tr>
+            </thead>
+            <tbody>
+              {filteredPlayers.map((player, key) => 
+              <tr key={key}>
+                <td>{player.fullName}</td>
+                <td>{player.category}</td>
+                <td>{player.phone}</td>
+                <td>{player.email}</td>
+                <td>{player.junior ? '\u2713' : '\u2717'}</td>
+                <td>{player.paid ? '\u2713' : '\u2717'}</td>
+              </tr>
+              )}
+            </tbody>
+          </table>
+        </form>
+      </div>
+    )
+  }
+}
+
+export default Players

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

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

+ 0 - 0
client/src/scraper/index.js → client/src/matches/index.js


+ 204 - 0
client/src/matches/state.js

@@ -0,0 +1,204 @@
+/** @module matches/state */
+import { call, put, takeEvery, select } from 'redux-saga/effects'
+import moment from 'moment'
+import { SZTM_API } from '../config/static'
+
+/**
+ * state.js
+ *
+ * Collection of everything which has to do with state changes.
+ **/
+
+/** actionTypes define what actions are handeled by the reducer. */
+export const actions = {
+  matchesSetFilter: (filter) => {
+    return {
+      type: 'MATCHES/SET_FILTER',
+      filter
+    }
+  },
+  matchesFilterApplied: (filteredMatches, filteredPlayers) => {
+    return {
+      type: 'MATCHES/FILTER_APPLIED',
+      filteredMatches, 
+      filteredPlayers
+    }
+  },
+  matchesGetMatchesRequest: (data) => {
+    return {
+      type: 'MATCHES/GET_MATCHES_REQUEST',
+      data
+    }
+  },
+  matchesGetMatchesSuccess: (data) => {
+    return {
+      type: 'MATCHES/GET_MATCHES_SUCCESS',
+      data
+    }
+  },
+  matchesGetMatchesFailure: (error) => {
+    return {
+      type: 'MATCHES/GET_MATCHES_FAILURE',
+      error
+    }
+  },
+  matchesGetPlayersRequest: (data) => {
+    return {
+      type: 'MATCHES/GET_PLAYERS_REQUEST',
+      data
+    }
+  },
+  matchesGetPlayersSuccess: (data) => {
+    return {
+      type: 'MATCHES/GET_PLAYERS_SUCCESS',
+      data
+    }
+  },
+  matchesGetPlayersFailure: (error) => {
+    return {
+      type: 'MATCHES/GET_PLAYERS_FAILURE',
+      error
+    }
+  },
+}
+console.log('State actions', actions)
+const emptyFilter = {
+  category: '',
+  date: '',
+  time: '',
+  place: '',
+  player: '',
+  result: '',
+  junior: '',
+  paid: '',
+  matchfilter: '',
+}
+
+/** state definition */
+export const state = {
+  matches: [],
+  players: [],
+  filteredMatches: [],
+  filteredPlayers: [],
+  filter: emptyFilter,
+  stats: {},
+  filteredStats: {},
+  matchGetRequested: 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 'MATCHES/SET_FILTER':
+      return { ...state, filter: { ...state.filter, ...action.filter } }
+    case 'MATCHES/FILTER_APPLIED':
+      return { ...state, filteredMatches: action.filteredMatches, filteredPlayers: action.filteredPlayers }
+    case 'MATCHES/GET_MATCHES_REQUEST':
+      return { ...state, matchGetRequested: true }
+    case 'MATCHES/GET_MATCHES_SUCCESS':
+      return { ...state, matches: action.data, matchGetRequested: false }
+    case 'MATCHES/GET_MATCHES_FAILURE':
+      return { ...state, matchGetRequested: false }
+    case 'MATCHES/GET_PLAYERS_REQUEST':
+      return { ...state, matchGetRequested: true }
+    case 'MATCHES/GET_PLAYERS_SUCCESS':
+      return { ...state, players: action.data, matchGetRequested: false }
+    case 'MATCHES/GET_PLAYERS_FAILURE':
+      return { ...state, matchGetRequested: false }
+    default:
+      return state
+  }
+}
+
+function * getMatches (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Get matches requested', action, token)
+    const { data } = action
+    
+    const queryParams = Object.keys(data || {})
+      .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
+      .join('&')
+    const response = yield call(fetch, `${SZTM_API}/api/sztm/schedule?${queryParams}`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        'x-access-token': token
+      }
+    })
+    if (response.status != 200) {
+      throw Error(yield response.json())
+    } else {
+      const responseJson = yield response.json()
+      console.log(responseJson)
+      yield put(actions.matchesGetMatchesSuccess(responseJson.matches))
+      yield put(actions.matchesSetFilter({}))
+    }
+  } catch (error) {
+    console.log('Matches failure!', actions.matchesGetMatchesFailure(error))
+    yield put(actions.matchesGetMatchesFailure(error))
+  }
+}
+
+function * getPlayers (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Get players requested', action, token)
+    const { data } = action
+    
+    const queryParams = Object.keys(data || {})
+      .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(data[k]))
+      .join('&')
+    const response = yield call(fetch, `${SZTM_API}/api/sztm/players?${queryParams}`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        'x-access-token': token
+      }
+    })
+    if (response.status != 200) {
+      throw Error(response.status)
+    } else {
+      const responseJson = yield response.json()
+      console.log(responseJson)
+      yield put(actions.matchesGetPlayersSuccess(responseJson.players))
+      yield put(actions.matchesSetFilter({}))
+    }
+  } catch (error) {
+    console.log('Players failure!', actions.matchesGetPlayersFailure(error))
+    yield put(actions.matchesGetPlayersFailure(error))
+  }
+}
+
+function * setFilter (action) {
+  const state = yield select()
+  const { matches, players, filter } = state.matches
+  const filteredMatches = matches.filter(match => {
+    return (
+      (filter.category ? match.category.includes(filter.category) : true) && 
+      (filter.place ? match.place.includes(filter.place) : true) && 
+      (filter.date ? moment(match.date).format('DD.MM.YYYY').includes(filter.date) : true) &&
+      (filter.time ? moment(match.date).format('HH:mm').includes(filter.time) : true) &&
+      (filter.player ? 
+        (match.player1 && match.player1.fullName.includes(filter.player)) || (match.player2 && match.player2.fullName.includes(filter.player)) :
+        true)
+    )
+  })
+
+  const filteredPlayers = players.filter(player => {
+    return (
+      (filter.category ? player.category.includes(filter.category) : true)
+    )
+  })
+  
+  yield put(actions.matchesFilterApplied(filteredMatches, filteredPlayers))
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {
+  console.log('Config saga started.')
+  yield takeEvery('MATCHES/GET_MATCHES_REQUEST', getMatches)
+  yield takeEvery('MATCHES/GET_PLAYERS_REQUEST', getPlayers)
+  yield takeEvery('MATCHES/SET_FILTER', setFilter)
+}

+ 0 - 167
client/src/module/container.js

@@ -1,167 +0,0 @@
-/** @module Container */
-
-// Import dependencies
-import React from 'react'
-// import { createStore, applyMiddleware, combineReducers, bindActionCreators, compose } from 'redux'
-// import { Provider, connect } from 'react-redux'
-import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
-import createSagaMiddleware from 'redux-saga'
-import { all } from 'redux-saga/effects'
-import { browserHistory, Router, Route, IndexRoute } from 'react-router'
-
-class Container {
-  constructor () {
-    this.states = {}
-    this.actionCreators = {}
-    this.reducers = {}
-    this.routes = []
-    this.sagas = []
-    this.middlewares = []
-  }
-
-  addModule (module) {
-    if (typeof module !== 'object') {
-      throw Error('Modules must be an object.')
-    }
-    if (!module.name || typeof module.name !== 'string') {
-      throw Error('Module object must have a name property')
-    }
-    const name = module.name
-    if (module.state) {
-      this.states[name] = module.state
-    }
-    if (module.reducer) {
-      this.reducers[name] = module.reducer
-    }
-    if (module.actionCreators) {
-      this.actionCreators[name] = module.actionCreators
-    }
-    if (module.route) {
-      this.routes.push(module.route)
-    }
-    if (module.saga) {
-      this.sagas.push(module.saga)
-    }
-    if (module.middleware) {
-      this.middlewares.push(module.middleware)
-    }
-  }
-
-  createApp (appClass) {
-    const withRedux = (this.reducers.length > 0 || this.actionCreators.length > 0 || this.states.length > 0)
-    const withRouter = (this.routes.length > 0)
-    const withSagas = (this.sagas.length > 0)
-    const withMiddleware = (this.middlewares.length > 0)
-
-    /** Check for available enhancers and combine all middleware */
-    const enhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
-      ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__(
-        applyMiddleware(...this.middleware))
-      : compose(applyMiddleware(...this.middleware))
-
-    // https://github.com/reactjs/redux/issues/1287
-    if (withRouter) {
-      const { BrowserRouter } = require('react-router')
-      const Root = () => (
-        <BrowserRouter />
-      )
-    }
-    if (withRedux || withRouter) {
-      const { createStore, applyMiddleware, combineReducers, bindActionCreators, compose } = require('redux')
-      const { Provider, connect } = require('react-redux')
-
-      /** The default state is combined from all sub-module states */
-      const defaultState = { ...this.states }
-      /** The root reducer is combined from all sub-module reducers */
-      const rootReducer = combineReducers({ ...this.reducers, routing: routerReducer })
-      /** The action creators of all sub-modules are combined */
-      const actionCreators = { ...this.actionCreators }
-      const store = createStore(rootReducer, defaultState, enhancer)
-      const history = syncHistoryWithStore(browserHistory, store)
-    } else if (withRedux) {
-
-    }
-
-    /** The root saga, calling all other sagas */
-    function * rootSaga () {
-      yield all(this.sagas.map(saga => saga()))
-    }
-    if (withSagas) {
-      const sagaMiddleware = createSagaMiddleware()
-      this.middlewares.push(sagaMiddleware)
-      sagaMiddleware.run(rootSaga)
-    }
-
-    function mapStateToProps (state) {
-      const propState = {}
-      Object.keys(state).forEach(key => {
-        propState[key] = state[key]
-      })
-      return propState
-    }
-
-    const mapDispatchToProps = function (dispatch) {
-      const boundActionCreators = {}
-      Object.keys(actionCreators).forEach(key => {
-        boundActionCreators[`${key}Actions`] = bindActionCreators(actionCreators[key], dispatch)
-      })
-      return boundActionCreators
-    }
-    const App = connect(mapStateToProps, mapDispatchToProps)(appClass)
-
-    let provider
-    if (withRouter) {
-      provider = (
-        <Provider store={store}>
-          <Router history={history}>
-            <Route component={App}>
-              <Route path='/'>
-                <IndexRoute component={demo_module.components.DemoModule} />
-                {this.routes.map(route => route)}
-              </Route>
-            </Route>
-          </Router>
-        </Provider>
-      )
-    } else {
-      provider = (
-        <Provider store={store}>
-          <App />
-        </Provider>
-      )
-    }
-    return provider
-  }
-}
-
-export default Container
-
-/** react-router is not used in this project.
-*/
-
-/**
- * Redux Section
- */
-/** The enhancer allows to use Redux development tools in Chrome */
-// see: https://github.com/zalmoxisus/redux-devtools-extension/issues/220
-
-/** Build the Redux store from the rootReducer, the defualtState and the enhancers. */
-/** react-route is not used in this project.
-
-*/
-
-/** Collect the action creators from all modules in actionCreators */
-
-/** Creates a function  */
-
-/**
- * React-Router Section
- **/
-
-/** Combine the routes from all modules.
-
-*/
-
-/**
- * Render the app
- **/

+ 0 - 5
client/src/module/index.js

@@ -1,5 +0,0 @@
-import Module from './module'
-import Container from './container'
-
-export const objects = { Module, Container }
-export default objects

+ 0 - 34
client/src/module/module.js

@@ -1,34 +0,0 @@
-class Module {
-  /** e.g. Module userProfile */
-  constructor (name) {
-    this.name = name
-  }
-
-  /** e.g. Upload the avatar file */
-  addAsyncState (name, defaultState, asyncFunction) {
-    if (!this.state) {
-      this.state = {}
-    }
-    this.state[name] = defaultState
-    if (!this.actionCreators) {
-      this.actionCreators = {}
-    }
-    ['init', 'success', 'failure'].forEach(state => {
-      const actionType = `${this.name}_${name}_${state}`.toUpperCase()
-      const actionCreatorName = `${this.name}${name[0].toUpperCase() + name.substring(1)}${state[0].toUpperCase() + state.substring(1)}`
-      this.actionCreators[actionCreatorName] = args => {
-        return {
-          type: actionType,
-          ...args
-        }
-      }
-      const reducerLogic = action => {}
-    })
-  }
-
-  /**  */
-  addDatabaseState () {}
-
-  /**  */
-  addLocalstorageState () {}
-}

+ 0 - 14
client/src/playerList/components/PlayerActions.js

@@ -1,14 +0,0 @@
-import React from 'react'
-
-class PlayerActions extends React.Component {
-  render () {
-    return (
-      <ul>
-        <li><a>E-Mail</a></li>
-        <li><a>SMS</a></li>
-      </ul>
-    )
-  }
-}
-
-export default PlayerActions

+ 0 - 43
client/src/playerList/components/PlayerFilter.js

@@ -1,43 +0,0 @@
-import React from 'react'
-import { FormGroup, ControlLabel, FormControl, HelpBlock, Checkbox } from 'react-bootstrap'
-
-function FieldGroup ({ id, label, help, ...props }) {
-  return (
-    <FormGroup controlId={id}>
-      <ControlLabel>{label}</ControlLabel>
-      <FormControl {...props} />
-      {help && <HelpBlock>{help}</HelpBlock>}
-    </FormGroup>
-  )
-}
-
-class PlayerFilter extends React.Component {
-  render () {
-    return (
-      <form>
-        <FieldGroup
-          id='filterName'
-          type='text'
-          label='Name'
-        />
-        <FormGroup>
-          <Checkbox inline>
-            Junior
-          </Checkbox>
-          <Checkbox inline>
-            Bezahlt
-          </Checkbox>
-        </FormGroup>
-        <FormGroup controlId='filterCategory'>
-          <ControlLabel>Konkurrenz</ControlLabel>
-          <FormControl componentClass='select' placeholder='Konkurrenz'>
-            <option value='WS 45+'>WS 45+</option>
-            <option value='MS 18&U'>MS 18&U</option>
-          </FormControl>
-        </FormGroup>
-      </form>
-    )
-  }
-}
-
-export default PlayerFilter

+ 0 - 61
client/src/playerList/components/PlayerForm.js

@@ -1,61 +0,0 @@
-import React from 'react'
-import { FormGroup, ControlLabel, FormControl, HelpBlock } from 'react-bootstrap'
-import { fileSize, date2s, time2s } from '../../helpers'
-
-function FieldGroup ({ id, label, help, file, ...props }) {
-  return (
-    <FormGroup controlId={id}>
-      <ControlLabel>{label}</ControlLabel>
-      <FormControl {...props} />
-      {help && <HelpBlock>{help}</HelpBlock>}
-    </FormGroup>
-  )
-}
-
-class PlayerForm extends React.Component {
-  constructor () {
-    super()
-    this.handleFileUpload = this.handleFileUpload.bind(this)
-  }
-
-  handleFileUpload (event) {
-    event.preventDefault()
-    const { fileUploadStart } = this.props.actions
-    const { files } = this.playerListFile
-    // if (files.length === 0) {
-    //   alertAdd({ type: 'info', text: 'Datei entfernt' })
-    //   return
-    // }
-    // if (files.length > 1) {
-    //   alertAdd({ type: 'warning', text: 'Mehrere Dateien gesendet. Nur die erste wird verarbeitet.' })
-    // }
-    const file = files[0]
-    fileUploadStart(file)
-  }
-
-  render () {
-    const { fileUpload, file, allPlayers } = this.props.state
-
-    return (
-      <div>
-        <form>
-          <FieldGroup
-            id='playerListFile'
-            label='PlayerList.xls File'
-            type='file'
-            file={file}
-            inputRef={input => { this.playerListFile = input }}
-            onChange={this.handleFileUpload}
-            disabled={(fileUpload === 'started')}
-          />
-        </form>
-        <div>{file
-          ? `${fileSize(file.size)} ${date2s(file.lastModified)} ${time2s(file.lastModified)} ${allPlayers.length} Spieler`
-          : 'Datei noch nicht geladen...'}
-        </div>
-      </div>
-    )
-  }
-}
-
-export default PlayerForm

+ 0 - 20
client/src/playerList/components/PlayerList.js

@@ -1,20 +0,0 @@
-import React from 'react'
-import PlayerActions from './PlayerActions'
-import PlayerTable from './PlayerTable'
-import PlayerFilter from './PlayerFilter'
-
-class PlayerList extends React.Component {
-  render () {
-    const { state, actions } = this.props
-    return (
-      <div>
-        <h1>Spielerliste</h1>
-        <PlayerActions state={state} actions={actions} />
-        <PlayerFilter state={state} actions={actions} />
-        <PlayerTable state={state} actions={actions} />
-      </div>
-    )
-  }
-}
-
-export default PlayerList

+ 0 - 55
client/src/playerList/components/PlayerTable.js

@@ -1,55 +0,0 @@
-import React from 'react'
-import { date2s, time2s } from '../../helpers'
-
-class PlayerRow extends React.Component {
-  render () {
-    const player = this.props.player
-    return (
-      <tr>
-        <td>{player.Konkurrenz}</td>
-        <td>{player.Name}</td>
-        <td>{player.Vorname}</td>
-        <td>{player.NameDP}</td>
-        <td>{player.VornameDP}</td>
-        <td>{player.Bezahlt ? 'Ja' : 'Nein'}</td>
-        <td>{player.BezahltAm ? `${date2s(player.BezahltAm)} ${time2s(player.BezahltAm)}` : 'Unbekannt'}</td>
-        <td>{player.isJunior ? 'Junior' : ''}</td>
-        <td>{player.isJuniorDP ? 'DP Junior' : ''}</td>
-      </tr>
-    )
-  }
-}
-
-class PlayerTable extends React.Component {
-  render () {
-    const { allPlayers, filteredPlayers } = this.props.state || { allPlayers: [], filteredPlayers: [] }
-
-    return (
-      <div>
-        <h2>Spielerliste ({filteredPlayers.length}/{allPlayers.length})</h2>
-        <table className='table table-bordered table-striped'>
-          <thead>
-            <tr>
-              <th>Konkurrenz</th>
-              <th>Name</th>
-              <th>Vorname</th>
-              <th>Name DP</th>
-              <th>Vorname DP</th>
-              <th>Bezahlt</th>
-              <th>Bezahlt Am</th>
-              <th>Junior</th>
-              <th>DP Junior</th>
-            </tr>
-          </thead>
-          <tbody>
-            {filteredPlayers.map((player, key) =>
-              <PlayerRow key={key} player={player} />
-            )}
-          </tbody>
-        </table>
-      </div>
-    )
-  }
-}
-
-export default PlayerTable

+ 0 - 8
client/src/playerList/components/index.js

@@ -1,8 +0,0 @@
-import PlayerList from './PlayerList'
-import PlayerForm from './PlayerForm'
-import PlayerTable from './PlayerTable'
-import PlayerFilter from './PlayerFilter'
-import PlayerActions from './PlayerActions'
-
-export { PlayerList, PlayerForm, PlayerTable, PlayerFilter, PlayerActions }
-export default { PlayerList, PlayerForm, PlayerTable, PlayerFilter, PlayerActions }

+ 0 - 20
client/src/playerList/functions.js

@@ -1,20 +0,0 @@
-import Excel from '../excel'         // Helper files to create Excel files
-import Player from '../classes/player'
-
-export function generatePlayerList (file) {
-  return new Promise((resolve, reject) => {
-    console.log('About to read the player list.')
-    Excel.readWorkbook(file).then(worksheets => {
-      console.log('got worksheets', worksheets)
-      const worksheet = worksheets.Players
-      if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
-        reject(Error(`Wrong file structure. Length: ${worksheet[4].length} (expected 32), Column A name: ${worksheet[3][0]} (expected Konkurrenz)`))
-      }
-      const headers = worksheet.slice(3, 1)
-      const allPlayers = worksheet.slice(4, worksheet.length).map(playerData => new Player.Player(playerData))
-      resolve({ headers, allPlayers })
-    }).catch(error => {
-      reject(Error(`Error reading workbook ${error.toString()}`))
-    })
-  })
-}

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

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

+ 0 - 121
client/src/playerList/state.js

@@ -1,121 +0,0 @@
-/** @module player/state */
-import { call, put, takeEvery } from 'redux-saga/effects'
-import { generatePlayerList } from './functions'
-
-/**
- * state.js
- *
- * Collection of everything which has to do with state changes.
- **/
-
-/** actionTypes define what actions are handeled by the reducer. */
-export const actions = {
-  fileUploadStart: file => {
-    return {
-      type: 'PLAYER_FILE_UPLOAD_START',
-      file
-    }
-  },
-  fileUploadSuccess: data => {
-    return {
-      type: 'PLAYER_FILE_UPLOAD_SUCCESS',
-      data
-    }
-  },
-  fileUploadFailure: error => {
-    return {
-      type: 'PLAYER_FILE_UPLOAD_FAILURE',
-      alert: { type: 'warning', text: error.toString() }
-    }
-  },
-  filterSortPlayers: () => {
-    return {
-      type: 'PLAYER_FILTER_SORT'
-    }
-  }
-}
-console.log('State actions', actions)
-
-/** state definition */
-export const state = {
-  headers: [],
-  allPlayers: [],
-  filteredPlayers: [],
-  filters: [
-    {
-      filterFunction: (player, filterValue) => {
-        return player.isJunior === filterValue
-      },
-      filterValue: true
-    }
-  ],
-  sorting: [
-    (player1, player2) => {
-      if (player1.Konkurrenz > player2.Konkurrenz) {
-        return 1
-      }
-      if (player1.Konkurrenz === player2.Konkurrenz) {
-        return 0
-      }
-      return -1
-    },
-    (player1, player2) => {
-      if (player1.Vorname > player2.Vorname) {
-        return 1
-      }
-      if (player1.Vorname === player2.Vorname) {
-        return 0
-      }
-      return -1
-    }
-  ],
-  fileUpload: 'idle',
-  file: null
-}
-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 'PLAYER_FILE_UPLOAD_START':
-      return { ...state, fileUpload: 'started', file: action.file }
-    case 'PLAYER_FILE_UPLOAD_SUCCESS':
-      return { ...state, fileUpload: 'finished', allPlayers: action.data.allPlayers, headers: action.data.headers }
-    case 'PLAYER_FILE_UPLOAD_FAILURE':
-      return { ...state, fileUpload: 'failure' }
-    case 'PLAYER_FILTER_SORT':
-      const { allPlayers, filters, sorting } = state
-      const sortedPlayers = allPlayers.sort((player1, player2) => {
-        return sorting.length ? sorting.reduce((acc, sortFun) => {
-          return acc === 0 ? sortFun(player1, player2) : acc
-        }, 0) : 0
-      })
-      console.log('Sorted players', sortedPlayers)
-      const filteredPlayers = sortedPlayers.filter(player => {
-        return filters.length ? filters.map(filter => filter.filterFunction(player, filter.filterValue)).every(value => !!value) : true
-      })
-      console.log('Filtered players', filteredPlayers)
-      return { ...state, filteredPlayers }
-    default:
-      return state
-  }
-}
-
-function * uploadFile (action) {
-  try {
-    console.log('PlayerList uploadFile', action.file)
-    const allPlayers = yield call(generatePlayerList, action.file)
-    console.log('PlayerList success!', actions.fileUploadSuccess(allPlayers))
-    yield put(actions.fileUploadSuccess(allPlayers))
-    yield put(actions.filterSortPlayers())
-  } catch (error) {
-    console.log('PlayerList failure!', actions.fileUploadFailure(error))
-    yield put(actions.fileUploadFailure(error))
-  }
-}
-
-/** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {
-  console.log('Player saga started.')
-  yield takeEvery('PLAYER_FILE_UPLOAD_START', uploadFile)
-}

+ 8 - 0
client/src/routes.js

@@ -3,6 +3,10 @@ import { Switch, Route } from 'react-router'
 import UserList from './users/components/UserList'
 import LoginForm from './users/components/LoginForm'
 import ConfigList from './config/components/ConfigList'
+import Matches from './matches/components/Matches'
+import Players from './matches/components/Players'
+import Swisstennis from './swisstennis/components/Swisstennis'
+import SMS from './sms/components/SMS'
 
 class Routes extends React.Component {
     render () {
@@ -13,6 +17,10 @@ class Routes extends React.Component {
             <Route path='/users' render={() => (<UserList {...this.props}/>)} />
             <Route path='/login' render={() => (<LoginForm {...this.props}/>)} />
             <Route path='/config' render={() => (<ConfigList {...this.props}/>)} />
+            <Route path='/matches' render={() => (<Matches {...this.props}/>)} />
+            <Route path='/players' render={() => (<Players {...this.props}/>)} />
+            <Route path='/swisstennis' render={() => (<Swisstennis {...this.props}/>)} />
+            <Route path='/sms' render={() => (<SMS {...this.props}/>)} />
         </Switch>
     </main>
         )

+ 0 - 87
client/src/scraper/components/ScraperInterface.js

@@ -1,87 +0,0 @@
-import React from 'react'
-import { FormGroup, ControlLabel, FormControl, Button } from 'react-bootstrap'
-
-class ScraperLogin extends React.Component {
-  constructor () {
-    super()
-    this.loginStart = this.loginStart.bind(this)
-  }
-
-  loginStart (event) {
-    event.preventDefault()
-    const { actions } = this.props
-    actions.loginStart({id: this.formId.value, pwd: this.formPwd.value})
-  }
-
-  render () {
-    const { loginState } = this.props.state
-
-    return (
-      <form>
-        <FormGroup controlId='id'>
-          <ControlLabel>Turniernummer</ControlLabel>
-          <FormControl inputRef={input => { this.formId = input }} type='text' placeholder='Turniernummer eingeben' />
-        </FormGroup>
-        <FormGroup controlId='pwd'>
-          <ControlLabel>Passwort</ControlLabel>
-          <FormControl inputRef={input => { this.formPwd = input }} type='password' />
-        </FormGroup>
-        <Button type='submit' onClick={this.loginStart}>
-          Login
-        </Button>
-        {(loginState === 'not_logged_in')
-          ? <div className='alert alert-warning'>Nicht eingeloggt.</div>
-          : (loginState === 'logging_in')
-          ? <div className='alert alert-info'>Login läft...</div>
-          : (loginState === 'login_failure')
-          ? <div className='alert alert-danger'>Login fehlgeschlagen.</div>
-          : <div className='alert alert-info'>Erfolgreich eingeloggt.</div>
-        }
-      </form>
-    )
-  }
-}
-
-class ScraperTournamentSelection extends React.Component {
-  render () {
-    return (
-      <form>
-        <FormGroup controlId='formControlsSelect'>
-          <ControlLabel>Turnier</ControlLabel>
-          <FormControl componentClass='select' placeholder='Turnier auswählen'>
-            <option value='select'>select</option>
-            <option value='other'>...</option>
-          </FormControl>
-        </FormGroup>
-        <Button type='submit'>
-          Turnier laden
-        </Button>
-      </form>
-    )
-  }
-}
-
-class ScraperProgress extends React.Component {
-  render () {
-    return (
-      <div />
-    )
-  }
-}
-
-class ScraperInterface extends React.Component {
-  render () {
-    const { actions } = this.props
-
-    return (
-      <div>
-        <ScraperLogin {...this.props} />
-        <ScraperTournamentSelection />
-        <button onClick={actions.startScraping}>Turnier laden</button>
-        <ScraperProgress />
-      </div>
-    )
-  }
-}
-
-export default ScraperInterface

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

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

+ 0 - 169
client/src/scraper/state.js

@@ -1,169 +0,0 @@
-/** @module setting/state */
-import rp from 'request-promise'
-import { takeLatest, all, call, put } from 'redux-saga/effects'
-/**
- * state.js
- *
- * Collection of everything which has to do with state changes.
- **/
-
-const SCRAPE_FILES = {
-  login: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-  tournament: 'https://comp.swisstennis.ch/advantage/servlet/ProtectedDisplayTournament?Lang=D&tournament=Id104840',
-  calendar: 'https://comp.swisstennis.ch/advantage/servlet/Calendar?tournament=Id104840&lang=D',
-  calendarXls: 'https://comp.swisstennis.ch/advantage/servlet/Calendar.xls?Lang=D&tournament=Id104840&Type=Match&Inp_DateRangeFilter.fromDate=05.07.2017&Inp_DateRangeFilter.toDate=16.07.2017',
-  playerList: 'https://comp.swisstennis.ch/advantage/servlet/PlayerList?tournament=Id104840&lang=D',
-  playerListXls: 'https://comp.swisstennis.ch/advantage/servlet/PlayerList.xls?tournament=Id104840&lang=D',
-  event: 'https://comp.swisstennis.ch/advantage/servlet/DisplayEvent?eventId=425828&lang=D',
-  draw: 'https://comp.swisstennis.ch/advantage/servlet/ModifyDraw?eventId=425828&lang=D',
-  drawXls: 'https://comp.swisstennis.ch/advantage/servlet/DisplayDraw.xls?eventId=425828&lang=D'
-}
-
-const DRAW_STATE = /Matches bereit zum spielen/
-
-/** actionTypes define what actions are handeled by the reducer. */
-export const actions = {
-  loginStart: form => {
-    return {
-      type: 'SCRAPE_LOGIN_START',
-      form
-    }
-  },
-  loginSuccess: data => {
-    return {
-      type: 'SCRAPE_LOGIN_SUCCESS',
-      data
-    }
-  },
-  loginFailure: error => {
-    return {
-      type: 'SCRAPE_LOGIN_FAILURE',
-      error
-    }
-  }
-}
-console.log('State actions', actions)
-
-/** state definition */
-export const state = {
-  loginState: 'not_logged_in',
-  jar: null,
-  pages: {},
-  files: {},
-  tournaments: {},
-  selectedTournament: {},
-  categories: {}
-}
-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 'SCRAPE_LOGIN_START':
-      return { ...state, loginState: 'logging_in' }
-    case 'SCRAPE_LOGIN_SUCCESS':
-      const { newPages } = { ...state.pages }
-      newPages.myTournaments = action.data.myTournamentsPage
-      return { ...state, loginState: 'logged_in', jar: action.data.jar, pages: newPages }
-    case 'SCRAPE_LOGIN_FAILURE':
-      return { ...state, loginState: 'login_failure' }
-    default:
-      return state
-  }
-}
-
-function * swisstennisLogin (action) {
-  console.log('swisstennisLogin', action)
-  const jar = rp.jar()
-
-  const requestOptions = {
-    uri: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-    //method: 'GET',
-    jar,
-    headers: {
-      Host: 'comp.swisstennis.ch',
-      'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
-      Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-      'Accept-Language': 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3',
-      Connection: 'keep-alive',
-      'Upgrade-Insecure-Requests': '1',
-      'Cache-Control': 'max-age=0'
-    },
-    resolveWithFullResponse: true
-  }
-  const loginOptions = {
-    uri: 'https://comp.swisstennis.ch/advantage/servlet/Login',
-    // method: 'POST',
-    jar,
-    form: {
-      Lang: 'D',
-      id: action.form.id,
-      pwd: action.form.pwd,
-      Tournament: ''
-    },
-    headers: {
-      Host: 'comp.swisstennis.ch',
-      'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
-      Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-      'Accept-Language': 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3',
-      Referer: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-      'Upgrade-Insecure-Requests': '1',
-      Connection: 'keep-alive'
-    }
-  }
-  const downloadOptions = {
-    uri: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-    // method: 'GET',
-    jar,
-    headers: {
-      Host: 'comp.swisstennis.ch',
-      'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
-      Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-      'Accept-Language': 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3',
-      Referer: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-      'Upgrade-Insecure-Requests': '1',
-      Connection: 'keep-alive'
-    }
-  }
-  console.log(jar)
-  try {
-    console.log('attempting to fetch login page.')
-    const reqPage = yield call(rp.get, requestOptions)
-    console.log('successfully fetched login page.', reqPage, jar)
-  } catch (error) {
-    console.log('Error fetching login page.', jar, error)
-    yield put(actions.loginFailure(error))
-    return
-  }
-  try {
-    console.log('attempting to login.', loginOptions)
-    const loginPage = yield call(rp.post, loginOptions)
-    console.log('received a page.', loginPage, jar)
-    if (loginPage.includes('Zugriff verweigert')) {
-      yield put(actions.loginFailure(Error('Login rejected')))
-    } else {
-      yield put(actions.loginFailure(Error('Other login problem')))
-    }
-    return
-  } catch (error) {
-    console.log('Error logging in.', error)
-    return
-  }
-  try {
-    console.log('attempting to fetch my tournaments.')
-    const myTournamentsPage = yield call(rp.get, downloadOptions)
-    const match = myTournamentsPage.match(/<a href=".*tournament=Id(\d+)">([^<]+)<\/a>/gm)
-    console.log('tournament page', match)
-    yield put(actions.loginSuccess({ myTournamentsPage, jar }))
-  } catch (error) {
-    console.log('Error fetching tournaments.')
-    yield put(actions.loginFailure(Error('Error fetching tournaments')))
-  }
-}
-
-/** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {
-  yield all([
-    takeLatest('SCRAPE_LOGIN_START', swisstennisLogin)
-  ])
-}

+ 0 - 86
client/src/scraper/test.js

@@ -1,86 +0,0 @@
-const rp = require('request-promise')
-const jar = rp.jar()
-
-const requestOptions = {
-  uri: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-  // method: 'GET',
-  jar,
-  headers: {
-    Host: 'comp.swisstennis.ch',
-    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
-    Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-    'Accept-Language': 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3',
-    // 'Accept-Encoding': 'gzip, deflate, br',
-    // Cookie: JSESSIONID=EE3CDACFFEBA200E8359F85E1C0DFB01; _ga=GA1.2.478228767.1500102081; _gid=GA1.2.1193701205.1500102081,
-    Connection: 'keep-alive',
-    'Upgrade-Insecure-Requests': '1',
-    'Cache-Control': 'max-age=0'
-  },
-  resolveWithFullResponse: true
-}
-
-const loginOptions = {
-  uri: 'https://comp.swisstennis.ch/advantage/servlet/Login',
-  // method: 'POST',
-  jar,
-  form: {
-    Lang: 'D',
-    id: '105',
-    pwd: 'S3589V',
-    Tournament: ''
-  },
-  headers: {
-    Host: 'comp.swisstennis.ch',
-    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
-    Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-    'Accept-Language': 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3',
-    // 'Accept-Encoding': 'gzip, deflate, br',
-    Referer: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-    // Cookie: JSESSIONID=53BE225CCCAC5F8D1FDB9C126F3A737C; _ga=GA1.2.478228767.1500102081; _gid=GA1.2.1193701205.1500102081
-    'Upgrade-Insecure-Requests': '1',
-    Connection: 'keep-alive'
-  }
-}
-
-const downloadOptions = {
-  uri: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-  // method: 'GET',
-  jar,
-  headers: {
-    Host: 'comp.swisstennis.ch',
-    'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0',
-    Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-    'Accept-Language': 'de-CH,de;q=0.8,en-US;q=0.5,en;q=0.3',
-    // 'Accept-Encoding': 'gzip, deflate, br',
-    Referer: 'https://comp.swisstennis.ch/advantage/servlet/MyTournamentList?Lang=D',
-    // Cookie: JSESSIONID=53BE225CCCAC5F8D1FDB9C126F3A737C; _ga=GA1.2.478228767.1500102081; _gid=GA1.2.1193701205.1500102081,
-    // 'Upgrade-Insecure-Requests': '1',
-    Connection: 'keep-alive'
-  }
-}
-
-console.log('Requesting login page.')
-rp.get(requestOptions).then(body => {
-  console.log(body)
-  console.log(jar)
-  console.log('Requesting login.')
-  rp.post(loginOptions).then(body => {
-    console.log('in then')
-    console.log(body)
-    console.log(jar)
-    console.log('Requesting tournament list.')
-    rp.get(downloadOptions).then(body => {
-      console.log(body)
-      console.log(jar)
-    })
-  }).catch(body => {
-    console.log('in catch')
-    console.log(body)
-    console.log(jar)
-    console.log('Requesting tournament list.')
-    rp.get(downloadOptions).then(body => {
-      console.log(body)
-      console.log(jar)
-    })
-  })
-})

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

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

+ 0 - 46
client/src/settings/state.js

@@ -1,46 +0,0 @@
-/** @module setting/state */
-
-/**
- * state.js
- *
- * Collection of everything which has to do with state changes.
- **/
-
-/** actionTypes define what actions are handeled by the reducer. */
-export const actions = {
-  changePriceAdult: priceAdult => {
-    return {
-      type: 'SETTING_CHANGE_PRICE_ADULT',
-      priceAdult
-    }
-  },
-  changePriceJunior: priceJunior => {
-    return {
-      type: 'SETTING_CHANGE_PRICE_JUNIOR',
-      priceJunior
-    }
-  }
-}
-console.log('State actions', actions)
-
-/** state definition */
-export const state = {
-  priceAdult: 50,
-  priceJunior: 30
-}
-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 'SETTING_CHANGE_PRICE_ADULT':
-      return { ...state, priceAdult: action.priceAdult }
-    case 'SETTING_CHANGE_PRICE_JUNIOR':
-      return { ...state, priceJunior: action.priceJunior }
-    default:
-      return state
-  }
-}
-
-/** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {}

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

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

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

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

+ 0 - 0
client/src/calendar/index.js → client/src/sms/index.js


+ 123 - 0
client/src/sms/state.js

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

+ 0 - 100
client/src/startPage/components/FileImport.js

@@ -1,100 +0,0 @@
-import React from 'react'
-import Excel from '../../excel'
-import { Button } from 'react-bootstrap'
-import { fileSize } from '../../helpers'
-
-class FileButton extends React.Component {
-  constructor () {
-    super()
-
-    this.labelStyle = {
-      fontSize: '1.25em',
-      fontWeight: 700,
-      color: '#f1e5e6',
-      backgroundColor: '#d3394c',
-      display: 'inline-block',
-      cursor: 'pointer',
-      textOverflow: 'ellipsis',
-      whiteSpace: 'nowrap',
-      overflow: 'hidden',
-      padding: '0.625rem 2.5rem'
-    }
-    this.inputStyle = {
-      width: 0.1,
-      height: 0.1,
-      opacity: 0,
-      overflow: 'hidden',
-      position: 'absolute',
-      zIndex: -1
-    }
-    this.file = null
-    this.getFocus = this.getFocus.bind(this)
-    this.loseFocus = this.loseFocus.bind(this)
-    this.processFile = this.processFile.bind(this)
-  }
-
-  getFocus (ev) {
-    console.log('enter button')
-    this.labelStyle = { ...this.labelStyle, backgroundColor: '#722040', outline: '1px solid #000' }
-  }
-
-  loseFocus (ev) {
-    console.log('leave button')
-    this.labelStyle = { ...this.labelStyle, backgroundColor: '#d3394c', outline: null }
-  }
-
-  processFile (ev) {
-    this.file = this.fileName.files[0]
-    console.log(`process file`, this.file)
-  }
-
-  render () {
-    return (
-      <div className='file-button'>
-        <input
-          name={this.props.name}
-          id={this.props.name}
-          ref={input => this.fileName = input}
-          type='file' style={this.inputStyle}
-          onChange={this.processFile} />
-        <label
-          htmlFor={this.props.name}
-          onMouseEnter={this.getFocus}
-          onMouseLeave={this.loseFocus}
-          style={this.labelStyle}>
-          <span className='glyphicon glyphicon-open' /> {this.props.children}
-        </label>
-        {this.file ? <span>{this.file.name}<i>{fileSize(this.file.size)}</i> not boring</span> : <span>boring</span>}
-      </div>
-    )
-  }
-}
-
-class FileImport extends React.Component {
-  constructor () {
-    super()
-    this.handleCalendar = this.handleCalendar.bind(this)
-    this.handlePlayerList = this.handlePlayerList.bind(this)
-  }
-
-  handleCalendar (event) {
-    const file = this.calendar.files[0]
-    Excel.readWorkbook(file, this.generateCalendar)
-  }
-
-  handlePlayerList (event) {
-    const file = this.playerList.files[0]
-    Excel.readWorkbook(file, this.generatePlayerList)
-  }
-
-  render () {
-    return (
-      <div>
-        <FileButton name='playerList' data-allowed-file-extensions='xls'>PlayerList.xls laden...</FileButton>
-        <FileButton name='calendar' data-allowed-file-extensions='xls'>Calendar.xls laden...</FileButton>
-      </div>
-    )
-  }
-}
-
-export default FileImport

+ 0 - 20
client/src/startPage/components/StartPage.js

@@ -1,20 +0,0 @@
-import React from 'react'
-import { PlayerForm } from '../../playerList/components'
-import { MatchForm } from '../../calendar/components'
-import { ScraperInterface } from '../../scraper/components'
-
-class StartPage extends React.Component {
-  render () {
-    return (
-      <div>
-        <h1>SZTM Planungshelfer</h1>
-        <p>Willkommen beim SZTM Planungshelfer</p>
-        <PlayerForm state={this.props.playerList} actions={this.props.playerListActions} />
-        <MatchForm state={this.props.calendar} actions={this.props.calendarActions} />
-        <ScraperInterface state={this.props.scraper} actions={this.props.scraperActions} />
-      </div>
-    )
-  }
-}
-
-export default StartPage

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

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

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

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

+ 0 - 27
client/src/startPage/state.js

@@ -1,27 +0,0 @@
-/** @module setting/state */
-// import { takeEvery } 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 = {}
-console.log('State actions', actions)
-
-/** state definition */
-export const state = {}
-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) {
-    default:
-      return state
-  }
-}
-
-/** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {}

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

@@ -1,81 +0,0 @@
-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

+ 100 - 0
client/src/swisstennis/components/Swisstennis.js

@@ -0,0 +1,100 @@
+import React from 'react'
+import moment from 'moment'
+
+class Swisstennis extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+  }
+
+  componentDidMount () {
+    console.log('Swisstennis did mount', this)
+    const { fileListRequest, getCalendarRequest, getPlayerListRequest } = this.props.swisstennisActions
+    fileListRequest()
+    getCalendarRequest()
+    getPlayerListRequest()
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+  }
+
+  render () {
+    const state = this.props.swisstennis
+    const actions = this.props.swisstennisActions
+    return (
+      <div>
+        <h2>Download</h2>
+        <button onClick={null}>Importieren</button><br />
+        <progress value={4} max={7}></progress>
+        <table>
+          <tr>
+            <td>Login</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+          </tr>
+          <tr>
+            <td>PlayerList herunterladen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+          </tr>
+          <tr>
+            <td>PlayerList einlesen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+          </tr>
+          <tr>
+            <td>Calendar herunterladen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+          </tr>
+          <tr>
+            <td>Calendar einlesen</td><td style={{color: state.loginStatus ? 'green' : 'red'}}>{state.loginStatus ? '\u2713' : '\u2717'}</td>
+          </tr>
+        </table>
+
+        <h2>Dateien</h2>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Name</th><th>Grösse</th><th>Datum</th><th>Verwendet</th><th>Aktionen</th>
+            </tr>
+          </thead>
+          <tbody>
+            {state.files.map(file => (
+            <tr>
+              <td>{file.filename}</td><td>{file.size/1024}kB</td><td>{moment(file.ctime).format('DD.MM.YYYY HH:mm')}</td><td>{(state.calendars.find(matchList => matchList.file == file.filename) || state.playerLists.find(playerList => playerList.file == file.filename)) ? 'Ja' : 'Nein'}</td><td><a>loeschen</a></td>
+            </tr>
+            ))}
+          </tbody>
+        </table>
+
+        <h2>Calendar Files</h2>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Datei</th><th>Datum</th><th>Matches</th><th>Aktionen</th>
+            </tr>
+          </thead>
+          <tbody>
+            {state.calendars.sort((a, b) => a.imported < b.imported).map(calendar => (
+            <tr>
+              <td>{calendar.file}</td><td>{moment(calendar.imported).format('DD.MM.YYYY HH:mm')}</td><td>{calendar.matches.length}</td><td><a>verwenden</a><a>loeschen</a></td>
+            </tr>
+            ))}
+          </tbody>
+        </table>
+
+        <h2>PlayerList Files</h2>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Datei</th><th>Datum</th><th>Spieler</th><th>Aktionen</th>
+            </tr>
+          </thead>
+          <tbody>
+            {state.playerLists.sort((a, b) => a.imported < b.imported).map(playerList => (
+            <tr>
+              <td>{playerList.file}</td><td>{moment(playerList.imported).format('DD.MM.YYYY HH:mm')}</td><td>{playerList.players.length}</td><td><a>verwenden</a><a>loeschen</a></td>
+            </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    )
+  }
+}
+
+export default Swisstennis

+ 3 - 3
client/src/swisstennis/components/index.js

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

+ 189 - 113
client/src/swisstennis/state.js

@@ -1,5 +1,6 @@
-/** @module config/state */
-import { call, put, takeEvery, select } from 'redux-saga/effects'
+/** @module swisstennis/state */
+import { call, put, takeEvery } from 'redux-saga/effects'
+import { SZTM_API } from '../config/static'
 
 /**
  * state.js
@@ -9,162 +10,217 @@ import { call, put, takeEvery, select } from 'redux-saga/effects'
 
 /** actionTypes define what actions are handeled by the reducer. */
 export const actions = {
-  configClearForm: () => {
+  fileListRequest: () => {
     return {
-      type: 'CONFIG/CLEAR_FORM'
+      type: 'SWISSTENNIS/FILE_LIST_REQUEST',
     }
   },
-  configLoadForm: (key) => {
+  fileListSuccess: data => {
     return {
-      type: 'CONFIG/LOAD_FORM',
+      type: 'SWISSTENNIS/FILE_LIST_SUCCESS',
+      data
+    }
+  },
+  fileListFailure: error => {
+    return {
+      type: 'SWISSTENNIS/FILE_LIST_FAILURE',
+      error
+    }
+  },
+  loginRequest: () => {
+    return {
+      type: 'SWISSTENNIS/LOGIN_REQUEST'
+    }
+  },
+  loginSuccess: (key) => {
+    return {
+      type: 'SWISSTENNIS/LOGIN_SUCCESS',
       key
     }
   },
-  configChangeForm: (data) => {
+  loginFailure: (data) => {
+    return {
+      type: 'SWISSTENNIS/LOGIN_FAILURE',
+      data
+    }
+  },
+  getPlayerListRequest: () => {
+    return {
+      type: 'SWISSTENNIS/GET_PLAYERLIST_REQUEST'
+    }
+  },
+  getPlayerListSuccess: (data) => {
+    return {
+      type: 'SWISSTENNIS/GET_PLAYERLIST_SUCCESS',
+      data
+    }
+  },
+  getPlayerListFailure: error => {
+    return {
+      type: 'SWISSTENNIS/GET_PLAYERLIST_FAILURE',
+      error
+    }
+  },
+  downloadPlayerListRequest: () => {
     return {
-      type: 'CONFIG/CHANGE_FORM',
+      type: 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_REQUEST'
+    }
+  },
+  downloadPlayerListSuccess: (data) => {
+    return {
+      type: 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_SUCCESS',
       data
     }
   },
-  configGetRequest: () => {
+  downloadPlayerListFailure: error => {
     return {
-      type: 'CONFIG/GET_REQUEST'
+      type: 'SWISSTENNIS/DOWNLOAD_PLAYERLIST_FAILURE',
+      error
     }
   },
-  configGetSuccess: (data) => {
+  getCalendarRequest: (data) => {
     return {
-      type: 'CONFIG/GET_SUCCESS',
+      type: 'SWISSTENNIS/GET_CALENDAR_REQUEST',
       data
     }
   },
-  configGetFailure: error => {
+  getCalendarSuccess: (data) => {
     return {
-      type: 'CONFIG/GET_FAILURE',
+      type: 'SWISSTENNIS/GET_CALENDAR_SUCCESS',
+      data
+    }
+  },
+  getCalendarFailure: error => {
+    return {
+      type: 'SWISSTENNIS/GET_CALENDAR_FAILURE',
       error
     }
   },
-  configAddRequest: (data) => {
+  downloadCalendarRequest: (data) => {
     return {
-      type: 'CONFIG/ADD_REQUEST',
+      type: 'SWISSTENNIS/DOWNLOAD_CALENDAR_REQUEST',
       data
     }
   },
-  configAddSuccess: (data) => {
+  downloadCalendarSuccess: (data) => {
     return {
-      type: 'CONFIG/ADD_SUCCESS',
+      type: 'SWISSTENNIS/DOWNLOAD_CALENDAR_SUCCESS',
       data
     }
   },
-  configAddFailure: error => {
+  downloadCalendarFailure: error => {
     return {
-      type: 'CONFIG/ADD_FAILURE',
+      type: 'SWISSTENNIS/DOWNLOAD_CALENDAR_FAILURE',
       error
     }
   },
-  configEditRequest: (data) => {
+  parsePlayerListRequest: (data) => {
     return {
-      type: 'CONFIG/EDIT_REQUEST',
+      type: 'SWISSTENNIS/PARSE_PLAYERLIST_REQUEST',
       data
     }
   },
-  configEditSuccess: (data) => {
+  parsePlayerListSuccess: (data) => {
     return {
-      type: 'CONFIG/EDIT_SUCCESS',
+      type: 'SWISSTENNIS/PARSE_PLAYERLIST_SUCCESS',
       data
     }
   },
-  configEditFailure: error => {
+  parsePlayerListFailure: error => {
     return {
-      type: 'CONFIG/EDIT_FAILURE',
+      type: 'SWISSTENNIS/PARSE_PLAYERLIST_FAILURE',
       error
     }
   },
-  configDeleteRequest: (key) => {
+  parseCalendarRequest: (key) => {
     return {
-      type: 'CONFIG/DELETE_REQUEST',
+      type: 'SWISSTENNIS/PARSE_CALENDAR_REQUEST',
       key
     }
   },
-  configDeleteSuccess: (data) => {
+  parseCalendarSuccess: (data) => {
     return {
-      type: 'CONFIG/DELETE_SUCCESS',
+      type: 'SWISSTENNIS/PARSE_CALENDAR_SUCCESS',
       data
     }
   },
-  configDeleteFailure: error => {
+  parseCalendarFailure: error => {
     return {
-      type: 'CONFIG/DELETE_FAILURE',
+      type: 'SWISSTENNIS/PARSE_CALENDAR_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
+  files: [],
+  fileListStatus: 'uninitialized',
+  calendars: [],
+  calendarsStatus: 'uninitialized',
+  playerLists: [],
+  playerListsStatus: 'uninitialized',
+  transferStarted: false,
+  loginState: 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 }
+    case 'SWISSTENNIS/FILE_LIST_REQUEST':
+      return { ...state, fileListStatus: 'loading' }
+    case 'SWISSTENNIS/FILE_LIST_SUCCESS':
+      return { ...state, fileListStatus: 'loaded', files: action.data }
+    case 'SWISSTENNIS/FILE_LIST_FAILURE':
+      return { ...state, fileListStatus: 'failed' }
+    case 'SWISSTENNIS/GET_CALENDAR_REQUEST':
+      return { ...state, calendarsStatus: 'loading' }
+    case 'SWISSTENNIS/GET_CALENDAR_SUCCESS':
+      return { ...state, calendarsStatus: 'loaded', calendars: action.data }
+    case 'SWISSTENNIS/GET_CALENDAR_FAILURE':
+      return { ...state, calendarsStatus: 'failed' }
+      case 'SWISSTENNIS/GET_PLAYERLIST_REQUEST':
+        return { ...state, playerListsStatus: 'loading' }
+      case 'SWISSTENNIS/GET_PLAYERLIST_SUCCESS':
+        return { ...state, playerListsStatus: 'loaded', playerLists: action.data }
+      case 'SWISSTENNIS/GET_PLAYERLIST_FAILURE':
+        return { ...state, playerListsStatus: 'failed' }
     default:
       return state
   }
 }
 
-function * getConfig (action) {
+function * fileList (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('File list requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/files`, {
+      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.fileListSuccess(responseJson.fileList))
+    }
+  } catch (error) {
+    console.log('Config failure!', actions.fileListFailure(error))
+    yield put(actions.fileListFailure(error))
+  }
+}
+
+function * login (action) {
   try {
     const token = localStorage.getItem('accessToken')
     console.log('Get config requested', action, token)
-    const response = yield call(fetch, 'http://localhost:3002/api/config', {
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
       method: 'GET',
       headers: {
         'Content-Type': 'application/json',
@@ -184,36 +240,58 @@ function * getConfig (action) {
   }
 }
 
-function * addConfig (action) {
+function * getCalendars (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',
+    console.log('Get config requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/calendar`, {
+      method: 'GET',
       headers: {
         'Content-Type': 'application/json',
         'x-access-token': token
-      },
-      body: JSON.stringify(action.data)
+      }
     }) 
     if (response.status != 200) {
       throw Error(response.status)
     } else {
-      yield put(actions.configAddSuccess(action.data))
-      yield put(actions.configGetRequest())
+      const responseJson = yield response.json()
+      yield put(actions.getCalendarSuccess(responseJson.calendars))
     }
   } catch (error) {
-    console.log('Config failure!', actions.configAddFailure(error))
-    yield put(actions.configAddFailure(error))
+    console.log('Config failure!', actions.getCalendarFailure(error))
+    yield put(actions.getCalendarFailure(error))
   }
 }
 
-function * editConfig (action) {
+function * getPlayerLists (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',
+    console.log('Get config requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/swisstennis/playerList`, {
+      method: 'GET',
+      headers: {
+        'Content-Type': 'application/json',
+        'x-access-token': token
+      }
+    }) 
+    if (response.status != 200) {
+      throw Error(response.status)
+    } else {
+      const responseJson = yield response.json()
+      yield put(actions.getPlayerListSuccess(responseJson.playerLists))
+    }
+  } catch (error) {
+    console.log('Config failure!', actions.getPlayerListFailure(error))
+    yield put(actions.getPlayerListFailure(error))
+  }
+}
+
+function * addConfig (action) {
+  try {
+    const token = localStorage.getItem('accessToken')
+    console.log('Add config requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
+      method: 'POST',
       headers: {
         'Content-Type': 'application/json',
         'x-access-token': token
@@ -223,45 +301,43 @@ function * editConfig (action) {
     if (response.status != 200) {
       throw Error(response.status)
     } else {
-      yield put(actions.configEditSuccess(action.data))
+      yield put(actions.configAddSuccess(action.data))
+      yield put(actions.configGetRequest())
     }
   } catch (error) {
-    console.log('Config failure!', actions.configEditFailure(error))
-    yield put(actions.configEditFailure(error))
+    console.log('Config failure!', actions.configAddFailure(error))
+    yield put(actions.configAddFailure(error))
   }
 }
 
-function * deleteConfig (action) {
+function * editConfig (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',
+    console.log('Edit config requested', action, token)
+    const response = yield call(fetch, `${SZTM_API}/api/config`, {
+      method: 'PUT',
       headers: {
         'Content-Type': 'application/json',
         'x-access-token': token
       },
-      body: JSON.stringify({ key })
+      body: JSON.stringify(action.data)
     }) 
     if (response.status != 200) {
       throw Error(response.status)
     } else {
-      yield put(actions.configDeleteSuccess(action.key))
+      yield put(actions.configEditSuccess(action.data))
     }
   } catch (error) {
-    console.log('Config failure!', actions.configDeleteFailure(error))
-    yield put(actions.configDeleteFailure(error))
+    console.log('Config failure!', actions.configEditFailure(error))
+    yield put(actions.configEditFailure(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)
+  yield takeEvery('SWISSTENNIS/FILE_LIST_REQUEST', fileList)
+  yield takeEvery('SWISSTENNIS/GET_CALENDAR_REQUEST', getCalendars)
+  yield takeEvery('SWISSTENNIS/GET_PLAYERLIST_REQUEST', getPlayerLists)
+  yield takeEvery('SWISSTENNIS/LOGIN_REQUEST', login)
 }

+ 0 - 56
client/src/tables/components/Table.js

@@ -1,56 +0,0 @@
-import React from 'react'
-import { Pagination, Popover, OverlayTrigger } from 'react-bootstrap'
-
-
-class TableHeader extends React.Component {
-  render () {
-    const { headers, data, actions } = this.props
-    const filters = {}
-    headers.forEach(header => {
-      
-    })
-    return (
-      <thead>
-        <tr>
-          {headers.map(header => 
-            <th>{header} <OverlayTrigger trigger='click' placement='bottom' overlay={}><span class='glyphicon glyphicon-filter'></span></OverlayTrigger></th>
-          )}
-        </tr>
-      </thead>
-    )
-  }
-}
-
-class Table extends React.Component {
-  changePagination (event) {
-    const { actions } = this.props
-    event.preventDefault()
-    actions.changePagination(event)
-  }
-
-  render () {
-    const { options, data, pagination } = this.props
-    if (!data.hasOwnProperty('headers')) {
-      data.headers = data.rows.map()
-    }
-    return (
-      <div>
-        <table class='table table-hover table-bordered table-condensed'>
-          <TableHeader headers={data.headers} />
-          <TableBody rows={data.rows} />
-        </table>
-        <Pagination
-          prev next
-          first last
-          ellipsis
-          items={pagination.items}
-          maxButtons={5}
-          activePage={pagination.activePage}
-          onSelect={this.changePagination}
-        />
-      </div>
-    )
-  }
-}
-
-export default Table

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

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

+ 0 - 55
client/src/tables/state.js

@@ -1,55 +0,0 @@
-/** @module setting/state */
-
-/**
- * state.js
- *
- * Collection of everything which has to do with state changes.
- **/
-
-/** actionTypes define what actions are handeled by the reducer. */
-export const actions = {
-  changePaginationPage: page => {
-    return {
-      type: 'TABLE_CHANGE_PAGINATION_PAGE',
-      page
-    }
-  },
-  changePaginationItems: items => {
-    return {
-      type: 'TABLE_CHANGE_PAGINATION_ITEMS',
-      items
-    }
-  }
-}
-console.log('State actions', actions)
-
-/** state definition */
-export const state = {
-  filters: {},
-  sorting: {},
-  pagination: {
-    activePage: 0,
-    items: 50
-  }
-}
-console.log('State state', state)
-
-/** reducer is called by the redux dispatcher and handles all component actions */
-export function reducer (state = [], action) {
-  let pagination
-  switch (action.type) {
-    case 'TABLE_CHANGE_PAGINATION_PAGE':
-      pagination = { ...state.pagination }
-      pagination.activePage = action.page
-      return { ...state, pagination }
-    case 'TABLE_CHANGE_PAGINATION_ITEMS':
-      pagination = { ...state.pagination }
-      pagination.items = action.items
-      return { ...state, pagination }
-    default:
-      return state
-  }
-}
-
-/** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {}

+ 0 - 0
client/src/users/components/EditUser.js


+ 8 - 7
client/src/users/state.js

@@ -2,6 +2,7 @@
 import { call, put, takeEvery } from 'redux-saga/effects'
 import jwt from 'jwt-simple'
 import bcrypt from 'bcryptjs'
+import { SZTM_API } from '../config/static'
 
 /**
 * state.js
@@ -68,8 +69,8 @@ console.log('State actions', actions)
 
 // Check, if there is a valid token.
 const token = localStorage.getItem('accessToken')
-const tokenData = JSON.parse(localStorage.getItem('accessTokenData'))
-const tokenValid = tokenData.expires > Date.now()
+const tokenData = token ? JSON.parse(localStorage.getItem('accessTokenData')) : null
+const tokenValid = tokenData ? tokenData.expires > Date.now() : false
 
 export const state = {
     loginRequested: false,
@@ -110,7 +111,7 @@ export function reducer (state = [], action) {
 function * login (action) {
     try {
         console.log('User login requested', action.data)
-        const response = yield call(fetch, 'http://localhost:3002/authenticate/login', {
+        const response = yield call(fetch, `${SZTM_API}/authenticate/login`, {
             method: 'POST',
             headers: {
                 'Content-Type': 'application/json'
@@ -138,13 +139,13 @@ function * getUserList (action) {
     try {
         const token = localStorage.getItem('accessToken')
         console.log('User list requested', action, token)
-        const response = yield call(fetch, 'http://localhost:3002/api/users', {
+        const response = yield call(fetch, `${SZTM_API}/api/users`, {
             method: 'GET',
             headers: new Headers({
                 'Content-Type': 'application/json',
                 'x-access-token': token
             })
-        }) 
+        })
         console.log('Received response')
         const responseJson = yield response.json()
         if (response.status != 200) {
@@ -163,7 +164,7 @@ function * addUser (action) {
     try {
         const token = localStorage.getItem('accessToken')
         console.log('Add user requested', action, token)
-        const response = yield call(fetch, 'http://localhost:3002/api/users', {
+        const response = yield call(fetch, `${SZTM_API}/api/users`, {
             method: 'POST',
             headers: new Headers({
                 'Content-Type': 'application/json',
@@ -189,7 +190,7 @@ function * saveUser (action) {
     try {
         const token = localStorage.getItem('accessToken')
         console.log('Save user requested', action, token)
-        const response = yield call(fetch, 'http://localhost:3002/api/users', {
+        const response = yield call(fetch, `${SZTM_API}/api/users`, {
             method: 'PUT',
             headers: new Headers({
                 'Content-Type': 'application/json',

+ 10 - 0
server/package-lock.json

@@ -869,6 +869,11 @@
         }
       }
     },
+    "base-64": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
+      "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
+    },
     "base64-js": {
       "version": "0.0.8",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
@@ -4171,6 +4176,11 @@
       "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
       "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
     },
+    "node-fetch": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
+      "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
+    },
     "nodemon": {
       "version": "1.17.4",
       "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.17.4.tgz",

+ 2 - 0
server/package.json

@@ -8,6 +8,7 @@
     "babel-polyfill": "^6.23.0",
     "babel-preset-env": "^1.7.0",
     "babel-register": "^6.24.1",
+    "base-64": "^0.1.0",
     "bcrypt": "^2.0.0",
     "bhttp": "^1.2.4",
     "blob": "^0.0.4",
@@ -22,6 +23,7 @@
     "moment": "^2.18.1",
     "mongoose": "^5.1.1",
     "morgan": "^1.8.2",
+    "node-fetch": "^2.1.2",
     "pdfmake": "^0.1.37",
     "request": "^2.81.0",
     "request-promise": "^4.2.1",

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

@@ -4,16 +4,19 @@ import bodyParser from 'body-parser'
 import mongoose from 'mongoose'
 import configFile from './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 download from './routes/download'
+import sms from './routes/sms'
+
 import { authenticate, verify } from './routes/authenticate'
 
 import { errorHandler } from './middleware/apiErrorHandler'
 
+mongoose.connect(configFile.database)
+
 const port = process.env.PORT || 3002
 const app = express()
 app.use(cors())
@@ -22,16 +25,18 @@ app.use(bodyParser.urlencoded({ extended: false }))
 app.use(bodyParser.json())
 
 app.get('/', (req, res) => {
-  res.send(`Express API at http://localhost:${port}/api`)
+  res.send(`Express API`)
 })
 
 app.use('/authenticate', authenticate)
+app.use('/download', download)
 
 const apiRoutes = express.Router()
 apiRoutes.use('/users', users)
 apiRoutes.use('/swisstennis', swisstennis)
 apiRoutes.use('/sztm', sztm)
 apiRoutes.use('/config', config)
+apiRoutes.use('/sms', sms)
 app.use('/api', verify)
 app.use('/api', apiRoutes)
 app.use(errorHandler)

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

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

+ 21 - 0
server/src/restServer/config/sztm.js

@@ -0,0 +1,21 @@
+const places = {
+    AU: "auswärts",
+    UE: "TC Uetliberg",
+    EN: "TC Engematt",
+    BA: "TC Bachtobel",
+    FR: "TC Credit Suisse Frauenthal",
+    HA: "TC Hakoah",
+    UI: "TC Uitikon"
+}
+
+const emails = {
+    UE: "info@sztm.ch",
+    EN: "engematt@sztm.ch",
+    BA: "bachtobel@sztm.ch",
+    FR: "cs@sztm.ch",
+    HA: "hakoah@sztm.ch",
+    UI: "uitikon@sztm.ch"
+}
+
+export { places, emails }
+export default { places, emails }

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

@@ -2,7 +2,7 @@ import mongoose from 'mongoose'
 
 const MatchSchema = new mongoose.Schema({
   created: Date,
-  idString: String,
+  idString: { type: String, unique: true },
   fileLine: Number,
   category: String,
   place: String,
@@ -15,6 +15,7 @@ const MatchSchema = new mongoose.Schema({
 
 MatchSchema.methods.equal = function (matchData, cb) {
   for (let property in matchData) {
+    if (property == 'created') return true
     if (this[property] instanceof Date) {
       if (this[property].getTime() != matchData[property].getTime()) {
         console.log('dates dont match', property, this[property], matchData[property])

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

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

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

@@ -31,6 +31,8 @@ const PlayerSchema = new mongoose.Schema({
 
 PlayerSchema.methods.equal = function (playerData, cb) {
   for (let property in playerData) {
+    if (property == 'created') continue
+    if (property == 'paid') console.log('paid', playerData[property], this[property])
     if (this[property] instanceof Date) {
       if (this[property].getTime() != playerData[property].getTime()) {
         console.log('dates dont match', property, this[property], playerData[property])

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

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

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

@@ -0,0 +1,13 @@
+import mongoose from 'mongoose'
+
+const SMSSchema = new mongoose.Schema({
+    sent: Date,
+    body: String,
+    recipients: [String],
+    sender: String,
+    response: String
+})
+
+const SMS = mongoose.model('SMS', SMSSchema)
+
+export default SMS

+ 1 - 1
server/src/restServer/routes/authenticate.js

@@ -85,7 +85,7 @@ const verify = express.Router()
 verify.use( (req, res, next) => {
   console.log("Trying to authenticate token.")
   const token = req.headers['x-access-token']
-  console.log('Got token', req.headers, token)
+  console.log('Got token', token)
 
   if (token) {
     const decoded = jwt.decode(token, config.secret)

+ 12 - 0
server/src/restServer/routes/download.js

@@ -0,0 +1,12 @@
+import express from 'express'
+
+const download = express.Router() 
+
+download.get('/:filename', async (req, res) => {
+        console.log(req.params)
+        const { filename } = req.params
+        const file = `/usr/src/sztm_files/${filename}`
+        res.download(file)
+})
+
+export default download

+ 66 - 0
server/src/restServer/routes/sms.js

@@ -0,0 +1,66 @@
+import express from 'express'
+import SMS from '../models/sms'
+import bulksms from '../config/bulksms'
+import base64 from 'base-64'
+import fetch from 'node-fetch'
+
+const sms = express.Router() 
+
+sms.post('/send', async (req, res) => {
+    try {
+        const { recipients, body, sender } = req.body
+        console.log(req.body, recipients, body, sender)
+        const Authorization = `Basic ${base64.encode(`${bulksms.tokenId}:${bulksms.tokenSecret}`)}`
+        const response = await fetch(`https://api.bulksms.com/v1/messages`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                Authorization
+            },
+            body: JSON.stringify({
+                to: recipients,
+                from: sender,
+                body
+            })
+        })
+        const responseContent = (response.status == 201) ?
+            await response.json() :
+            await response.text()
+        const dbSMS = new SMS({
+            sent: new Date(),
+            body,
+            recipients,
+            sender,
+            response: (response.status == 201) ? JSON.stringify(responseContent) : responseContent
+        })
+        dbSMS.save()
+        if (response.status != 201) {
+            throw Error(responseContent)
+        }
+        res.json(responseContent)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+})
+
+sms.get('/profile', async (req, res) => {
+    try {
+        const Authorization = `Basic ${base64.encode(`${bulksms.tokenId}:${bulksms.tokenSecret}`)}`
+        const response = await fetch(`https://api.bulksms.com/v1/profile`, {
+            method: 'GET',
+            headers: {
+                Authorization
+            }
+        })
+        console.log('response', response)
+        if (response.status != 200) {
+            throw Error(`Received status code ${response.status}`)
+        }
+        const responseJson = await response.json()
+        res.json(responseJson)
+    } catch (error) {
+        return res.status(400).json({ msg: error.toString() })
+    }
+})
+
+export default sms

+ 29 - 2
server/src/restServer/routes/swisstennis.js

@@ -61,7 +61,7 @@ swisstennis.post('/login', async (req, res) => {
       throw Error('Access denied!')
     }
   } catch (error) {
-    return res.status(400).json({ msg: error })
+    return res.status(400).json({ msg: error.toString() })
   }
   res.json({ msg: 'Logged in successfully.' })
 })
@@ -119,6 +119,20 @@ swisstennis.get('/draws', async (req, res) => {
   }
 })
 
+swisstennis.get('/playerlist', async (req, res) => {
+  try {
+    const tournament = req.query.tournament || config.tournamentId
+    
+    if (!tournament) {
+      throw Error('No tournament given.')
+    }
+    const playerLists = await PlayerList.find()
+    return res.json({ playerLists })
+  } catch (error) {
+    return res.status(400).json({ msg: error.toString() })
+  }
+})
+
 // Download a playerlist
 swisstennis.get('/playerlist/download', async (req, res) => {
   try {
@@ -237,7 +251,6 @@ swisstennis.get('/playerlist/parse/:filename', async (req, res) => {
 
 // List downloaded files
 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 => {
@@ -261,6 +274,20 @@ swisstennis.delete('/files', async (req, res) => {
   }
 })
 
+swisstennis.get('/calendar', async (req, res) => {
+  try {
+    const tournament = req.query.tournament || config.tournamentId
+    
+    if (!tournament) {
+      throw Error('No tournament given.')
+    }
+    const calendars = await MatchList.find()
+    return res.json({ calendars })
+  } catch (error) {
+    return res.status(400).json({ msg: error.toString() })
+  }
+})
+
 swisstennis.get('/calendar/download', async (req, res) => {
   const tournament = req.query.tournament || config.tournamentId
   

+ 211 - 29
server/src/restServer/routes/sztm.js

@@ -1,53 +1,235 @@
 import fs from 'fs'
-import awaitFs from 'await-fs'
-import { bulksmsConfig } from '../config/bulksms'
 import express from 'express'
-import Player from '../models/player'
+import moment from 'moment'
+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'
 
-const sztm = express.Router()
+const sztm = express.Router() 
 
 sztm.get('/schedule', async (req, res) => {
     try{
-        const { matchListId, startDate, endDate, place, category } = req.query
+        console.log(req.query)
+        const { matchListId, date, place, category } = req.query
         
         const matchList = matchListId ? 
-        await MatchList.findOne({ _id: matchListId }) : 
-        await MatchList.findOne().sort({ imported: -1 })
+            await MatchList.findOne({ _id: matchListId }) : 
+            await MatchList.findOne().sort({ imported: -1 })
         
+        console.log('matches', matchList.matches.length)
         const query = { _id: { $in: matchList.matches } }
-        if (place) query.place = place
+        /*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
-        }
+        if (date) { 
+            query.date = {} 
+            const dateElems = date.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1, 7)
+            const startDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} 00:00:00`)
+            query.date.$gte = startDate
+            const endDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} 23:59:59`)
+            query.date.$lte = endDate
+        }*/
+
+        const matches = await Match.find(query).sort({ date: 1 }).populate('player1').populate('player2')
+        console.log('matches', matches.length)
+        //const players = await Player.find({ _id: { $in: playerList } })
+        //console.log(playerList, players)
+        res.json({ matches })
         
-        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 })
+    } catch (error) {
+        res.status(400).json({ msg: error.toString() })
+    }
+})
+
+sztm.get('/players', async (req, res) => {
+    try{
+        console.log(req.query)
+        const { playerListId, date, place, category } = req.query
+        
+        const playerList = playerListId ? 
+            await PlayerList.findOne({ _id: playerListId }) : 
+            await PlayerList.findOne().sort({ imported: -1 })
+        
+        console.log('players', playerList.players.length)
+        const query = { _id: { $in: playerList.players } }
+        /*if (place) query.place = place
+        if (category) query.category = category
+        if (date) { 
+            query.date = {} 
+            const dateElems = date.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1, 7)
+            const startDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} 00:00:00`)
+            query.date.$gte = startDate
+            const endDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} 23:59:59`)
+            query.date.$lte = endDate
+        }*/
+
+        const players = await Player.find(query).sort({ date: 1 })
+        console.log('players', players.length)
+        
+        res.json({ players })
         
-        //const filteredMatches = matchList.matches.sort().filter()
     } catch (error) {
         res.status(400).json({ msg: error.toString() })
     }
 })
 
-sztm.post('/sms', async (req, res) => {
+sztm.post('/pdf', async (req, res) => {
+    try {
+        console.log(req.body)
+        const { matchListId, date, place, category } = req.body
+        
+        const matchList = matchListId ? 
+            await MatchList.findOne({ _id: matchListId }) : 
+            await MatchList.findOne().sort({ imported: -1 })
+        console.log(matchList._id)
+        
+        const query = {}//{ _id: { $in: matchList.matches } }
+        if (place) query.place = place
+        if (category) query.category = category
+        if (date) { 
+            query.date = {} 
+            const dateElems = date.match(/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/).slice(1, 7)
+            const startDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} 00:00:00`)
+            query.date.$gte = startDate
+            const endDate = new Date(`${dateElems[0]}-${dateElems[1]}-${dateElems[2]} 23:59:59`)
+            query.date.$lte = endDate
+        }
+        
+        const matches = await Match.find(query).sort({ date: 1 }).populate('player1').populate('player2')
+        console.log(matches)
+        const players = []
+        matches.forEach(match => {
+            if (match.player1) players.push(match.player1)
+            if (match.player2) players.push(match.player2)
+        })
+        console.log('matches', matches, players)
+
+        const allPlaces = []
+        matches.forEach(match => {
+            if (!!match.place && !allPlaces.includes(match.place)) allPlaces.push(match.place)
+        })
+
+        allPlaces.forEach(place => {
+            const tableRows = []
+            matches.filter(match => match.place && (match.place == place)).forEach(match => {
+                if (match.player1) tableRows.push([match.player1.paid ? 'OK' : '', match.category, moment(match.date).format('HH:mm'), `(${match.player1.junior ? '30.-' : '50.-'}) ${match.player1.fullName}`, ''])
+                if (match.player2) tableRows.push([match.player2.paid ? 'OK' : '', match.category, moment(match.date).format('HH:mm'), `(${match.player2.junior ? '30.-' : '50.-'}) ${match.player2.fullName}`, ''])
+            })
+            const paylistDoc = {
+                pageSize: 'A4',
+                pageOrientation: 'portrait',
+                content: [
+                    { text: 'Stadtzürcher Tennismeisterschaft', style: 'header' },
+                    { text: `Nenngelder für den ${moment(query.date.$gte).format('DD.MM.YYYY')}`, style: 'subheader' },
+                    { columns: [
+                        {text: places[place]},
+                        {text: '50.- oder 30.- (Junioren Jg. 2000 oder jünger)'}
+                    ]},
+                    { table: {
+                        headerRows: 1,
+                        widths: ['10%', '20%', '10%', '*', '15%'],
+                        heights: 30,
+                        body: [
+                            [{text: 'bezahlt', fillColor: '#eeeeee'}, 
+                             {text: 'Kategorie', fillColor: '#eeeeee'}, 
+                             {text: 'Zeit', fillColor: '#eeeeee'}, 
+                             {text: 'Name', fillColor: '#eeeeee'}, 
+                             {text: 'Betrag bez.', fillColor: '#eeeeee'}],
+                            ...tableRows
+                        ]},
+                        style: 'tableStyle'
+                    },
+                    { table: {
+                        dontBreakRows: true,
+                        widths: ['25%', '*', '25%', '25%'],
+                        body: [
+                            [{text: 'Datum', bold: true}, {text: '', colSpan: 3}, {}, {}],
+                            [{text: 'Turnierleiter', colSpan:2, margin: [0, 10, 0, 0], bold: true}, {}, 
+                             {text: 'Kassierer', colSpan:2, margin: [0, 10, 0, 0], bold: true}, {}],
+                            [{text: 'Betrag von Spielern erhalten', colSpan:2, margin: [0, 25, 0, 0]}, {}, 
+                             {text: 'Betrag von Turnierleiter erhalten', colSpan:2, margin: [0, 25, 0, 0]}, {}]
+                        ]},
+                      style: 'tableStyle'}
+                    ],
+                styles: {
+                    header: {
+                        fontSize: 22,
+                        bold: true,
+                        margin: [0, 20, 0, 8]
+                    },
+                    subheader: {
+                        fontSize: 14,
+                        margin: [0, 15, 0, 12]
+                    },
+                    tableStyle: {
+                        margin: [0, 15, 0, 5]
+                    }
+                }
+            }
     
+            const scheduleDoc = {
+                pageSize: 'A4',
+                pageOrientation: 'landscape',
+                content: [
+                    { text: 'Stadtzürcher Tennismeisterschaft', style: 'header' },
+                    { text: `Spielplan für den ${moment(query.date.$gte).format('DD.MM.YYYY')} (${places[place]})`, style: 'subheader' },
+                    { table: {
+                        headerRows: 1,
+                        widths: ['5%', '5%', '12%', '*', '5%', '*', '5%', '7%', '7%', '7%', '10%'],
+                        heights: 30,
+                        body: [
+                            [{text: 'Platz', fillColor: '#eeeeee'}, 
+                             {text: 'Zeit', fillColor: '#eeeeee'}, 
+                             {text: 'Kategorie', fillColor: '#eeeeee'}, 
+                             {text: 'Spieler 1', fillColor: '#eeeeee', colSpan: 2}, 
+                             {}, 
+                             {text: 'Spieler 2', fillColor: '#eeeeee', colSpan: 2}, 
+                             {}, 
+                             {text: '1. Satz', fillColor: '#eeeeee'}, 
+                             {text: '2. Satz', fillColor: '#eeeeee'}, 
+                             {text: '3. Satz', fillColor: '#eeeeee'}, 
+                             {text: 'WO Grund', fillColor: '#eeeeee'}],
+                            ...matches.filter(match => match.place && (match.place == place)).map((match) => {
+                                return ['', moment(match.date).format('HH:mm'), match.category, 
+                                match.player1 && match.player1.fullName, match.player1.ranking, 
+                                match.player2 && match.player2.fullName, match.player2.ranking, 
+                                '', '', '', '']
+                            })
+                        ]},
+                        style: 'tableStyle'
+                    }],
+                styles: {
+                    header: {
+                        fontSize: 22,
+                        bold: true,
+                        margin: [0, 20, 0, 8]
+                    },
+                    subheader: {
+                        fontSize: 14,
+                        margin: [0, 15, 0, 12]
+                    },
+                    tableStyle: {
+                        margin: [0, 15, 0, 5]
+                    }
+                }
+            }
+    
+            const paylistGenerator = pdfMake.createPdf(paylistDoc)
+            paylistGenerator.getBuffer((buffer) => {
+                fs.writeFileSync(`/usr/src/sztm_files/Zahlliste-${place}-${moment(query.date.$gte).format('YYYYMMDD')}.pdf`, new Buffer(new Uint8Array(buffer)))
+            })
+            const scheduleGenerator = pdfMake.createPdf(scheduleDoc)
+            scheduleGenerator.getBuffer((buffer) => {
+                fs.writeFileSync(`/usr/src/sztm_files/Spielliste-${place}-${moment(query.date.$gte).format('YYYYMMDD')}.pdf`, new Buffer(new Uint8Array(buffer)))
+            })
+
+        })
+        
+        res.json({ msg: "All PDFs generated." })
+    } catch (error) {
+        return res.json({ msg: error.toString(), stack: error.stack })
+    }
 })
 
 export default sztm

+ 1 - 1
server/src/restServer/routes/users.js

@@ -52,7 +52,7 @@ users.get('/pdf', async (req, res) => {
         })
         res.send("Alles paletti")
     } catch (error) {
-        return res.json({ msg: error.toString() })
+        return res.status(400).json({ msg: error.toString() })
     }
 })