Skip to content

Commit cf720f4

Browse files
committed
feat(mailables): add mailables for calltaker
1 parent 29d455d commit cf720f4

10 files changed

Lines changed: 955 additions & 56 deletions

File tree

lib/actions/call-taker.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const storeSession = createAction('STORE_SESSION')
2424

2525
export const beginCall = createAction('BEGIN_CALL')
2626
export const toggleCallHistory = createAction('TOGGLE_CALL_HISTORY')
27+
export const toggleMailables = createAction('TOGGLE_MAILABLES')
2728

2829
/**
2930
* Fully reset form and toggle call history (and close field trips if open).
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { Component } from 'react'
2+
import { connect } from 'react-redux'
3+
4+
import * as callTakerActions from '../../actions/call-taker'
5+
import CallRecord from './call-record'
6+
import DraggableWindow from './draggable-window'
7+
import Icon from '../narrative/icon'
8+
import {WindowHeader} from './styled'
9+
10+
class CallHistoryWindow extends Component {
11+
render () {
12+
const {callTaker, fetchQueries, searches, toggleCallHistory} = this.props
13+
const {activeCall, callHistory} = callTaker
14+
if (!callHistory.visible) return null
15+
return (
16+
<DraggableWindow
17+
header={<WindowHeader><Icon type='history' /> Call history</WindowHeader>}
18+
onClickClose={toggleCallHistory}
19+
style={{right: '15px', top: '50px', width: '450px'}}
20+
>
21+
{activeCall
22+
? <CallRecord
23+
call={activeCall}
24+
searches={searches}
25+
inProgress />
26+
: null
27+
}
28+
{callHistory.calls.data.length > 0
29+
? callHistory.calls.data.map((call, i) => (
30+
<CallRecord
31+
key={i}
32+
index={i}
33+
call={call}
34+
fetchQueries={fetchQueries} />
35+
))
36+
: <div>No calls in history</div>
37+
}
38+
</DraggableWindow>
39+
)
40+
}
41+
}
42+
43+
const mapStateToProps = (state, ownProps) => {
44+
return {
45+
callTaker: state.callTaker,
46+
currentQuery: state.otp.currentQuery,
47+
searches: state.otp.searches
48+
}
49+
}
50+
51+
const mapDispatchToProps = {
52+
fetchQueries: callTakerActions.fetchQueries,
53+
toggleCallHistory: callTakerActions.toggleCallHistory
54+
}
55+
56+
export default connect(mapStateToProps, mapDispatchToProps)(CallHistoryWindow)
Lines changed: 7 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,19 @@
11
import React, { Component } from 'react'
2-
import { connect } from 'react-redux'
32

4-
import * as callTakerActions from '../../actions/call-taker'
5-
import CallRecord from './call-record'
6-
import DraggableWindow from './draggable-window'
7-
import Icon from '../narrative/icon'
8-
import {WindowHeader} from './styled'
3+
import CallHistoryWindow from './call-history-window'
4+
import MailablesWindow from './mailables-window'
95

106
/**
117
* Collects the various draggable windows used in the Call Taker module to
128
* display, for example, the call record list and (TODO) the list of field trips.
139
*/
14-
class CallTakerWindows extends Component {
10+
export default class CallTakerWindows extends Component {
1511
render () {
16-
const {callTaker, fetchQueries, searches, toggleCallHistory} = this.props
17-
const {activeCall, callHistory} = callTaker
18-
if (!callHistory.visible) return null
1912
return (
20-
<DraggableWindow
21-
header={<WindowHeader><Icon type='history' /> Call history</WindowHeader>}
22-
onClickClose={toggleCallHistory}
23-
style={{right: '15px', top: '50px', width: '450px'}}
24-
>
25-
{activeCall
26-
? <CallRecord
27-
call={activeCall}
28-
searches={searches}
29-
inProgress />
30-
: null
31-
}
32-
{callHistory.calls.data.length > 0
33-
? callHistory.calls.data.map((call, i) => (
34-
<CallRecord
35-
key={i}
36-
index={i}
37-
call={call}
38-
fetchQueries={fetchQueries} />
39-
))
40-
: <div>No calls in history</div>
41-
}
42-
</DraggableWindow>
13+
<>
14+
<CallHistoryWindow />
15+
<MailablesWindow />
16+
</>
4317
)
4418
}
4519
}
46-
47-
const mapStateToProps = (state, ownProps) => {
48-
return {
49-
callTaker: state.callTaker,
50-
currentQuery: state.otp.currentQuery,
51-
searches: state.otp.searches
52-
}
53-
}
54-
55-
const mapDispatchToProps = {
56-
fetchQueries: callTakerActions.fetchQueries,
57-
toggleCallHistory: callTakerActions.toggleCallHistory
58-
}
59-
60-
export default connect(mapStateToProps, mapDispatchToProps)(CallTakerWindows)

lib/components/admin/draggable-window.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default class DraggableWindow extends Component {
1414
header,
1515
height = '245px',
1616
onClickClose,
17+
scroll = true,
1718
style
1819
} = this.props
1920
const GREY_BORDER = '#777 1.3px solid'
@@ -57,7 +58,7 @@ export default class DraggableWindow extends Component {
5758
<div style={{
5859
height,
5960
margin: '0px 5px',
60-
overflowY: 'scroll'
61+
overflowY: scroll ? 'scroll' : null
6162
}}>
6263
{children}
6364
</div>
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import React, { Component } from 'react'
2+
import { connect } from 'react-redux'
3+
4+
import * as callTakerActions from '../../actions/call-taker'
5+
import DraggableWindow from './draggable-window'
6+
import Icon from '../narrative/icon'
7+
import {WindowHeader} from './styled'
8+
import {createLetter, MAILABLE_FIELDS} from '../../util/mailables'
9+
10+
/**
11+
* A window enabled through the Call Taker module that allows Call Taker users
12+
* to generate a PDF with an invoice of items to be mailed to transit customers.
13+
*/
14+
class MailablesWindow extends Component {
15+
constructor (props) {
16+
super(props)
17+
this.state = {
18+
mailables: []
19+
}
20+
}
21+
22+
_addMailable = (mailable) => {
23+
const mailables = [...this.state.mailables]
24+
if (!mailables.find(m => m.name === mailable.name)) {
25+
mailables.push({...mailable, quantity: 1})
26+
} else {
27+
// FIXME: Increase quanity?
28+
}
29+
this.setState({mailables})
30+
}
31+
32+
_removeMailable = (mailable) => {
33+
const mailables = [...this.state.mailables]
34+
const removeIndex = mailables.findIndex(m => m.name === mailable.name)
35+
mailables.splice(removeIndex, 1)
36+
this.setState({mailables})
37+
}
38+
39+
_updateMailableField = (index, fieldName, value) => {
40+
const mailables = [...this.state.mailables]
41+
mailables[index] = {...mailables[index], [fieldName]: value}
42+
this.setState({mailables})
43+
}
44+
45+
_onClickCreateLetter = () => createLetter(this.state, this.props.callConfig.options)
46+
47+
_updateField = (evt) => {
48+
this.setState({[evt.target.id]: evt.target.value})
49+
}
50+
51+
render () {
52+
const {callConfig, callTaker, toggleMailables} = this.props
53+
const {mailables: selectedMailables} = this.state
54+
const {mailables} = callConfig.options
55+
if (!callTaker.mailables.visible) return null
56+
const selectableMailables = mailables.filter(m => !selectedMailables.find(mailable => mailable.name === m.name))
57+
return (
58+
<DraggableWindow
59+
header={
60+
<>
61+
<WindowHeader>
62+
<Icon type='graduation-cap' /> Mailables{' '}
63+
<span className='pull-right'>
64+
<button
65+
className='clear-button-formatting'
66+
onClick={this._onClickCreateLetter}
67+
style={{marginRight: '5px', verticalAlign: 'bottom'}}
68+
>
69+
Create Letter
70+
</button>
71+
</span>
72+
</WindowHeader>
73+
</>
74+
}
75+
onClickClose={toggleMailables}
76+
scroll={false}
77+
style={{width: '600px'}}
78+
>
79+
<div>
80+
{MAILABLE_FIELDS.map(f => (
81+
<input
82+
key={f.fieldName}
83+
id={f.fieldName}
84+
onChange={this._updateField}
85+
placeholder={f.fieldName}
86+
value={this.state[f.fieldName]} />
87+
))}
88+
</div>
89+
<div style={{display: 'flex'}}>
90+
<div style={{width: '300px'}}>
91+
<h4>All Mailables</h4>
92+
<div style={{maxHeight: '120px', overflowY: 'scroll'}}>
93+
{selectableMailables.map((mailable, i) => (
94+
<MailableOption
95+
key={mailable.name}
96+
mailable={mailable}
97+
onClick={this._addMailable} />
98+
))}
99+
</div>
100+
</div>
101+
<div style={{width: '300px'}}>
102+
<h4>Selected Mailables</h4>
103+
<div style={{maxHeight: '120px', overflowY: 'scroll'}}>
104+
{selectedMailables.length > 0
105+
? selectedMailables.map((mailable, i) => (
106+
<MailableOption
107+
index={i}
108+
key={mailable.name}
109+
mailable={mailable}
110+
onClear={this._removeMailable}
111+
updateField={this._updateMailableField} />
112+
))
113+
: <div className='text-muted'>No mailables selected.</div>
114+
}
115+
</div>
116+
</div>
117+
</div>
118+
</DraggableWindow>
119+
)
120+
}
121+
}
122+
123+
class MailableOption extends Component {
124+
_changeLargeFormat = (evt) => {
125+
const {index, updateField} = this.props
126+
updateField(index, 'largeFormat', evt.target.checked)
127+
}
128+
129+
_changeQuantity = (evt) => {
130+
const {index, updateField} = this.props
131+
updateField(index, 'quantity', evt.target.value)
132+
}
133+
134+
_onClear = () => this.props.onClear && this.props.onClear(this.props.mailable)
135+
136+
_onClick = () => this.props.onClick && this.props.onClick(this.props.mailable)
137+
138+
render () {
139+
const {mailable, onClear} = this.props
140+
const isSelected = Boolean(onClear)
141+
if (isSelected) {
142+
return (
143+
<div>
144+
{mailable.name}
145+
<button className='pull-right clear-button-formatting' onClick={onClear}>
146+
x
147+
</button>
148+
<div>
149+
<input
150+
min={0}
151+
onChange={this._changeQuantity}
152+
step={1}
153+
style={{marginRight: '5px', width: '50px'}}
154+
type='number'
155+
value={mailable.quantity} />
156+
{mailable.largePrint &&
157+
<>
158+
<input
159+
id='largeFormat'
160+
onChange={this._changeLargeFormat}
161+
type='checkbox'
162+
value={mailable.largeFormat} />
163+
<label style={{marginLeft: '5px'}} htmlFor='largeFormat'>
164+
Large format?
165+
</label>
166+
</>
167+
}
168+
</div>
169+
</div>
170+
)
171+
}
172+
return (
173+
<button className='clear-button-formatting' onClick={this._onClick}>
174+
{mailable.name}
175+
</button>
176+
)
177+
}
178+
}
179+
180+
const mapStateToProps = (state, ownProps) => {
181+
const callConfig = state.otp.config.modules.find(m => m.id === 'call')
182+
return {
183+
callConfig,
184+
callTaker: state.callTaker
185+
}
186+
}
187+
188+
const mapDispatchToProps = {
189+
toggleMailables: callTakerActions.toggleMailables
190+
}
191+
192+
export default connect(mapStateToProps, mapDispatchToProps)(MailablesWindow)

lib/reducers/call-taker.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ function createCallTakerReducer () {
2626
},
2727
visible: false
2828
},
29+
mailables: {
30+
visible: true
31+
},
2932
session: null
3033
}
3134
return (state = initialState, action) => {
@@ -148,6 +151,11 @@ function createCallTakerReducer () {
148151
}
149152
})
150153
}
154+
case 'TOGGLE_MAILABLES': {
155+
return update(state, {
156+
mailables: { visible: { $set: !state.mailables.visible } }
157+
})
158+
}
151159
case 'END_CALL': {
152160
return update(state, {
153161
activeCall: { $set: null }

0 commit comments

Comments
 (0)