Tomi Cvetic 7 年之前
父節點
當前提交
2a79ab8511

+ 2 - 0
.env

@@ -0,0 +1,2 @@
+DEBUG=SZTM*
+REACT_APP_DEBUG=SZTM*

+ 3 - 0
package.json

@@ -5,12 +5,15 @@
   "dependencies": {
     "blob": "^0.0.4",
     "bootstrap": "3",
+    "debug": "^2.6.8",
+    "dotenv": "^4.0.0",
     "file-saver": "^1.3.3",
     "moment": "^2.18.1",
     "react": "^15.5.4",
     "react-dom": "^15.5.4",
     "react-redux": "^5.0.5",
     "redux": "^3.6.0",
+    "redux-saga": "^0.15.4",
     "xlsx": "^0.10.4"
   },
   "devDependencies": {

+ 2 - 0
src/.env

@@ -0,0 +1,2 @@
+DEBUG=SZTM*
+REACT_APP_DEBUG=SZTM*

+ 18 - 405
src/Main.js

@@ -1,418 +1,31 @@
 import React from 'react'           // React to manage the GUI
-import Player from './classes/player'
-import Match from './classes/match'
-import PlayerList from './playerList'       // Everything that has to do with players
-import MatchList from './matchList'         // Everything that has to do with matches
-import Excel from './excel'         // Helper files to create Excel files
-import { date2s, time2s } from './helpers'
-import 'bootstrap/dist/css/bootstrap.css'
-import EmailList from './lists/components/EmailList'
-import PhoneList from './lists/components/PhoneList'
 
-/**
- * General Application Design
- *
- * 4 Components:
- * - PlayerList (representing the PlayerList Excel file)
- * - Calendar (representing the Calendar Excel file)
- * - MatchList (representing the Spielliste Excel file)
- * - PaymentList (representing the Zahlliste Excel file)
- *
- * PlayerList
- * - Shows the relevant information from the file
- * - Shows calculated information from combination with Calendar
- * - Points out potential problems
- * - Allows access to player information
- *
- * Calendar
- * - Shows the relevant information from the file
- * - Shows calculated information from combination with PlayerList
- * - Points out potential problems
- * - Allows access to match information
- *
- * MatchList
- * - Shows the calculated match lists
- * - Points out problems
- *
- * PaymentList
- * - Shows the calculated payment lists
- * - Points out problems
- *
- */
+/** Import sub-modules */
+import startPage from './startPage'
+import playerList from './playerList'       // Everything that has to do with players
+import matchList from './matchList'         // Everything that has to do with matches
+import layout from './layout'
 
-const FILTER_OFF = 'Alle'
+/** Import CSS Styles */
+import 'bootstrap/dist/css/bootstrap.css'
 
 /** Main application */
 class Main extends React.Component {
-  /**
-   * Constructor
-   * Defines the state and binds all 'this' in all functions to the object.
-   */
-  constructor () {
-    super()
-
-    // Bind 'this' to functions
-    this.handlePlayerList = this.handlePlayerList.bind(this)
-    this.handleCalendar = this.handleCalendar.bind(this)
-    this.generatePlayerList = this.generatePlayerList.bind(this)
-    this.generateCalendar = this.generateCalendar.bind(this)
-    this.filterMatches = this.filterMatches.bind(this)
-    this.filterPlayers = this.filterPlayers.bind(this)
-    this.generateSchedule = this.generateSchedule.bind(this)
-    this.generatePayTable = this.generatePayTable.bind(this)
-    this.generatePhoneList = this.generatePhoneList.bind(this)
-  }
-
-  calculateMatchFilters () {
-    const dates = {}
-    const places = []
-    const categories = []
-    this.state.match.matches.forEach((item) => {
-      const dateString = date2s(item.Datum)
-      if (!!item.Datum & !dates.hasOwnProperty(dateString)) {
-        dates[dateString] = item.Datum
-      }
-      if (!!item.Ort & !places.includes(item.Ort)) {
-        places.push(item.Ort)
-      }
-      if (!!item.Konkurrenz & !categories.includes(item.Konkurrenz)) {
-        categories.push(item.Konkurrenz)
-      }
-    })
-    const match = { ...this.state.match, dates, places, categories }
-    this.setState({ match })
-  }
-
-  calculatePlayerFilters () {
-    const categories = []
-    this.state.player.players.forEach((item) => {
-      if (!!item.Konkurrenz & !categories.includes(item.Konkurrenz)) {
-        categories.push(item.Ort)
-      }
-    })
-    const player = { ...this.state.player, categories }
-    this.setState({ player })
-  }
-
-  handlePlayerList (event) {
-    const file = this.playerList.files[0]
-    Excel.readWorkbook(file, this.generatePlayerList)
-  }
-
-  handleCalendar (event) {
-    const file = this.calendar.files[0]
-    Excel.readWorkbook(file, this.generateCalendar)
-  }
-
-  generatePlayerList (worksheet) {
-    console.log('About to read the player list.')
-    const worksheets = { ...this.state.worksheets }
-    worksheets['PlayerList'] = worksheet
-    this.setState({ worksheets })
-
-    if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
-      throw Error('Wrong file structure.')
-    }
-    const players = worksheet.slice(4, worksheet.length).map((playerData) => new Player.Player(playerData))
-    const player = { ...this.state.player }
-    player.players = players
-    this.setState({ player })
-    this.calculatePayDate()
-    this.filterPlayers()
-    console.log('State after generating player list:', this.state)
-  }
-
-  generateCalendar (worksheet) {
-    console.log('About to read the calendar.')
-    const worksheets = { ...this.state.worksheets }
-    worksheets['Calendar'] = worksheet
-    this.setState({ worksheets })
-
-    if (worksheet[2].length < 8 | worksheet[2].length > 9) {
-      throw Error('Wrong file structure.')
-    }
-    const matches = worksheet.slice(2, worksheet.length).map((matchData) => new Match.MatchClass(matchData))
-    const match = { ...this.state.match }
-    match.matches = matches
-    this.setState({ match })
-    this.calculateMatchFilters()
-    this.calculatePayDate()
-    this.filterMatches()
-    console.log('State after generating calendar:', this.state)
-  }
-
-  filterMatches () {
-    const filters = {
-      date: this.matchDate.value !== FILTER_OFF ? this.matchDate.value : null,
-      place: this.matchPlace.value !== FILTER_OFF ? this.matchPlace.value : null,
-      category: this.matchCategory.value !== FILTER_OFF ? this.matchCategory.value : null
-    }
-    console.log('New filter settings:', filters)
-
-    const match = { ...this.state.match }
-    match.filtered = match.matches.filter((match) => {
-      const matchDate = new Date(match.Datum)
-      matchDate.setHours(0, 0, 0, 0)
-      const filtDate = new Date(filters.date)
-      filtDate.setHours(0, 0, 0, 0)
-      return (!filters.date | matchDate.getTime() === filtDate.getTime()) &
-      (!filters.place | filters.place === match.Ort) &
-      (!filters.category | filters.category === match.Konkurrenz)
-    })
-    this.setState({ match })
-
-    const player = { ...this.state.player, filters }
-    player.filtered = player.players
-    this.setState({ player })
-  }
-
-  filterPlayers () {
-    const filters = {
-      junior: this.playerJunior.checked,
-      paid: this.playerPaid.checked,
-      category: this.playerCategory.value !== FILTER_OFF ? this.playerCategory.value : null
-    }
-    console.log('New filter settings:', filters)
-
-    const player = { ...this.state.player, filters }
-    player.filtered = player.players
-    this.setState({ player })
-  }
-
-  generateSchedule (event) {
-    event.preventDefault()
-
-    const matchlist = new Excel.Workbook()
-    matchlist.SheetNames = []
-    matchlist.Sheets = {}
-
-    const worksheets = {}
-
-    let placeArray = this.state.match.places
-    if (placeArray.length > 1) {
-      placeArray = placeArray.concat([FILTER_OFF])
-    }
-    const date = Object.keys(this.state.match.dates).find((key) =>
-      String(this.state.match.dates[key]) === this.matchDate.value
-    )
-
-    placeArray.forEach(place => {
-      let header = [
-        ['Stadtzürcher Tennismeisterschaft'],
-        [`Spielplan für den ${date} (${place})`],
-        [],
-        ['Ort', 'Zeit', 'Kategorie', 'Spieler 1', '', 'Spieler 2', '', '1. Satz', '2. Satz', '3. Satz', 'WO Grund']
-      ]
-      let matchListPerPlace = this.state.match.filtered.filter((match) => (match.Ort === place | place === FILTER_OFF)).map((match) =>
-        [match.Ort, time2s(match.Datum), match.Konkurrenz, match.Spieler1, match.Spieler1Klassierung, match.Spieler2, match.Spieler2Klassierung]
-      )
-      console.log('Generated match list per place:', matchListPerPlace)
-      worksheets[place] = Excel.SheetFromArray(header.concat(matchListPerPlace))
-      matchlist.SheetNames.push(place)
-      matchlist.Sheets[place] = worksheets[place]
-    })
-
-    Excel.saveAs(matchlist, 'Spielliste.xlsx')
-  }
-
-  generatePhoneList (event) {
-    event.preventDefault()
-
-    const phoneMail = new Excel.Workbook()
-    phoneMail.SheetNames = []
-    phoneMail.Sheets = {}
-
-    const dataList = [
-      ['Vorname', 'Nachname', 'Anrede', 'Geschlecht', 'Handy', 'E-Mail']
-    ]
-    const phonePot = []
-
-    const players = this.state.player.filtered
-    players.forEach(player => {
-      if (!player.phone.match(/^FEHLER/) && !phonePot.includes(player.phone)) {
-        phonePot.push(player.phone)
-        dataList.push([
-          player.Vorname,
-          player.Name,
-          2,
-          player.geschlecht === 'w' ? 2 : 1,
-          player.phone
-        ])
-      }
-    })
-    phoneMail.Sheets['Sheet1'] = Excel.SheetFromArray(dataList)
-    phoneMail.SheetNames.push('Sheet1')
-    Excel.saveAs(phoneMail, 'Telefon.xlsx')
-  }
-
-  calculatePayDate () {
-    if ((this.state.player.players.length === 0) | (this.state.match.matches.length === 0)) {
-      return
-    }
-
-    this.state.match.matches.forEach((match) => {
-      [match.Spieler1, match.Spieler2].forEach((matchPlayer) => {
-        if (matchPlayer) {
-          let foundPlayer = this.state.player.players.find((player) =>
-            (player.name === matchPlayer) & (player.Konkurrenz === match.Konkurrenz)
-          )
-          if (!foundPlayer) {
-            console.log('Debug payerlist:', foundPlayer, match)
-            throw Error('Match player not found in player list. This is an error!')
-          }
-          if (!foundPlayer.BezahltAm) {
-            foundPlayer.BezahltAm = match.Datum
-          }
-        }
-      })
-    })
-  }
-
-  generatePayTable (event) {
-    event.preventDefault()
-
-    const paylist = new Excel.Workbook()
-    paylist.SheetNames = []
-    paylist.Sheets = {}
-
-    const worksheets = {}
-
-    let placeArray = this.state.match.places
-    /* if (placeArray.length > 1) {
-      placeArray = placeArray.concat([FILTER_OFF])
-    } */
-    const date = Object.keys(this.state.match.dates).find((key) =>
-      String(this.state.match.dates[key]) === this.matchDate.value
-    )
-
-    placeArray.forEach((place) => {
-      let header = [
-        ['Stadtzürcher Tennismeisterschaft'],
-        [`Nenngelder für ${date}`],
-        [],
-        [`${place}`, null, '50.- oder 30.- (Junioren Jg. 1999 oder jünger)'],
-        [],
-        ['bereits bez.', 'Kat.', 'Zeit', 'Name', 'Betrag bez.', 'Quittung abgeben']
-      ]
-
-      // Per place
-      let payListPerPlace = []
-      this.state.match.filtered.forEach((match) => {
-        [match.Spieler1, match.Spieler2].forEach((matchPlayer) => {
-          if (!!matchPlayer & (match.Ort === place | FILTER_OFF === place)) {
-            const player = this.state.player.players.find((player) =>
-              (player.Konkurrenz === match.Konkurrenz) & (player.name === matchPlayer)
-            )
-            let paid = null
-            if (player.BezahltAm < this.matchDate.value) {
-              paid = date2s(player.BezahltAm)
-            }
-            if (player.Bezahlt) {
-              paid = 'OK'
-            }
-            let price
-            if (player.isDoubles) {
-              price = (player.isJunior ? 15 : 25) + (player.isJuniorDP ? 15 : 25)
-            } else {
-              price = player.isJunior ? 30 : 50
-            }
-            payListPerPlace.push([ paid, match.Konkurrenz, time2s(match.Datum), `${matchPlayer} (${price}.-)` ])
-          }
-        })
-      })
-
-      let footer = [
-        [],
-        ['Datum'],
-        ['Turnierleiter', null, null, 'Kassierer'],
-        ['Betrag von Spielern erhalten', null, null, 'Betrag von Turnierleiter erhalten']
-      ]
-      console.log(`Generated pay list per place ${place}:`, payListPerPlace)
-      worksheets[place] = Excel.SheetFromArray(header.concat(payListPerPlace, footer))
-      paylist.SheetNames.push(place)
-      paylist.Sheets[place] = worksheets[place]
-    })
-
-    let payListPerPlace = []
-    this.state.match.filtered.forEach((match) => {
-      [match.Spieler1, match.Spieler2].forEach((matchPlayer) => {
-        if (matchPlayer) {
-          const player = this.state.player.players.find((player) =>
-            (player.Konkurrenz === match.Konkurrenz) & (player.name === matchPlayer)
-          )
-          let price
-          if (player.isDoubles) {
-            price = (player.isJunior ? 15 : 25) + (player.isJuniorDP ? 15 : 25)
-          } else {
-            price = player.isJunior ? 30 : 50
-          }
-          payListPerPlace.push([ match.Ort, match.Konkurrenz, `${matchPlayer} (${price}.-)` ])
-        }
-      })
-    })
-    console.log(`Generated pay list for "Alle":`, payListPerPlace)
-    worksheets[FILTER_OFF] = Excel.SheetFromArray(payListPerPlace)
-    paylist.SheetNames.push(FILTER_OFF)
-    paylist.Sheets[FILTER_OFF] = worksheets[FILTER_OFF]
-
-    Excel.saveAs(paylist, 'Zahlliste.xlsx')
-  }
-
   render () {
-    const Pli = PlayerList.components.PlayerList
-    const Mli = MatchList.components.MatchList
-    const places = this.props.matchList.places || []
-    const dates = this.props.matchList.dates || {}
-    const matchCategories = this.props.matchList.categories || []
-    const playerCategories = this.props.playerList.categories || []
+    const AppLayout = layout.components.AppLayout
 
     return (
       <div className='container'>
-        <form>
-          <label htmlFor='PlayerList'>Swisstennis PlayerList.xls</label>
-          <input type='file' id='PlayerList' ref={(input) => { this.playerList = input }} accept='.xls' placeholder='PlayerList File' onChange={this.handlePlayerList} />
-          <label htmlFor='Calendar'>Swisstennis Calendar.xls</label>
-          <input type='file' id='Calendar' ref={(input) => { this.calendar = input }} accept='.xls' placeholder='Calendar File' onChange={this.handleCalendar} />
-          <label htmlFor='Date'>Datum</label>
-          <select id='Date' ref={(input) => { this.matchDate = input }} onChange={this.filterMatches}>
-            <option>{FILTER_OFF}</option>
-            {Object.keys(dates).map((key) => (
-              <option key={key} value={dates[key]}>{key}</option>
-            ))}
-          </select>
-          <label htmlFor='Place'>Ort</label>
-          <select id='Place' ref={(input) => { this.matchPlace = input }} onChange={this.filterMatches}>
-            <option>{FILTER_OFF}</option>
-            {places.map((place, key) => (
-              <option key={key}>{place}</option>
-            ))}
-          </select>
-          <label htmlFor='MatchCategory'>Match Konkurrenz</label>
-          <select id='MatchCategory' ref={(input) => { this.matchCategory = input }} onChange={this.filterMatches}>
-            <option>{FILTER_OFF}</option>
-            {matchCategories.map((category, key) => (
-              <option key={key}>{category}</option>
-            ))}
-          </select>
-          <label htmlFor='Junior'>Junior</label>
-          <input id='Junior' ref={(input) => { this.playerJunior = input }} type='checkbox' onChange={this.filterPlayers} />
-          <label htmlFor='Paid'>Bezahlt</label>
-          <input id='Paid' ref={(input) => { this.playerPaid = input }} type='checkbox' onChange={this.filterPlayers} />
-          <label htmlFor='PlayerCategory'>Spieler Konkurrenz</label>
-          <select id='PlayerCategory' ref={(input) => { this.playerCategory = input }} onChange={this.filterPlayers}>
-            <option>{FILTER_OFF}</option>
-            {playerCategories.map((category, key) => (
-              <option key={key}>{category}</option>
-            ))}
-          </select>
-          <button onClick={this.generateSchedule} disabled={!this.props.matchList.filteredMatches.length}>Spielliste generieren</button>
-          <button onClick={this.generatePayTable} disabled={(!this.props.matchList.filteredMatches.length | !this.props.playerList.filteredPlayers.length)}>Zahlliste generieren</button>
-          <button onClick={this.generatePhoneList} disabled={!this.props.playerList.allPlayers.length}>Telefonliste generieren</button>
-        </form>
-        <Mli match={this.props.matchList} />
-        <PhoneList filtered={this.props.matchList.filteredMatches} players={this.props.playerList.allPlayers} />
-        <EmailList filtered={this.props.matchList.filteredMatches} players={this.props.playerList.allPlayers} />
-        <Pli player={this.props.playerList} />
+        <AppLayout
+          layout={this.props.layout}
+          layoutActions={this.props.layoutActions}
+          state={this.props}
+          components={{
+            PlayerList: playerList.components.PlayerList,
+            MatchTable: matchList.components.MatchTable,
+            StartPage: startPage.components.StartPage
+          }}
+        />
       </div>
     )
   }

+ 2 - 2
src/calendar/index.js

@@ -1,4 +1,4 @@
-import { actions, reducer, state } from './state'
+import { actions, reducer, state, saga } from './state'
 import components from './components'
 import { normalize } from '../helpers.js'
 
@@ -28,4 +28,4 @@ class MatchClass {
   }
 }
 
-export default { MatchClass, actions, components, filters, selectors, reducer, state }
+export default { MatchClass, actions, components, filters, selectors, reducer, state, saga }

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

@@ -0,0 +1,16 @@
+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)
+})

+ 1 - 0
src/constants.js

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

+ 0 - 109
src/excel/SZTM_Zahlliste.bas

@@ -1,109 +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 = "F\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
-                
-            Range("A3:F3").Select
-            With Selection.Interior
-                .Pattern = xlSolid
-                .PatternColorIndex = xlAutomatic
-                .ThemeColor = xlThemeColorDark1
-                .TintAndShade = -0.149998474074526
-                .PatternTintAndShade = 0
-            End With
-            Selection.Font.Bold = True
-            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
-            ActiveWindow.SmallScroll Down:=-3
-            Range("A3").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
-            Columns("A:A").EntireColumn.AutoFit
-            Columns("B:B").EntireColumn.AutoFit
-            Columns("C:C").EntireColumn.AutoFit
-            Columns("D:D").EntireColumn.AutoFit
-            Columns("E:E").EntireColumn.AutoFit
-            Columns("F:F").EntireColumn.AutoFit
-            Range("A4").Select
-            Range(Selection, ActiveCell.SpecialCells(xlLastCell)).Select
-            Range("A4").Select
-            Range(Selection, ActiveCell.SpecialCells(xlLastCell)).Select
-            Selection.RowHeight = 33.75
-            
-        End With
-    Next
-
-End Sub

+ 15 - 9
src/helpers.js

@@ -12,18 +12,24 @@ function datetime2s (date) {
   return moment(date).format('DD.MM. HH:mm')
 }
 
-function sortTable (array, columns) {
-  function compare (item1, item2) {
-    if (item1 instanceof Date) {
-
-    }
-  }
-}
-
 function normalize (item, type) {
   return item ? String(item).replace(/\s+/g, ' ').trim() : null
 }
 
+function fileSize (int) {
+  let value
+  if (int > Math.pow(2, 10)) {
+    value = `${int / Math.pow(2, 10)}kB`
+  }
+  if (int > Math.pow(2, 20)) {
+    value = `${int / Math.pow(2, 20)}MB`
+  }
+  if (int > Math.pow(2, 30)) {
+    value = `${int / Math.pow(2, 30)}GB`
+  }
+  return value
+}
+
 function normalizePhone (item) {
   let phone = String(item).replace(/\s|\+|\/|,|-|'/g, '').replace(/\(0\)/, '').replace(/^0+/, '')
   if (phone.match(/[^\d*]/)) {
@@ -43,4 +49,4 @@ function normalizePhone (item) {
   return phone
 }
 
-export { date2s, time2s, datetime2s, sortTable, normalize, normalizePhone }
+export { date2s, time2s, datetime2s, normalize, normalizePhone, fileSize }

+ 26 - 0
src/layout/components/AppLayout.js

@@ -0,0 +1,26 @@
+import React from 'react'
+import { Tabs, Tab } from 'react-bootstrap'
+
+class AppLayout extends React.Component {
+  render () {
+    console.log('AppLayout state', this.props.state)
+    const { activeTab } = this.props.layout
+    const { changeTab } = this.props.layoutActions
+    const { state } = this.props
+    const { PlayerList, MatchTable, StartPage } = this.props.components
+
+    return (
+      <div>
+        <Tabs activeKey={activeTab} onSelect={changeTab} id='layout-tabs'>
+          <Tab eventKey={0} title='Setup'><StartPage state={state.setupPage} actions={state.setupPageActions} /></Tab>
+          <Tab eventKey={1} title='PlayerList' disabled><PlayerList state={state.playerList} actions={state.playerListActions} /></Tab>
+          <Tab eventKey={2} title='Calendar' disabled><MatchTable state={state.matchList} actions={state.matchListActions} /></Tab>
+          <Tab eventKey={3} title='Spielplan' disabled />
+          <Tab eventKey={4} title='Zahlliste' disabled />
+        </Tabs>
+      </div>
+    )
+  }
+}
+
+export default AppLayout

+ 3 - 0
src/layout/components/index.js

@@ -0,0 +1,3 @@
+import AppLayout from './AppLayout'
+
+export default { AppLayout }

+ 10 - 0
src/layout/index.js

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

+ 38 - 0
src/layout/state.js

@@ -0,0 +1,38 @@
+/** @module player/state */
+// import { call, put, takeEvery, takeLatest } 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 = {
+  changeTab: activeTab => {
+    return {
+      type: 'CHANGE_TAB',
+      activeTab
+    }
+  }
+}
+console.log('State actions', actions)
+
+/** state definition */
+export const state = {
+  activeTab: 0
+}
+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 'CHANGE_TAB':
+      return { ...state, activeTab: action.activeTab }
+    default:
+      return state
+  }
+}
+
+/** sagas are asynchronous workers (JS generators) to handle the state. */
+export function * saga () {}

+ 13 - 8
src/macros/SZTM_Spielliste.bas

@@ -1,7 +1,7 @@
 Attribute VB_Name = "Module2"
 Sub SZTM_Spielliste()
 Attribute SZTM_Spielliste.VB_Description = "Formatiert die Spielliste"
-Attribute SZTM_Spielliste.VB_ProcData.VB_Invoke_Func = "X\n14"
+Attribute SZTM_Spielliste.VB_ProcData.VB_Invoke_Func = "S\n14"
 '
 ' SZTM_Spielliste Macro
 ' Formatiert die Spielliste
@@ -15,6 +15,7 @@ Attribute SZTM_Spielliste.VB_ProcData.VB_Invoke_Func = "X\n14"
         With ws
         
             ws.Activate
+            ws.PageSetup.Orientation = xlLandscape
             
             'Select first row, change font size, make bold and merge
             Range("A1:K1").Select
@@ -112,13 +113,17 @@ Attribute SZTM_Spielliste.VB_ProcData.VB_Invoke_Func = "X\n14"
                 .TintAndShade = 0
                 .Weight = xlThin
             End With
-            Selection.RowHeight = 33.75
-            Selection.EntireColumn.AutoFit
-            'Columns("A:A").EntireColumn.AutoFit
-            'Columns("B:B").EntireColumn.AutoFit
-            'Columns("C:C").EntireColumn.AutoFit
-            'Columns("D:D").EntireColumn.AutoFit
-            'Columns("E:E").EntireColumn.AutoFit
+            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

+ 11 - 9
src/macros/SZTM_Zahlliste.bas

@@ -13,7 +13,6 @@ Attribute SZTM_Zahlliste.VB_ProcData.VB_Invoke_Func = "Z\n14"
         With ws
         
             ws.Activate
-            ws.PageSetup.Orientation = xlLandscape
             
             'Select first row, change font size, make bold and merge
             Range("A1:F1").Select
@@ -66,6 +65,7 @@ Attribute SZTM_Zahlliste.VB_ProcData.VB_Invoke_Func = "Z\n14"
             
             '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
@@ -88,7 +88,7 @@ Attribute SZTM_Zahlliste.VB_ProcData.VB_Invoke_Func = "Z\n14"
             Selection.Offset(-1, -4).Select
             Selection.Font.Bold = True
             Selection.Offset(0, 1).Select
-            Selection.Resize(Selection.Rows.Count, Selection.Columns.Count + 1).Select
+            Selection.Resize(Selection.Rows.Count, Selection.Columns.Count + 4).Select
             Selection.Merge
             
             Selection.Offset(0, -1).Select
@@ -186,13 +186,15 @@ Attribute SZTM_Zahlliste.VB_ProcData.VB_Invoke_Func = "Z\n14"
                 .TintAndShade = 0
                 .Weight = xlThin
             End With
-            Selection.RowHeight = 33.75
-            Selection.EntireColumn.AutoFit
-            'Columns("A:A").EntireColumn.AutoFit
-            'Columns("B:B").EntireColumn.AutoFit
-            'Columns("C:C").EntireColumn.AutoFit
-            'Columns("D:D").EntireColumn.AutoFit
-            'Columns("E:E").EntireColumn.AutoFit
+            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

+ 0 - 20
src/matchList/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 - 35
src/matchList/components/MatchList.js

@@ -1,35 +0,0 @@
-import React from 'react'
-import MatchDisp from './MatchDisp'
-
-class MatchList extends React.Component {
-  render () {
-    console.log(this.props)
-    const matches = this.props.match.allMatches
-    const filtered = this.props.match.filteredMatches
-
-    return (
-      <div>
-        <h2>Matches ({filtered.length}/{matches.length})</h2>
-        <table className='table table-bordered table-striped'>
-          <thead>
-            <tr>
-              <th>Ort</th>
-              <th>Datum</th>
-              <th>Zeit</th>
-              <th>Konkurrenz</th>
-              <th>Spieler 1</th>
-              <th>Spieler 2</th>
-            </tr>
-          </thead>
-          <tbody>
-            {filtered.map((match, key) =>
-              <MatchDisp key={key} match={match} />
-            )}
-          </tbody>
-        </table>
-      </div>
-    )
-  }
-}
-
-export default MatchList

+ 50 - 0
src/matchList/components/MatchTable.js

@@ -0,0 +1,50 @@
+import React from 'react'
+import { date2s, time2s } from '../../helpers.js'
+
+class MatchRow 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>
+    )
+  }
+}
+
+class MatchTable extends React.Component {
+  render () {
+    console.log('Match props', this.props.state)
+    const { allMatches, filteredMatches } = this.props.state || { allMatches: [], filteredMatches: [] }
+
+    return (
+      <div>
+        <h2>Matches ({filteredMatches.length}/{allMatches.length})</h2>
+        <table className='table table-bordered table-striped'>
+          <thead>
+            <tr>
+              <th>Ort</th>
+              <th>Datum</th>
+              <th>Zeit</th>
+              <th>Konkurrenz</th>
+              <th>Spieler 1</th>
+              <th>Spieler 2</th>
+            </tr>
+          </thead>
+          <tbody>
+            {filteredMatches.map((match, key) =>
+              <MatchRow key={key} match={match} />
+            )}
+          </tbody>
+        </table>
+      </div>
+    )
+  }
+}
+
+export default MatchTable

+ 2 - 3
src/matchList/components/index.js

@@ -1,4 +1,3 @@
-import MatchList from './MatchList'
-import MatchDisp from './MatchDisp'
+import MatchTable from './MatchTable'
 
-export default { MatchList, MatchDisp }
+export default { MatchTable }

+ 239 - 2
src/matchList/index.js

@@ -1,5 +1,8 @@
-import { actions, reducer, state } from './state'
+import { actions, reducer, state, saga } from './state'
 import components from './components'
+import { date2s, time2s } from '../helpers'
+import Match from '../classes/match'
+import Excel from '../excel'
 
 const filters = {
   all: players => players
@@ -7,4 +10,238 @@ const filters = {
 
 const selectors = {}
 
-export default { actions, components, filters, selectors, reducer, state }
+export default { actions, components, filters, selectors, reducer, state, saga }
+
+const PLACES = {
+  'LE': 'TC Lerchenberg',
+  'WA': 'TC Waidberg',
+  'VA': 'TC Valsana',
+  'SE': 'TC Seebach',
+  'BU': 'TC Bührle',
+  'HO': 'TC Höngg',
+  'TS': 'Tennis-Sport Club',
+  'HA': 'Städtische Plätze Hardhof',
+  'AU': 'Auswärtig'
+}
+
+const FILTER_OFF = 'Alle'
+
+function calculateMatchFilters () {
+  const dates = {}
+  const places = []
+  const categories = []
+  this.state.match.matches.forEach((item) => {
+    const dateString = date2s(item.Datum)
+    if (!!item.Datum & !dates.hasOwnProperty(dateString)) {
+      dates[dateString] = item.Datum
+    }
+    if (!!item.Ort & !places.includes(item.Ort)) {
+      places.push(item.Ort)
+    }
+    if (!!item.Konkurrenz & !categories.includes(item.Konkurrenz)) {
+      categories.push(item.Konkurrenz)
+    }
+  })
+  const match = { ...this.state.match, dates, places, categories }
+  this.setState({ match })
+}
+
+function generateCalendar (worksheet) {
+  console.log('About to read the calendar.')
+  const worksheets = { ...this.state.worksheets }
+  worksheets['Calendar'] = worksheet
+  this.setState({ worksheets })
+
+  if (worksheet[2].length < 8 | worksheet[2].length > 9) {
+    throw Error('Wrong file structure.')
+  }
+  const matches = worksheet.slice(2, worksheet.length).map((matchData) => new Match.Match(matchData))
+  const match = { ...this.state.match }
+  match.matches = matches
+  this.setState({ match })
+  this.calculateMatchFilters()
+  this.calculatePayDate()
+  this.filterMatches()
+  console.log('State after generating calendar:', this.state)
+}
+
+function filterMatches () {
+  const filters = {
+    date: this.matchDate.value !== FILTER_OFF ? this.matchDate.value : null,
+    place: this.matchPlace.value !== FILTER_OFF ? this.matchPlace.value : null,
+    category: this.matchCategory.value !== FILTER_OFF ? this.matchCategory.value : null
+  }
+  console.log('New filter settings:', filters)
+
+  const match = { ...this.state.match }
+  match.filtered = match.matches.filter((match) => {
+    const matchDate = new Date(match.Datum)
+    matchDate.setHours(0, 0, 0, 0)
+    const filtDate = new Date(filters.date)
+    filtDate.setHours(0, 0, 0, 0)
+    return (!filters.date | matchDate.getTime() === filtDate.getTime()) &
+      (!filters.place | filters.place === match.Ort) &
+      (!filters.category | filters.category === match.Konkurrenz)
+  })
+  this.setState({ match })
+
+  const player = { ...this.state.player, filters }
+  player.filtered = player.players
+  this.setState({ player })
+}
+
+function generatePhoneList (event) {
+  event.preventDefault()
+
+  const phoneMail = new Excel.Workbook()
+  phoneMail.SheetNames = []
+  phoneMail.Sheets = {}
+
+  const dataList = [
+      ['Vorname', 'Nachname', 'Anrede', 'Geschlecht', 'Handy', 'E-Mail']
+  ]
+  const phonePot = []
+
+  const players = this.state.player.filtered
+  players.forEach(player => {
+    if (!player.phone.match(/^FEHLER/) && !phonePot.includes(player.phone)) {
+      phonePot.push(player.phone)
+      dataList.push([
+        player.Vorname,
+        player.Name,
+        2,
+        player.geschlecht === 'w' ? 2 : 1,
+        player.phone
+      ])
+    }
+  })
+  phoneMail.Sheets['Sheet1'] = Excel.SheetFromArray(dataList)
+  phoneMail.SheetNames.push('Sheet1')
+  Excel.saveAs(phoneMail, 'Telefon.xlsx')
+}
+
+function calculatePayDate () {
+  if ((this.state.player.players.length === 0) | (this.state.match.matches.length === 0)) {
+    return
+  }
+
+  this.state.match.matches.forEach((match) => {
+    [match.Spieler1, match.Spieler2].forEach((matchPlayer) => {
+      if (matchPlayer) {
+        let foundPlayer = this.state.player.players.find((player) =>
+            (player.name === matchPlayer) & (player.Konkurrenz === match.Konkurrenz)
+          )
+        if (!foundPlayer) {
+          console.log('Debug payerlist:', foundPlayer, match)
+          throw Error('Match player not found in player list. This is an error!')
+        }
+        if (!foundPlayer.BezahltAm) {
+          foundPlayer.BezahltAm = match.Datum
+        }
+      }
+    })
+  })
+}
+
+function generatePayTable (event) {
+  event.preventDefault()
+
+  const paylist = new Excel.Workbook()
+  paylist.SheetNames = []
+  paylist.Sheets = {}
+
+  const worksheets = {}
+
+  let placeArray = this.state.match.places
+    /* if (placeArray.length > 1) {
+      placeArray = placeArray.concat([FILTER_OFF])
+    } */
+  const date = Object.keys(this.state.match.dates).find((key) =>
+      String(this.state.match.dates[key]) === this.matchDate.value
+    )
+
+  placeArray.forEach((place) => {
+    let header = [
+        ['Stadtzürcher Tennismeisterschaft'],
+        [`Nenngelder für ${date}`],
+        [],
+        [`${PLACES[place] || place}`, null, '50.- oder 30.- (Junioren Jg. 1999 oder jünger)'],
+        [],
+        ['bezahlt', 'Kat.', 'Zeit', 'Name', 'Betrag bez.', 'Quittung']
+    ]
+
+      // Per place
+    let payListPerPlace = []
+    this.state.match.filtered.forEach((match) => {
+      [match.Spieler1, match.Spieler2].forEach((matchPlayer) => {
+        if (!!matchPlayer & (match.Ort === place | FILTER_OFF === place)) {
+          const player = this.state.player.players.find((player) =>
+              (player.Konkurrenz === match.Konkurrenz) & (player.name === matchPlayer)
+            )
+          let paid = null
+          if (player.BezahltAm < this.matchDate.value) {
+            paid = date2s(player.BezahltAm)
+          }
+          if (player.Bezahlt) {
+            paid = 'OK'
+          }
+          let price
+          if (player.isDoubles) {
+            price = (player.isJunior ? 15 : 25) + (player.isJuniorDP ? 15 : 25)
+          } else {
+            price = player.isJunior ? 30 : 50
+          }
+          payListPerPlace.push([ paid, match.Konkurrenz, time2s(match.Datum), `(${price}.-) ${matchPlayer}` ])
+        }
+      })
+    })
+
+    let footer = [
+        [],
+        ['Datum'],
+        ['Turnierleiter', null, null, 'Kassierer'],
+        ['Betrag von Spielern erhalten', null, null, 'Betrag von Turnierleiter erhalten']
+    ]
+    console.log(`Generated pay list per place ${place}:`, payListPerPlace)
+    worksheets[place] = Excel.SheetFromArray(header.concat(payListPerPlace, footer))
+    paylist.SheetNames.push(place)
+    paylist.Sheets[place] = worksheets[place]
+  })
+  Excel.saveAs(paylist, 'Zahlliste.xlsx')
+}
+
+function generateSchedule (event) {
+  event.preventDefault()
+
+  const matchlist = new Excel.Workbook()
+  matchlist.SheetNames = []
+  matchlist.Sheets = {}
+
+  const worksheets = {}
+
+  let placeArray = this.state.match.places
+  if (placeArray.length > 1) {
+      // placeArray = placeArray.concat([FILTER_OFF])
+  }
+  const date = Object.keys(this.state.match.dates).find((key) =>
+      String(this.state.match.dates[key]) === this.matchDate.value
+    )
+
+  placeArray.forEach(place => {
+    let header = [
+        ['Stadtzürcher Tennismeisterschaft'],
+        [`Spielplan für den ${date} (${PLACES[place] || place})`],
+        [],
+        ['Platz', 'Zeit', 'Kategorie', 'Spieler 1', '', 'Spieler 2', '', '1. Satz', '2. Satz', '3. Satz', 'WO Grund']
+    ]
+    let matchListPerPlace = this.state.match.filtered.filter((match) => (match.Ort === place | place === FILTER_OFF)).map((match) =>
+        [null, time2s(match.Datum), match.Konkurrenz, match.Spieler1, match.Spieler1Klassierung, match.Spieler2, match.Spieler2Klassierung]
+      )
+    console.log('Generated match list per place:', matchListPerPlace)
+    worksheets[place] = Excel.SheetFromArray(header.concat(matchListPerPlace))
+    matchlist.SheetNames.push(place)
+    matchlist.Sheets[place] = worksheets[place]
+  })
+
+  Excel.saveAs(matchlist, 'Spielplan.xlsx')
+}

+ 0 - 23
src/playerList/components/PlayerDisp.js

@@ -1,23 +0,0 @@
-import React from 'react'
-import { date2s, time2s } from '../../helpers'
-
-class PlayerDisp extends React.Component {
-  render () {
-    const player = this.props.player
-    return (
-      <tr>
-        <td><i>{player.Konkurrenz}</i></td>
-        <td><b>{player.Name}</b></td>
-        <td>{player.Vorname}</td>
-        <td><b>{player.NameDP}</b></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>
-    )
-  }
-}
-
-export default PlayerDisp

+ 29 - 0
src/playerList/components/PlayerFilter.js

@@ -0,0 +1,29 @@
+import React from 'react'
+import { FormGroup, ControlLabel, FormControl, HelpBlock } 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='playerListFile'
+          type='file'
+          label='playerList.xls File'
+          help='Die Datei wird von der Swisstennis Turniersoftware generiert.'
+        />
+      </form>
+    )
+  }
+}
+
+export default PlayerFilter

+ 49 - 0
src/playerList/components/PlayerForm.js

@@ -0,0 +1,49 @@
+import React from 'react'
+import { Panel, FormGroup, ControlLabel, FormControl, HelpBlock } from 'react-bootstrap'
+
+function FieldGroup ({ id, label, help, ...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 () {
+    const file = this.playerListFile
+    const { fileUploadStart } = this.props.actions
+    fileUploadStart(file)
+  }
+
+  render () {
+    const { fileUpload } = this.props.state
+
+    return (
+      <div>
+        {fileUpload === 'failure'
+        ? (<Panel header='Fehler'>Fehler beim laden.</Panel>)
+        : ''}
+        <form>
+          <FieldGroup
+            id='playerListFile'
+            type='file'
+            label='PlayerList.xls File'
+            help='Die Datei wird von der Swisstennis Turniersoftware generiert.'
+            ref={input => { this.playerListFile = input }}
+            onChange={this.handleFileUpload}
+          />
+        </form>
+      </div>
+    )
+  }
+}
+
+export default PlayerForm

+ 20 - 37
src/playerList/components/PlayerList.js

@@ -1,37 +1,20 @@
-import React from 'react'
-import PlayerDisp from './PlayerDisp'
-
-class PlayerList extends React.Component {
-  render () {
-    console.log(this.props)
-    const filtered = this.props.player.filteredPlayers
-
-    return (
-      <div>
-        <h2>Spielerliste ({filtered.length})</h2>
-        <table className='table table-bordered table-striped'>
-          <thead>
-            <tr>
-              <th><i>Konkurrenz</i></th>
-              <th><b>Name</b></th>
-              <th>Vorname</th>
-              <th><b>Name DP</b></th>
-              <th>Vorname DP</th>
-              <th>Bezahlt</th>
-              <th>Bezahlt Am</th>
-              <th>Junior</th>
-              <th>DP Junior</th>
-            </tr>
-          </thead>
-          <tbody>
-            {filtered.map((player, key) =>
-              <PlayerDisp key={key} player={player} />
-            )}
-          </tbody>
-        </table>
-      </div>
-    )
-  }
-}
-
-export default PlayerList
+import React from 'react'
+import PlayerForm from './PlayerForm'
+import PlayerTable from './PlayerTable'
+import PlayerFilter from './PlayerFilter'
+
+class PlayerList extends React.Component {
+  render () {
+    const { state, actions } = this.props
+    return (
+      <div>
+        <h1>Spielerliste</h1>
+        <PlayerForm state={state} actions={actions} />
+        <PlayerFilter state={state} actions={actions} />
+        <PlayerTable state={state} actions={actions} />
+      </div>
+    )
+  }
+}
+
+export default PlayerList

+ 56 - 0
src/playerList/components/PlayerTable.js

@@ -0,0 +1,56 @@
+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 () {
+    console.log('PlayerTable state', this.props.state)
+    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

+ 4 - 2
src/playerList/components/index.js

@@ -1,4 +1,6 @@
-import PlayerDisp from './PlayerDisp'
 import PlayerList from './PlayerList'
+import PlayerForm from './PlayerForm'
+import PlayerTable from './PlayerTable'
+import PlayerFilter from './PlayerFilter'
 
-export default { PlayerDisp, PlayerList }
+export default { PlayerList, PlayerForm, PlayerTable, PlayerFilter }

+ 25 - 0
src/playerList/functions.js

@@ -0,0 +1,25 @@
+import Excel from '../excel'         // Helper files to create Excel files
+import Player from '../classes/player'
+
+function handlePlayerList (event) {
+  const file = this.playerList.files[0]
+  Excel.readWorkbook(file, this.generatePlayerList)
+}
+
+export function generatePlayerList (worksheet) {
+  console.log('About to read the player list.')
+    /* const worksheets = { ...this.state.worksheets }
+    worksheets['PlayerList'] = worksheet
+    this.setState({ worksheets }) */
+
+  if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
+    throw Error('Wrong file structure.')
+  }
+  const players = worksheet.slice(4, worksheet.length).map((playerData) => new Player.Player(playerData))
+  const player = { ...this.state.player }
+  player.players = players
+  this.setState({ player })
+  this.calculatePayDate()
+  this.filterPlayers()
+  console.log('State after generating player list:', this.state)
+}

+ 47 - 2
src/playerList/index.js

@@ -1,5 +1,6 @@
-import { actions, reducer, state } from './state'
+import { actions, reducer, state, saga } from './state'
 import components from './components'
+import Player from '../classes/player'
 
 const filters = {
   all: players => players
@@ -7,4 +8,48 @@ const filters = {
 
 const selectors = {}
 
-export default { actions, components, filters, selectors, reducer, state }
+export default { actions, components, filters, selectors, reducer, state, saga }
+
+const FILTER_OFF = 'Alle'
+
+function calculatePlayerFilters () {
+  const categories = []
+  this.state.player.players.forEach((item) => {
+    if (!!item.Konkurrenz & !categories.includes(item.Konkurrenz)) {
+      categories.push(item.Ort)
+    }
+  })
+  const player = { ...this.state.player, categories }
+  this.setState({ player })
+}
+
+function generatePlayerList (worksheet) {
+  console.log('About to read the player list.')
+    /* const worksheets = { ...this.state.worksheets }
+    worksheets['PlayerList'] = worksheet
+    this.setState({ worksheets }) */
+
+  if (worksheet[4].length !== 32 & worksheet[3][0] !== 'Konkurrenz' & worksheet[3][31] !== 'bezahlt') {
+    throw Error('Wrong file structure.')
+  }
+  const players = worksheet.slice(4, worksheet.length).map((playerData) => new Player.Player(playerData))
+  const player = { ...this.state.player }
+  player.players = players
+  this.setState({ player })
+  this.calculatePayDate()
+  this.filterPlayers()
+  console.log('State after generating player list:', this.state)
+}
+
+function filterPlayers () {
+  const filters = {
+    junior: this.playerJunior.checked,
+    paid: this.playerPaid.checked,
+    category: this.playerCategory.value !== FILTER_OFF ? this.playerCategory.value : null
+  }
+  console.log('New filter settings:', filters)
+
+  const player = { ...this.state.player, filters }
+  player.filtered = player.players
+  this.setState({ player })
+}

+ 47 - 4
src/playerList/state.js

@@ -1,4 +1,6 @@
 /** @module player/state */
+import { put, takeEvery } from 'redux-saga/effects'
+import { generatePlayerList } from './functions'
 
 /**
  * state.js
@@ -13,6 +15,22 @@ export const actions = {
       type: 'SET_PLAYERS',
       players
     }
+  },
+  fileUploadStart: event => {
+    return {
+      type: 'PLAYER_FILE_UPLOAD_START',
+      event
+    }
+  },
+  fileUploadSuccess: () => {
+    return {
+      type: 'PLAYER_FILE_UPLOAD_SUCCESS'
+    }
+  },
+  fileUploadFailure: () => {
+    return {
+      type: 'PLAYER_FILE_UPLOAD_FAILURE'
+    }
   }
 }
 console.log('State actions', actions)
@@ -21,15 +39,40 @@ console.log('State actions', actions)
 export const state = {
   allPlayers: [],
   filteredPlayers: [],
-  filters: {}
+  filters: {},
+  fileUpload: 'idle'
 }
 console.log('State state', state)
 
 /** reducer is called by the redux dispatcher and handles all component actions */
 export function reducer (state = [], action) {
-  let nextState = state
-  return nextState
+  switch (action.type) {
+    case 'SET_PLAYERS':
+      return { ...state, allPlayers: action.players }
+    case 'PLAYER_FILE_UPLOAD_START':
+      return { ...state, fileUpload: 'started' }
+    case 'PLAYER_FILE_UPLOAD_SUCCESS':
+      return { ...state, fileUpload: 'finished' }
+    case 'PLAYER_FILE_UPLOAD_FAILURE':
+      return { ...state, fileUpload: 'failure' }
+    default:
+      return state
+  }
+}
+
+function * uploadFile (action) {
+  console.log('PlayerList uploadFile')
+  try {
+    console.log(action.event)
+    generatePlayerList(action)
+    yield put(actions.fileUploadSuccess())
+  } catch (e) {
+    yield put(actions.fileUploadFailure())
+  }
 }
 
 /** sagas are asynchronous workers (JS generators) to handle the state. */
-export function * saga () {}
+export function * saga () {
+  console.log('Player saga started.')
+  yield takeEvery('PLAYER_FILE_UPLOAD_START', uploadFile)
+}

+ 8 - 0
src/settings/index.js

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

+ 46 - 0
src/settings/state.js

@@ -0,0 +1,46 @@
+/** @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 () {}

+ 90 - 0
src/startPage/components/FileImport.js

@@ -0,0 +1,90 @@
+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) {
+    console.log(`process file`)
+    this.file = this.fileName.files[0]
+  }
+
+  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} props={this.props} onChange={this.processFile} />
+        <label htmlFor={this.props.name} onMouseEnter={this.getFocus} onMouseLeave={this.loseFocus} style={this.labelStyle}><span className='glyphicon glyphicon-open' ariaHidden='true' /> PlayerList.xls laden...</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>
+        <Button onClick={this.handlePlayerList}>PlayerList.xls</Button>
+        <Button onClick={this.handleCalendar}>Calendar.xls</Button>
+        <FileButton name='playerList' data-allowed-file-extensions='xls' />
+      </div>
+    )
+  }
+}
+
+export default FileImport

+ 16 - 0
src/startPage/components/StartPage.js

@@ -0,0 +1,16 @@
+import React from 'react'
+import FileImport from './FileImport'
+
+class StartPage extends React.Component {
+  render () {
+    return (
+      <div>
+        <h1>SZTM Planungshelfer</h1>
+        <p>Willkommen beim SZTM Planungshelfer</p>
+        <FileImport />
+      </div>
+    )
+  }
+}
+
+export default StartPage

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

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

+ 8 - 0
src/startPage/index.js

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

+ 46 - 0
src/startPage/state.js

@@ -0,0 +1,46 @@
+/** @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 () {}

+ 5 - 1
yarn.lock

@@ -1912,7 +1912,7 @@ dot-prop@^3.0.0:
   dependencies:
     is-obj "^1.0.0"
 
-dotenv@4.0.0:
+dotenv@4.0.0, dotenv@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
 
@@ -5144,6 +5144,10 @@ reduce-function-call@^1.0.1:
   dependencies:
     balanced-match "^0.4.2"
 
+redux-saga@^0.15.4:
+  version "0.15.4"
+  resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.15.4.tgz#27982a947280053b7ecbb5d3170c837a5fe6b261"
+
 redux@^3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/redux/-/redux-3.6.0.tgz#887c2b3d0b9bd86eca2be70571c27654c19e188d"