Browse Source

working version 1st weekend.

Tomi Cvetic 6 years ago
parent
commit
5c30d55f7e

+ 1 - 0
.gitignore

@@ -11,6 +11,7 @@ node_modules/
 /data
 /data
 /server/swisstennis_files
 /server/swisstennis_files
 /server/sztm_files
 /server/sztm_files
+/server/core
 
 
 # misc
 # misc
 .DS_Store
 .DS_Store

File diff suppressed because it is too large
+ 256 - 445
client/package-lock.json


+ 6 - 13
client/package.json

@@ -9,20 +9,16 @@
     "babel-preset-env": "^1.7.0",
     "babel-preset-env": "^1.7.0",
     "babel-register": "^6.24.1",
     "babel-register": "^6.24.1",
     "bcryptjs": "^2.4.3",
     "bcryptjs": "^2.4.3",
-    "blob": "^0.0.4",
     "body-parser": "^1.17.2",
     "body-parser": "^1.17.2",
-    "bootstrap": "3",
+    "bootstrap": "4.1.1",
     "connected-react-router": "^4.3.0",
     "connected-react-router": "^4.3.0",
-    "debug": "^2.6.8",
-    "dotenv": "^4.0.0",
+    "debug": "^3.1.0",
+    "dotenv": "^6.0.0",
     "express": "^4.15.3",
     "express": "^4.15.3",
-    "file-saver": "^1.3.3",
     "jwt-simple": "^0.5.1",
     "jwt-simple": "^0.5.1",
     "moment": "^2.18.1",
     "moment": "^2.18.1",
     "mongoose": "^4.11.3",
     "mongoose": "^4.11.3",
     "morgan": "^1.8.2",
     "morgan": "^1.8.2",
-    "passport": "^0.3.2",
-    "passport-jwt": "^2.2.1",
     "react": "^15.5.4",
     "react": "^15.5.4",
     "react-dom": "^15.5.4",
     "react-dom": "^15.5.4",
     "react-redux": "^5.0.5",
     "react-redux": "^5.0.5",
@@ -31,14 +27,11 @@
     "redux": "^3.6.0",
     "redux": "^3.6.0",
     "redux-saga": "^0.15.4",
     "redux-saga": "^0.15.4",
     "request": "^2.81.0",
     "request": "^2.81.0",
-    "request-promise": "^4.2.1",
-    "xlsx": "^0.10.4"
+    "request-promise": "^4.2.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "bhttp": "^1.2.4",
-    "eslint": "^4.19.1",
-    "eslint-plugin-react": "^7.8.2",
-    "html-inline": "^1.2.0",
+    "eslint": "^5.0.1",
+    "eslint-plugin-react": "^7.10.0",
     "react-bootstrap": "^0.31.0",
     "react-bootstrap": "^0.31.0",
     "react-scripts": "^1.1.4"
     "react-scripts": "^1.1.4"
   },
   },

+ 2 - 0
client/src/Main.js

@@ -5,6 +5,7 @@ import Routes from './routes'
 import AlertList from './alerts/components/AlertList'
 import AlertList from './alerts/components/AlertList'
 
 
 /** Import CSS Styles */
 /** Import CSS Styles */
+//import bootstrap from 'bootstrap'
 import 'bootstrap/dist/css/bootstrap.min.css'
 import 'bootstrap/dist/css/bootstrap.min.css'
 import './App.css'
 import './App.css'
 
 
@@ -18,6 +19,7 @@ class Main extends React.Component {
     this.props.swisstennisActions.getCalendarsRequest()
     this.props.swisstennisActions.getCalendarsRequest()
     this.props.swisstennisActions.getCalendarRequest()
     this.props.swisstennisActions.getCalendarRequest()
     this.props.usersActions.getUserListRequest()
     this.props.usersActions.getUserListRequest()
+    this.props.sztmActions.fileListRequest()
     this.props.smsActions.getCreditRequest()
     this.props.smsActions.getCreditRequest()
   }
   }
 
 

+ 7 - 4
client/src/api/state.js

@@ -71,7 +71,7 @@ export function reducer (state = [], action) {
     }
     }
 }
 }
 
 
-function * login (action) {
+export function * login (action) {
     try {
     try {
         const state = yield select()
         const state = yield select()
         console.log('User login requested', state, action) 
         console.log('User login requested', state, action) 
@@ -102,15 +102,18 @@ function * login (action) {
     }
     }
 }
 }
 
 
-function * api (action) {
+export function * api (action) {
     const state = yield select()
     const state = yield select()
     console.log('API requested', action, 'state', state, 'token', token)
     console.log('API requested', action, 'state', state, 'token', token)
-    const { path, method, headers, body, onSuccess, onFailure } = action.data
+    const { path, method, headers, body, params, query, onSuccess, onFailure } = action.data
     if (!state.api.tokenValues || state.api.tokenValues.expires < new Date()) yield put(replace({
     if (!state.api.tokenValues || state.api.tokenValues.expires < new Date()) yield put(replace({
         path: '/login',
         path: '/login',
         state: {}
         state: {}
     }))
     }))
-    const response = yield call(fetch, `${SZTM_API}/${path}`, {
+    const paramsString = params ? '/' + Object.keys(params).map(key => encodeURIComponent(params[key])).join('/') : ''
+    const queryString = query ? '?' + Object.keys(query).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`).join('&') : ''
+
+    const response = yield call(fetch, `${SZTM_API}/${path}${paramsString}${queryString}`, {
         method: method || 'GET',
         method: method || 'GET',
         headers: {
         headers: {
             'Content-Type': 'application/json',
             'Content-Type': 'application/json',

+ 5 - 0
client/src/index.js

@@ -23,6 +23,7 @@ import users from './users'
 import config from './config'
 import config from './config'
 import sms from './sms'
 import sms from './sms'
 import swisstennis from './swisstennis'
 import swisstennis from './swisstennis'
+import sztm from './sztm'
 
 
 /** 
 /** 
  * Browser History
  * Browser History
@@ -42,6 +43,7 @@ const rootReducer = combineReducers({
   users: users.reducer,
   users: users.reducer,
   config: config.reducer,
   config: config.reducer,
   swisstennis: swisstennis.reducer,
   swisstennis: swisstennis.reducer,
+  sztm: sztm.reducer,
   sms: sms.reducer
   sms: sms.reducer
 })
 })
 console.log('Root reducer:', rootReducer)
 console.log('Root reducer:', rootReducer)
@@ -54,6 +56,7 @@ const defaultState = {
   users: users.state,
   users: users.state,
   config: config.state,
   config: config.state,
   swisstennis: swisstennis.state,
   swisstennis: swisstennis.state,
+  sztm: sztm.state,
   sms: sms.state
   sms: sms.state
 }
 }
 console.log('Default state:', defaultState)
 console.log('Default state:', defaultState)
@@ -68,6 +71,7 @@ function * rootSaga () {
     users.saga(),
     users.saga(),
     config.saga(),
     config.saga(),
     swisstennis.saga(),
     swisstennis.saga(),
+    sztm.saga(),
     sms.saga()
     sms.saga()
   ])
   ])
 }
 }
@@ -132,6 +136,7 @@ const actionCreators = {
   users: users.actions,
   users: users.actions,
   config: config.actions,
   config: config.actions,
   swisstennis: swisstennis.actions,
   swisstennis: swisstennis.actions,
+  sztm: sztm.actions,
   sms: sms.actions
   sms: sms.actions
 }
 }
 
 

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

@@ -14,7 +14,7 @@ class Header extends React.Component {
                 <li className="nav-item"><Link to='/swisstennis'>Swisstennis</Link></li>
                 <li className="nav-item"><Link to='/swisstennis'>Swisstennis</Link></li>
                 <li className="nav-item"><Link to='/players'>Spieler</Link></li>
                 <li className="nav-item"><Link to='/players'>Spieler</Link></li>
                 <li className="nav-item"><Link to='/matches'>Matches</Link></li>
                 <li className="nav-item"><Link to='/matches'>Matches</Link></li>
-                <li className="nav-item"><Link to='/pdfs'>Listen</Link></li>
+                <li className="nav-item"><Link to='/sztm'>SZTM</Link></li>
                 <li className="nav-item"><Link to='/sms'>SMS</Link></li>
                 <li className="nav-item"><Link to='/sms'>SMS</Link></li>
             </ul>
             </ul>
         </nav>
         </nav>

+ 3 - 3
client/src/routes.js

@@ -3,9 +3,8 @@ import { Switch, Route } from 'react-router'
 import UserList from './users/components/UserList'
 import UserList from './users/components/UserList'
 import LoginForm from './api/components/Login'
 import LoginForm from './api/components/Login'
 import ConfigList from './config/components/ConfigList'
 import ConfigList from './config/components/ConfigList'
-import Matches from './swisstennis/components/Matches'
-import Players from './swisstennis/components/Players'
-import Swisstennis from './swisstennis/components/Swisstennis'
+import { Swisstennis, Matches, Players } from './swisstennis/components'
+import SZTM from './sztm/components/SZTM'
 import SMS from './sms/components/SMS'
 import SMS from './sms/components/SMS'
 
 
 class Routes extends React.Component {
 class Routes extends React.Component {
@@ -20,6 +19,7 @@ class Routes extends React.Component {
             <Route path='/matches' render={() => (<Matches {...this.props}/>)} />
             <Route path='/matches' render={() => (<Matches {...this.props}/>)} />
             <Route path='/players' render={() => (<Players {...this.props}/>)} />
             <Route path='/players' render={() => (<Players {...this.props}/>)} />
             <Route path='/swisstennis' render={() => (<Swisstennis {...this.props}/>)} />
             <Route path='/swisstennis' render={() => (<Swisstennis {...this.props}/>)} />
+            <Route path='/sztm' render={() => (<SZTM {...this.props}/>)} />
             <Route path='/sms' render={() => (<SMS {...this.props}/>)} />
             <Route path='/sms' render={() => (<SMS {...this.props}/>)} />
         </Switch>
         </Switch>
     </main>
     </main>

+ 16 - 12
client/src/sms/components/SMS.js

@@ -8,15 +8,9 @@ class SMS extends React.Component {
     this.submitForm = this.submitForm.bind(this)
     this.submitForm = this.submitForm.bind(this)
   }
   }
 
 
-  componentDidMount () {
-    const { getCreditRequest } = this.props.smsActions
-    getCreditRequest()
-  }
-
   handleChange (event) {
   handleChange (event) {
     event.preventDefault()
     event.preventDefault()
-    const { changeForm } = this.props.smsActions
-    changeForm({
+    this.props.smsActions.changeForm({
       sender: this.sender.value,
       sender: this.sender.value,
       body: this.body.value,
       body: this.body.value,
       newRecipient: this.newRecipient.value
       newRecipient: this.newRecipient.value
@@ -25,15 +19,23 @@ class SMS extends React.Component {
 
 
   submitForm (event) {
   submitForm (event) {
     event.preventDefault()
     event.preventDefault()
-    const { sendSMSRequest } = this.props.smsActions
     const state = this.props.sms
     const state = this.props.sms
-    sendSMSRequest(state)
+    this.props.smsActions.sendSMSRequest(state)
   }
   }
 
 
   addRecipient (event) {
   addRecipient (event) {
     event.preventDefault()
     event.preventDefault()
-    const { addRecipient } = this.props.smsActions
-    addRecipient(this.newRecipient.value)
+    this.props.smsActions.addRecipient(this.newRecipient.value)
+  }
+
+  removeRecipient (key, event) {
+    event.preventDefault()
+    this.props.smsActions.removeRecipient(key)
+  }
+
+  setRecipients (value, event) {
+    event.preventDefault()
+    this.props.smsActions.setRecipients(value)
   }
   }
 
 
   render () {
   render () {
@@ -59,9 +61,11 @@ class SMS extends React.Component {
           <input type="submit" value="Senden" onClick={this.submitForm}/>
           <input type="submit" value="Senden" onClick={this.submitForm}/>
         </form>
         </form>
         <h3>Alle Empfaenger</h3>
         <h3>Alle Empfaenger</h3>
+        <p>{state.recipients.filter(recipient => (typeof recipient === 'string') || recipient.phone).length} von {state.recipients.length} mit Telefonnummer</p>
+        {state.recipients.length ? <p><a onClick={(event) => {this.setRecipients([], event)}}>{'\u2716'}</a> Alle löschen</p> : null}
         <ul>
         <ul>
           {state.recipients.map((recipient, key) => 
           {state.recipients.map((recipient, key) => 
-          <li key={key}>{(typeof recipient === 'string') ? recipient : `${recipient.fullName} (${recipient.phone ? recipient.phone : 'keine Nummer'})`}</li>
+          <li key={key}><a onClick={(event) => {this.removeRecipient(key, event)}}>{'\u2716'}</a> {(typeof recipient === 'string') ? recipient : `${recipient.fullName} (${recipient.phone ? recipient.phone : 'keine Nummer'})`}</li>
           )}
           )}
         </ul>
         </ul>
       </div>
       </div>

+ 11 - 4
client/src/sms/state.js

@@ -30,6 +30,12 @@ export const actions = {
       data
       data
     }
     }
   },
   },
+  removeRecipient: (data) => {
+    return {
+      type: 'SMS/REMOVE_RECIPIENT',
+      data
+    }
+  },
   sendSMSRequest: (data) => {
   sendSMSRequest: (data) => {
     return {
     return {
       type: 'SMS/SEND_REQUEST',
       type: 'SMS/SEND_REQUEST',
@@ -87,16 +93,17 @@ export function reducer (state = [], action) {
     case 'SMS/CHANGE_FORM':
     case 'SMS/CHANGE_FORM':
       return { ...state, ...action.data }
       return { ...state, ...action.data }
     case 'SMS/SET_RECIPIENTS':
     case 'SMS/SET_RECIPIENTS':
-      const recipients = action.data.map(player => player.phone).filter(number => !!number)
-      console.log(recipients)
-      return { ...state, recipients }
+      return { ...state, recipients: action.data }
     case 'SMS/ADD_RECIPIENT':
     case 'SMS/ADD_RECIPIENT':
       const number = normalizePhone(action.data)
       const number = normalizePhone(action.data)
       if (number) {
       if (number) {
         return { ...state, recipients: [ number, ...state.recipients ], newRecipient: '' }
         return { ...state, recipients: [ number, ...state.recipients ], newRecipient: '' }
       } else {
       } else {
         return state
         return state
-      }      
+      }  
+    case 'SMS/REMOVE_RECIPIENT':
+      console.log(action.data)
+      return { ...state, recipients: [ ...state.recipients.slice(0, action.data), ...state.recipients.slice(action.data + 1) ] }
     case 'SMS/SEND_REQUEST':
     case 'SMS/SEND_REQUEST':
       return { ...state, sending: true }
       return { ...state, sending: true }
     case 'SMS/SEND_SUCCESS':
     case 'SMS/SEND_SUCCESS':

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

@@ -80,7 +80,7 @@ class Swisstennis extends React.Component {
             <tr key={key}>
             <tr key={key}>
               <td>{file.filename}</td>
               <td>{file.filename}</td>
               <td>{file.size/1024}kB</td>
               <td>{file.size/1024}kB</td>
-              <td>{moment(file.ctime).format('DD.MM.YYYY HH:mm')}</td>
+              <td>{moment(file.mtime).format('DD.MM.YYYY HH:mm')}</td>
               <td>{(state.calendars.find(matchList => matchList.file === file.filename) || state.playerLists.find(playerList => playerList.file === file.filename)) ? 'Ja' : 'Nein'}</td>
               <td>{(state.calendars.find(matchList => matchList.file === file.filename) || state.playerLists.find(playerList => playerList.file === file.filename)) ? 'Ja' : 'Nein'}</td>
               <td>
               <td>
                 <a onClick={(event) => {
                 <a onClick={(event) => {

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

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

+ 6 - 6
client/src/swisstennis/state.js

@@ -401,7 +401,7 @@ export function reducer (state = [], action) {
 
 
 function * filterPlayers (action) {
 function * filterPlayers (action) {
   const state = yield select()
   const state = yield select()
-  const { playerList, calendar } = state.swisstennis
+  const { playerList } = state.swisstennis
   const filter = action.data
   const filter = action.data
   const filteredPlayers = playerList ? playerList.players.filter(player => {
   const filteredPlayers = playerList ? playerList.players.filter(player => {
     return (
     return (
@@ -418,7 +418,7 @@ function * filterPlayers (action) {
 
 
 function * filterMatches (action) {
 function * filterMatches (action) {
   const state = yield select()
   const state = yield select()
-  const { playerList, calendar } = state.swisstennis
+  const { calendar } = state.swisstennis
   const filter = action.data
   const filter = action.data
   const filteredMatches = calendar ? calendar.matches.filter(match => {
   const filteredMatches = calendar ? calendar.matches.filter(match => {
     return (
     return (
@@ -455,7 +455,7 @@ function * login (action) {
         'x-access-token': token
         'x-access-token': token
       }
       }
     }) 
     }) 
-    if (response.status != 200) {
+    if (response.status !== 200) {
       console.log(response)
       console.log(response)
       throw Error(response.status)
       throw Error(response.status)
     } else {
     } else {
@@ -498,7 +498,7 @@ function * downloadCalendar (action) {
         'x-access-token': token
         'x-access-token': token
       }
       }
     }) 
     }) 
-    if (response.status != 200) {
+    if (response.status !== 200) {
       throw Error(response.status)
       throw Error(response.status)
     } else {
     } else {
       const responseJson = yield response.json()
       const responseJson = yield response.json()
@@ -557,7 +557,7 @@ function * downloadPlayerList (action) {
         'x-access-token': token
         'x-access-token': token
       }
       }
     }) 
     }) 
-    if (response.status != 200) {
+    if (response.status !== 200) {
       throw Error(response.status)
       throw Error(response.status)
     } else {
     } else {
       const responseJson = yield response.json()
       const responseJson = yield response.json()
@@ -580,7 +580,7 @@ function * parsePlayerList (action) {
         'x-access-token': token
         'x-access-token': token
       }
       }
     })
     })
-    if (response.status != 200) {
+    if (response.status !== 200) {
       throw Error(response.status)
       throw Error(response.status)
     } else {
     } else {
       const responseJson = yield response.json()
       const responseJson = yield response.json()

+ 95 - 0
client/src/sztm/components/SZTM.js

@@ -0,0 +1,95 @@
+import React from 'react'
+import moment from 'moment'
+import { SZTM_API } from '../../local-constants'
+
+class SZTM extends React.Component {
+  constructor() {
+    super()
+    this.handleChange = this.handleChange.bind(this)
+    this.setDate = this.setDate.bind(this)
+  }
+
+  handleChange (event) {
+    event.preventDefault()
+  }
+
+  setDate (date, event) {
+    event.preventDefault()
+    this.props.sztmActions.setFilter({ date })
+  }
+
+  generateLists (places, date, matchListId, event) {
+    event.preventDefault()
+    this.props.sztmActions.resetProgress()
+    places.forEach(place => {
+      if (place) this.props.sztmActions.generateListRequest({place, date: moment.unix(date).format('YYYYMMDDTHHmmss'), matchListId})
+    })
+  }
+
+  indicate (status) {
+    if (status === 'uninitialized') {
+      return (<span>{'\u2610'}</span>)
+    } else if (status === 'request') {
+      return (<span style={{color: 'grey'}}>{'\u2610'}</span>)
+    } else if (status === 'success') {
+      return (<span style={{color: 'green'}}>{'\u2611'}</span>)
+    } else if (status === 'failure') {
+      return (<span style={{color: 'red'}}>{'\u2612'}</span>)
+    }
+  }
+
+  render () {
+    const state = this.props.sztm
+    const actions = this.props.sztmActions
+    const calendar = this.props.swisstennis.calendar
+    const { filteredFiles, filter, progress } = state
+    const dates = []
+    const places = []
+    calendar && calendar.matches.forEach(match => {
+      const date = moment(match.date).startOf('day').unix()
+      if (!dates.includes(date)) dates.push(date)
+      if (!places.includes(match.place)) places.push(match.place)
+    })
+    
+    return (
+      <div>
+        <h2>Filter</h2>
+        {dates.map((date, key) => <a key={key} onClick={(event) => this.setDate(date, event)} style={filter.date === date ? {fontWeight: 'bold'} : {}}>{moment.unix(date).format('DD.MM.')}</a>)}
+        <p>Calendar: {calendar && <span>{calendar.file} ({moment(calendar.imported).format('DD.MM. HH:mm')})</span>}</p>
+        <p>
+          <button onClick={(event) => this.generateLists(places, filter.date, calendar._id, event)} disabled={!filter.date}>Listen generieren</button>
+          <progress value={progress.length} max={places.length}>{progress.length / places.length * 100}%</progress>
+        </p>
+        <p><button onClick={(event) => {
+          event.preventDefault()
+          actions.generateZipRequest({date: moment.unix(filter.date).format('YYYYMMDDTHHmmss')})
+        }} disabled={!filter.date}>ZIP generieren</button></p>
+        <h2>Dateien</h2>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Name</th><th>Grösse</th><th>Datum</th><th>Aktionen</th>
+            </tr>
+          </thead>
+          <tbody>
+            {filteredFiles.map((file, key) => (
+            <tr key={key}>
+              <td><a href={`${SZTM_API}/download/${file.filename}`}>{file.filename}</a></td>
+              <td>{file.size/1024}kB</td>
+              <td>{moment(file.mtime).format('DD.MM.YYYY HH:mm')}</td>
+              <td>
+                <a onClick={(event) => {
+                  event.preventDefault()
+                  if (file.filename.includes('Calendar')) actions.parseCalendarRequest({filename: file.filename})
+                  if (file.filename.includes('PlayerList')) actions.parsePlayerListRequest({filename: file.filename})
+                }}>parsen</a> <a>loeschen</a>
+              </td>
+            </tr>))}
+          </tbody>
+        </table>
+      </div>
+    )
+  }
+}
+
+export default SZTM

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

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

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

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

+ 167 - 0
client/src/sztm/state.js

@@ -0,0 +1,167 @@
+/** @module swisstennis/state */
+import { put, takeEvery } from 'redux-saga/effects'
+import api from '../api'
+import moment from 'moment'
+
+/**
+ * state.js
+ *
+ * Collection of everything which has to do with state changes.
+ **/
+
+/** actionTypes define what actions are handeled by the reducer. */
+export const actions = {
+  setFilter: (data) => {
+    return {
+      type: 'SZTM/SET_FILTER',
+      data
+    }
+  },
+  fileListRequest: () => {
+    return {
+      type: 'SZTM/FILE_LIST_REQUEST',
+    }
+  },
+  fileListSuccess: data => {
+    return {
+      type: 'SZTM/FILE_LIST_SUCCESS',
+      data
+    }
+  },
+  fileListFailure: error => {
+    return {
+      type: 'SZTM/FILE_LIST_FAILURE',
+      error
+    }
+  },
+  resetProgress: () => {
+    return {
+      type: 'SZTM/RESET_PROGRESS'
+    }
+  },
+  generateListRequest: (data) => {
+    return {
+      type: 'SZTM/GENERATE_LIST_REQUEST',
+      data
+    }
+  },
+  generateListSuccess: (data) => {
+    return {
+      type: 'SZTM/GENERATE_LIST_SUCCESS',
+      data
+    }
+  },
+  generateListFailure: error => {
+    return {
+      type: 'SZTM/GENERATE_LIST_FAILURE',
+      error
+    }
+  },
+  generateZipRequest: (data) => {
+    return {
+      type: 'SZTM/GENERATE_ZIP_REQUEST',
+      data
+    }
+  },
+  generateZipSuccess: (data) => {
+    return {
+      type: 'SZTM/GENERATE_ZIP_SUCCESS',
+      data
+    }
+  },
+  generateZipFailure: error => {
+    return {
+      type: 'SZTM/GENERATE_ZIP_FAILURE',
+      error
+    }
+  },
+}
+console.log('State actions', actions)
+
+/** state definition */
+const emptyFilter = {
+  date: '',
+}
+
+export const state = {
+  files: [],
+  fileListStatus: 'uninitialized',
+  filteredFiles: [],
+  filter: emptyFilter,
+  progress: []
+}
+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 'SZTM/SET_FILTER':
+      const filter = { ...state.filter, ...action.data }
+      const filteredFiles = state.files.filter(file => {
+        const dateString = moment.unix(filter.date).format('YYYYMMDD')
+        return filter.date ? file.filename.includes(dateString) : true
+      })
+      return { ...state, filter, filteredFiles }
+    case 'SZTM/FILE_LIST_REQUEST':
+      return { ...state, fileListStatus: 'request' }
+    case 'SZTM/FILE_LIST_SUCCESS':
+      return { ...state, fileListStatus: 'success', files: action.data }
+    case 'SZTM/FILE_LIST_FAILURE':
+      return { ...state, fileListStatus: 'failure' }
+    case 'SZTM/RESET_PROGRESS':
+      return { ...state, progress: [] }
+    case 'SZTM/GENERATE_LIST_REQUEST':
+      return { ...state, playerListDownloadStatus: 'request' }
+    case 'SZTM/GENERATE_LIST_SUCCESS':
+      return { ...state, playerListDownloadStatus: 'success', 
+        files: [ ...state.files, action.data.paylistFile, action.data.scheduleFile ],
+        progress: [ ...state.progress, action.data.place ] }
+    case 'SZTM/GENERATE_LIST_FAILURE':
+      return { ...state, playerListDownloadStatus: 'failure' }
+    case 'SZTM/GENERATE_ZIP_REQUEST':
+      return { ...state, playerListDownloadStatus: 'request' }
+    case 'SZTM/GENERATE_ZIP_SUCCESS':
+      return { ...state, playerListDownloadStatus: 'success', files: [ ...state.files, action.data ] }
+    case 'SZTM/GENERATE_ZIP_FAILURE':
+      return { ...state, playerListDownloadStatus: 'failure' }
+    default:
+      return state
+  }
+}
+
+function * fileList (action) {
+  yield put(api.actions.apiRequest({
+    path: `api/sztm/files`,
+    method: 'GET',
+    onSuccess: [ actions.fileListSuccess, actions.setFilter ],
+    onFailure: actions.fileListFailure
+  }))
+}
+
+function * generateList (action) {
+  yield put(api.actions.apiRequest({
+    path: `api/sztm/pdf`,
+    method: 'POST',
+    body: JSON.stringify(action.data),
+    onSuccess: [ actions.generateListSuccess, actions.setFilter ],
+    onFailure: actions.generateListFailure
+  }))
+}
+
+function * generateZip (action) {
+  yield put(api.actions.apiRequest({
+    path: `api/sztm/zip`,
+    method: 'POST',
+    body: JSON.stringify(action.data),
+    onSuccess: [ actions.generateZipSuccess, actions.setFilter ],
+    onFailure: actions.generateZipFailure
+  }))
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {
+  console.log('Config saga started.')
+  yield takeEvery('SZTM/FILE_LIST_REQUEST', fileList)
+  yield takeEvery('SZTM/GENERATE_LIST_REQUEST', generateList)
+  yield takeEvery('SZTM/GENERATE_ZIP_REQUEST', generateZip)
+}

+ 186 - 5
server/package-lock.json

@@ -79,6 +79,34 @@
         "normalize-path": "^2.1.1"
         "normalize-path": "^2.1.1"
       }
       }
     },
     },
+    "archiver": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/archiver/-/archiver-2.1.1.tgz",
+      "integrity": "sha1-/2YrSnggFJSj7lRNOjP+dJZQnrw=",
+      "requires": {
+        "archiver-utils": "^1.3.0",
+        "async": "^2.0.0",
+        "buffer-crc32": "^0.2.1",
+        "glob": "^7.0.0",
+        "lodash": "^4.8.0",
+        "readable-stream": "^2.0.0",
+        "tar-stream": "^1.5.0",
+        "zip-stream": "^1.2.0"
+      }
+    },
+    "archiver-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz",
+      "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=",
+      "requires": {
+        "glob": "^7.0.0",
+        "graceful-fs": "^4.1.0",
+        "lazystream": "^1.0.0",
+        "lodash": "^4.8.0",
+        "normalize-path": "^2.0.0",
+        "readable-stream": "^2.0.0"
+      }
+    },
     "arr-diff": {
     "arr-diff": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -1359,6 +1387,15 @@
       "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
       "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
       "dev": true
       "dev": true
     },
     },
+    "bl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+      "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==",
+      "requires": {
+        "readable-stream": "^2.3.5",
+        "safe-buffer": "^5.1.1"
+      }
+    },
     "blob": {
     "blob": {
       "version": "0.0.4",
       "version": "0.0.4",
       "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
       "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
@@ -1556,11 +1593,35 @@
       "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz",
       "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz",
       "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ=="
       "integrity": "sha512-D8zmlb46xfuK2gGvKmUjIklQEouN2nQ0LEHHeZ/NoHM2LDiMk2EYzZ5Ntw/Urk+bgMDosOZxaRzXxvhI5TcAVQ=="
     },
     },
+    "buffer-alloc": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
+      "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
+      "requires": {
+        "buffer-alloc-unsafe": "^1.1.0",
+        "buffer-fill": "^1.0.0"
+      }
+    },
+    "buffer-alloc-unsafe": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
+      "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg=="
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI="
+    },
     "buffer-equal": {
     "buffer-equal": {
       "version": "0.0.1",
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
       "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
       "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs="
       "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs="
     },
     },
+    "buffer-fill": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
+      "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw="
+    },
     "buffer-from": {
     "buffer-from": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.0.0.tgz",
@@ -1760,6 +1821,17 @@
       "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
       "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=",
       "dev": true
       "dev": true
     },
     },
+    "compress-commons": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz",
+      "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=",
+      "requires": {
+        "buffer-crc32": "^0.2.1",
+        "crc32-stream": "^2.0.0",
+        "normalize-path": "^2.0.0",
+        "readable-stream": "^2.0.0"
+      }
+    },
     "concat-map": {
     "concat-map": {
       "version": "0.0.1",
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1840,6 +1912,11 @@
         "vary": "^1"
         "vary": "^1"
       }
       }
     },
     },
+    "crc": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz",
+      "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ="
+    },
     "crc-32": {
     "crc-32": {
       "version": "1.1.1",
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.1.1.tgz",
       "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.1.1.tgz",
@@ -1849,6 +1926,15 @@
         "printj": "~1.1.0"
         "printj": "~1.1.0"
       }
       }
     },
     },
+    "crc32-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz",
+      "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=",
+      "requires": {
+        "crc": "^3.4.4",
+        "readable-stream": "^2.0.0"
+      }
+    },
     "create-error-class": {
     "create-error-class": {
       "version": "3.0.2",
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
       "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
@@ -2084,6 +2170,14 @@
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
       "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
       "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
     },
     },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+      "requires": {
+        "once": "^1.4.0"
+      }
+    },
     "errors": {
     "errors": {
       "version": "0.2.0",
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/errors/-/errors-0.2.0.tgz",
       "resolved": "https://registry.npmjs.org/errors/-/errors-0.2.0.tgz",
@@ -2601,6 +2695,16 @@
       "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
       "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=",
       "dev": true
       "dev": true
     },
     },
+    "fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
     "fsevents": {
     "fsevents": {
       "version": "1.2.4",
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
@@ -3155,6 +3259,19 @@
         "assert-plus": "^1.0.0"
         "assert-plus": "^1.0.0"
       }
       }
     },
     },
+    "glob": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+      "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
     "glob-parent": {
     "glob-parent": {
       "version": "3.1.0",
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
@@ -3212,8 +3329,7 @@
     "graceful-fs": {
     "graceful-fs": {
       "version": "4.1.11",
       "version": "4.1.11",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
-      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
-      "dev": true
+      "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
     },
     },
     "har-schema": {
     "har-schema": {
       "version": "2.0.0",
       "version": "2.0.0",
@@ -3519,6 +3635,15 @@
       "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
       "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=",
       "dev": true
       "dev": true
     },
     },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
     "inherits": {
     "inherits": {
       "version": "2.0.3",
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@@ -3861,6 +3986,14 @@
         "package-json": "^4.0.0"
         "package-json": "^4.0.0"
       }
       }
     },
     },
+    "lazystream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz",
+      "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=",
+      "requires": {
+        "readable-stream": "^2.0.5"
+      }
+    },
     "levn": {
     "levn": {
       "version": "0.3.0",
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@@ -4232,7 +4365,6 @@
       "version": "2.1.1",
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
       "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
       "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
-      "dev": true,
       "requires": {
       "requires": {
         "remove-trailing-separator": "^1.0.1"
         "remove-trailing-separator": "^1.0.1"
       }
       }
@@ -4334,6 +4466,14 @@
       "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
       "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
       "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
       "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
     },
     },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
     "optionator": {
     "optionator": {
       "version": "0.8.2",
       "version": "0.8.2",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
       "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
@@ -4759,8 +4899,7 @@
     "remove-trailing-separator": {
     "remove-trailing-separator": {
       "version": "1.1.0",
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
       "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
-      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
-      "dev": true
+      "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
     },
     },
     "repeat-element": {
     "repeat-element": {
       "version": "1.1.2",
       "version": "1.1.2",
@@ -5438,6 +5577,27 @@
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
       "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
       "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
     },
     },
+    "tar-stream": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz",
+      "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==",
+      "requires": {
+        "bl": "^1.0.0",
+        "buffer-alloc": "^1.1.0",
+        "end-of-stream": "^1.0.0",
+        "fs-constants": "^1.0.0",
+        "readable-stream": "^2.3.0",
+        "to-buffer": "^1.1.0",
+        "xtend": "^4.0.0"
+      },
+      "dependencies": {
+        "xtend": {
+          "version": "4.0.1",
+          "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+          "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
+        }
+      }
+    },
     "term-size": {
     "term-size": {
       "version": "1.2.0",
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
       "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
@@ -5513,6 +5673,11 @@
       "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz",
       "integrity": "sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c="
       "integrity": "sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c="
     },
     },
+    "to-buffer": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
+      "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
+    },
     "to-fast-properties": {
     "to-fast-properties": {
       "version": "1.0.3",
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
       "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz",
@@ -5938,6 +6103,11 @@
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
       "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
       "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
       "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
     },
     },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
     "write-file-atomic": {
     "write-file-atomic": {
       "version": "2.3.0",
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
       "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
@@ -5979,6 +6149,17 @@
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
       "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
       "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
       "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
       "dev": true
       "dev": true
+    },
+    "zip-stream": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz",
+      "integrity": "sha1-qLxF9MG0lpnGuQGYuqyqzbzUugQ=",
+      "requires": {
+        "archiver-utils": "^1.3.0",
+        "compress-commons": "^1.2.0",
+        "lodash": "^4.8.0",
+        "readable-stream": "^2.0.0"
+      }
     }
     }
   }
   }
 }
 }

+ 1 - 0
server/package.json

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "version": "0.1.0",
   "private": true,
   "private": true,
   "dependencies": {
   "dependencies": {
+    "archiver": "^2.1.1",
     "await-fs": "^1.0.0",
     "await-fs": "^1.0.0",
     "babel-core": "^6.25.0",
     "babel-core": "^6.25.0",
     "babel-polyfill": "^6.23.0",
     "babel-polyfill": "^6.23.0",

+ 3 - 3
server/src/restServer/routes/swisstennis.js

@@ -161,7 +161,7 @@ swisstennis.get('/playerlist/download', async (req, res) => {
     })
     })
     await streamDone
     await streamDone
     const stats = fs.statSync(`swisstennis_files/${filename}`)
     const stats = fs.statSync(`swisstennis_files/${filename}`)
-    return res.json({ filename, size: stats.size, ctime: stats.ctime })
+    return res.json({ filename, size: stats.size, mtime: stats.mtime })
   } catch (error) {
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
     return res.status(400).json({ msg: error.toString() })
   }
   }
@@ -282,7 +282,7 @@ swisstennis.get('/files', async (req, res) => {
     return tournament ? filename.includes(tournament) : true
     return tournament ? filename.includes(tournament) : true
   }).map(filename => {
   }).map(filename => {
     const stats = fs.statSync(`swisstennis_files/${filename}`)
     const stats = fs.statSync(`swisstennis_files/${filename}`)
-    return { filename, size: stats.size, ctime: stats.ctime }
+    return { filename, size: stats.size, mtime: stats.mtime }
   })
   })
   return res.json(fileList)
   return res.json(fileList)
 })
 })
@@ -339,7 +339,7 @@ swisstennis.get('/calendar/download', async (req, res) => {
     })
     })
     await streamDone
     await streamDone
     const stats = fs.statSync(`swisstennis_files/${filename}`)
     const stats = fs.statSync(`swisstennis_files/${filename}`)
-    return res.json({ filename, size: stats.size, ctime: stats.ctime })
+    return res.json({ filename, size: stats.size, mtime: stats.mtime })
   } catch (error) {
   } catch (error) {
     return res.status(400).json({ msg: error.toString() })
     return res.status(400).json({ msg: error.toString() })
   }
   }

+ 195 - 190
server/src/restServer/routes/sztm.js

@@ -1,234 +1,239 @@
 import fs from 'fs'
 import fs from 'fs'
+import awaitFs from 'await-fs'
 import express from 'express'
 import express from 'express'
 import moment from 'moment'
 import moment from 'moment'
+import pdfMake from 'pdfmake/build/pdfmake'
+import pdfFonts from 'pdfmake/build/vfs_fonts'
+import archiver from 'archiver'
 import { places } from '../config/sztm'
 import { places } from '../config/sztm'
 import Match from '../models/match'
 import Match from '../models/match'
 import Player from '../models/player'
 import Player from '../models/player'
 import MatchList from '../models/matchList'
 import MatchList from '../models/matchList'
 import PlayerList from '../models/playerList'
 import PlayerList from '../models/playerList'
-import pdfMake from "pdfmake/build/pdfmake";
-import pdfFonts from "pdfmake/build/vfs_fonts";
-pdfMake.vfs = pdfFonts.pdfMake.vfs;
+import { resolve } from 'path';
+pdfMake.vfs = pdfFonts.pdfMake.vfs
 
 
 const sztm = express.Router() 
 const sztm = express.Router() 
 
 
-sztm.get('/schedule', async (req, res) => {
-    try{
-        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 })
-        
-        console.log('matches', matchList.matches.length)
-        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
-        }*/
+sztm.get('/files', async (req, res) => {
+  const dirContent = await awaitFs.readdir('sztm_files')
+  const fileList = dirContent.map(filename => {
+    const stats = fs.statSync(`sztm_files/${filename}`)
+    return { filename, size: stats.size, mtime: stats.mtime }
+  })
+  return res.json(fileList)
+})
 
 
-        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 })
-        
-    } catch (error) {
-        res.status(400).json({ msg: error.toString() })
-    }
+sztm.delete('/files', async (req, res) => {
+  try {
+    const { filename } = req.body
+    fs.unlink(`sztm_files/${filename}`, (error) => {
+      if (error) throw error
+      res.json({ msg: `successfully deleted sztm_files/${filename}.` })
+    })
+  } catch (error) {
+    return 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 })
+sztm.post('/zip', async (req, res) => {
+    try {
+        const { date } = req.body
+        if (!date) {
+            throw Error('date is mandatory.')
+        }
         
         
-        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 parsedDate = date.match(/(\d{4}\d{2}\d{2})T\d{2}\d{2}\d{2}/)[1]
+        const zipFile = `sztm_file_${parsedDate}.zip`
+        const zipStream = fs.createWriteStream(`sztm_files/${zipFile}`)
+        const archive = archiver('zip', { zlib: { level: 9 } })
 
 
-        const players = await Player.find(query).sort({ date: 1 })
-        console.log('players', players.length)
-        
-        res.json({ players })
-        
+        const stream = new Promise((resolve, reject) => {
+            zipStream.on('close', resolve)
+            zipStream.on('error', reject)
+        })
+
+        archive.on('error', (error) => { throw error })
+        archive.pipe(zipStream)
+
+        archive.glob(`*-${parsedDate}.pdf`, { cwd: 'sztm_files/' })
+        archive.finalize()
+
+        await stream
+        const stats = fs.statSync(`sztm_files/${zipFile}`)
+        return res.json({ filename: zipFile, size: stats.size, mtime: stats.mtime })
     } catch (error) {
     } catch (error) {
-        res.status(400).json({ msg: error.toString() })
+        return res.status(400).json({ msg: error.toString(), stack: error.stack })
     }
     }
 })
 })
 
 
 sztm.post('/pdf', async (req, res) => {
 sztm.post('/pdf', async (req, res) => {
     try {
     try {
-        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 { matchListId, date, place } = req.body
+        if (!matchListId || !date || !place) {
+            console.log('matchListId, date and place are mandatory.', req.body)
+            throw Error('matchListId, date and place are mandatory.')
+        }
         
         
-        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 matchList = await MatchList.findOne({ _id: matchListId })
+        const dateString = date.match(/(\d{4}\d{2}\d{2})T\d{2}\d{2}\d{2}/)[1]
+        const parsedDate = moment(dateString)
+        console.log('Date', parsedDate, dateString, moment(new Date()))
+        const query = { 
+            _id: { $in: matchList.matches },
+            place,
+            date: {
+                $gte: parsedDate.toDate(),
+                $lt: parsedDate.add(1, 'day').toDate()
+            }
         }
         }
         
         
         const matches = await Match.find(query).sort({ date: 1 }).populate('player1').populate('player2')
         const matches = await Match.find(query).sort({ date: 1 }).populate('player1').populate('player2')
         const players = []
         const players = []
+        const allPlaces = []
         matches.forEach(match => {
         matches.forEach(match => {
             if (match.player1) players.push(match.player1)
             if (match.player1) players.push(match.player1)
             if (match.player2) players.push(match.player2)
             if (match.player2) players.push(match.player2)
+            if (match.place && !allPlaces.includes(match.place)) allPlaces.push(match.place)
         })
         })
-
-        const allPlaces = []
-        matches.forEach(match => {
-            if (!!match.place && !allPlaces.includes(match.place)) allPlaces.push(match.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}`, ''])
         })
         })
-
-        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)'}
+        const paylistDoc = {
+            pageSize: 'A4',
+            pageOrientation: 'portrait',
+            content: [
+                { text: 'Stadtzürcher Tennismeisterschaft', style: 'header' },
+                { text: `Nenngelder für den ${moment(parsedDate).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
                     ]},
                     ]},
-                    { 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]
-                    }
+                    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 && match.player1.ranking, 
-                                match.player2 && match.player2.fullName, match.player2 && 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 scheduleDoc = {
+            pageSize: 'A4',
+            pageOrientation: 'landscape',
+            header: {
+                stack: [
+                    { text: `Spielplan für den ${moment(parsedDate).format('DD.MM.YYYY')} (${places[place]})`, style: 'subheader' }
+                ],
+                margin: [40, 0]
+            },
+            content: [
+                { text: 'Stadtzürcher Tennismeisterschaft', style: 'header' },
+                { 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 && match.player1.ranking, 
+                            match.player2 && match.player2.fullName, match.player2 && 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)))
-            })
+        }
 
 
+        const paylistGenerator = pdfMake.createPdf(paylistDoc)
+        const paylistFile = `Zahlliste-${place}-${dateString}.pdf`
+        paylistGenerator.getBuffer((buffer) => {
+            fs.writeFileSync(`sztm_files/${paylistFile}`, new Buffer(new Uint8Array(buffer)))
         })
         })
+        const scheduleGenerator = pdfMake.createPdf(scheduleDoc)
+        const scheduleFile = `Spielliste-${place}-${dateString}.pdf`
+        scheduleGenerator.getBuffer((buffer) => {
+            fs.writeFileSync(`sztm_files/${scheduleFile}`, new Buffer(new Uint8Array(buffer)))
+        })
+
+        /*
+        const payListStats = fs.statSync(`sztm_files/${paylistFile}`)
+        const scheduleStats = fs.statSync(`sztm_files/${scheduleFile}`)
         
         
-        res.json({ msg: "All PDFs generated." })
+        res.json({ 
+            place,
+            paylistFile: {filename: paylistFile, size: payListStats.size, mtime: payListStats.mtime}, 
+            scheduleFile: {filename: scheduleFile, size: scheduleStats.size, mtime: scheduleStats.mtime} 
+        }) */
+        
+        res.json({ 
+            place,
+            paylistFile: {filename: paylistFile, size: 0, mtime: new Date()}, 
+            scheduleFile: {filename: scheduleFile, size: 0, mtime: new Date()} 
+        })
     } catch (error) {
     } catch (error) {
-        return res.json({ msg: error.toString(), stack: error.stack })
+        return res.status(400).json({ msg: error.toString(), stack: error.stack })
     }
     }
 })
 })
 
 

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