Skip to content

Commit

Permalink
Merge pull request #398 from opentripplanner/calltaker-print-group-itins
Browse files Browse the repository at this point in the history
Calltaker: Print group itineraries
  • Loading branch information
evansiroky authored Jul 1, 2021
2 parents 6918774 + 6ef72b9 commit 1ed767a
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 20 deletions.
8 changes: 4 additions & 4 deletions lib/actions/field-trip.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import { serialize } from 'object-to-formdata'
import qs from 'qs'
import { createAction } from 'redux-actions'

import {getGroupSize, getTripFromRequest, sessionIsInvalid} from '../util/call-taker'

import {routingQuery} from './api'
import {toggleCallHistory} from './call-taker'
import {resetForm, setQueryParam} from './form'
import {getGroupSize, getTripFromRequest, sessionIsInvalid} from '../util/call-taker'

if (typeof (fetch) === 'undefined') require('isomorphic-fetch')

/// PRIVATE ACTIONS

const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS')
const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS')
const receivedFieldTripDetails = createAction('RECEIVED_FIELD_TRIP_DETAILS')
const requestingFieldTrips = createAction('REQUESTING_FIELD_TRIPS')
const requestingFieldTripDetails = createAction('REQUESTING_FIELD_TRIP_DETAILS')

// PUBLIC ACTIONS

export const receivedFieldTrips = createAction('RECEIVED_FIELD_TRIPS')
export const setFieldTripFilter = createAction('SET_FIELD_TRIP_FILTER')
export const setActiveFieldTrip = createAction('SET_ACTIVE_FIELD_TRIP')
export const setGroupSize = createAction('SET_GROUP_SIZE')
Expand Down
28 changes: 19 additions & 9 deletions lib/components/admin/field-trip-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import { connect } from 'react-redux'
import styled from 'styled-components'

import * as fieldTripActions from '../../actions/field-trip'
import Icon from '../narrative/icon'
import {
getGroupSize,
GROUP_FIELDS,
PAYMENT_FIELDS,
TICKET_TYPES
} from '../../util/call-taker'

import DraggableWindow from './draggable-window'
import EditableSection from './editable-section'
import FieldTripNotes from './field-trip-notes'
import Icon from '../narrative/icon'
import {
Bold,
Button,
Expand All @@ -25,12 +32,6 @@ import {
} from './styled'
import TripStatus from './trip-status'
import Updatable from './updatable'
import {
getGroupSize,
GROUP_FIELDS,
PAYMENT_FIELDS,
TICKET_TYPES
} from '../../util/call-taker'

const WindowHeader = styled(DefaultWindowHeader)`
margin-bottom: 0px;
Expand Down Expand Up @@ -59,7 +60,9 @@ class FieldTripDetails extends Component {
}

_renderFooter = () => {
const cancelled = this.props.request.status === 'cancelled'
const { request, sessionId } = this.props
const cancelled = request.status === 'cancelled'
const printFieldTripLink = `/#/printFieldTrip/?requestId=${request.id}&sessionId=${sessionId}`
return (
<div style={{padding: '5px 10px 0px 10px'}}>
<DropdownButton
Expand All @@ -81,6 +84,12 @@ class FieldTripDetails extends Component {
>
<Icon type='file-text-o' /> Receipt link
</MenuItem>
<MenuItem
href={printFieldTripLink}
target='_blank'
>
<Icon type='print' /> Printable trip plan
</MenuItem>
</DropdownButton>
<Button
bsSize='xsmall'
Expand Down Expand Up @@ -214,7 +223,8 @@ const mapStateToProps = (state, ownProps) => {
currentQuery: state.otp.currentQuery,
datastoreUrl: state.otp.config.datastoreUrl,
dateFormat: getDateFormat(state.otp.config),
request
request,
sessionId: state.callTaker.session.sessionId
}
}

Expand Down
184 changes: 184 additions & 0 deletions lib/components/admin/print-field-trip-layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import PrintableItinerary from '@opentripplanner/printable-itinerary'
import React, { Component } from 'react'
import { Button } from 'react-bootstrap'
import { connect } from 'react-redux'

import * as callTakerActions from '../../actions/call-taker'
import * as fieldTripActions from '../../actions/field-trip'
import { getTripFromRequest, lzwDecode } from '../../util/call-taker'
import { ComponentContext } from '../../util/contexts'
import { addPrintViewClassToRootHtml, clearClassFromRootHtml } from '../../util/print'

import {
Header,
ItineraryContainer,
PrintableItineraryContainer,
PrintLayout,
TripBody,
TripContainer,
TripInfoList,
TripSummary,
TripTitle,
Val
} from './print-styled'

/**
* Component that renders the print version of field trip itineraries.
*/
class PrintFieldTripLayout extends Component {
static contextType = ComponentContext

_print = () => {
window.print()
}

componentDidMount () {
const { initializeModules } = this.props
// Add print-view class to html tag to ensure that iOS scroll fix only applies
// to non-print views.
addPrintViewClassToRootHtml()

// Load call-taker/field-trip functionality (performs a fetch).
initializeModules()
}

componentDidUpdate (prevProps) {
const {
fetchFieldTripDetails,
receivedFieldTrips,
request,
requestId,
session
} = this.props
if (!prevProps.session && session) {
// When session is set,
// create a placeholder in the calltaker redux state that has just one request
// for fetching/receiving the details of the field trip per request id.
receivedFieldTrips({
fieldTrips: [{
endTime: 0,
id: requestId
}]
})
fetchFieldTripDetails(requestId)
}

if (request && request !== prevProps.request) {
// Set window title when request has fully loaded
// (appears in print headings)
const { endLocation, schoolName } = request
document.title = `Field Trip: ${schoolName} to ${endLocation}`
}
}

componentWillUnmount () {
clearClassFromRootHtml()
}

render () {
const { config, request } = this.props
const { LegIcon } = this.context
if (!request) return null

const {
address,
classpassId,
emailAddress,
endLocation,
faxNumber,
grade,
numChaperones,
numFreeStudents,
numStudents,
phoneNumber,
schoolName,
teacherName,
timeStamp
} = request

// Outbound/inbound template
const tripStructure = [
{
title: 'Outbound Trip (to Destination)',
trip: getTripFromRequest(request, true),
tripAbsentMessage: 'No Outbound Trip Planned'
},
{
title: 'Inbound Trip (from Destination)',
trip: getTripFromRequest(request, false),
tripAbsentMessage: 'No Inbound Trip Planned'
}
]

return (
<PrintLayout>
<Header>
<Button bsSize='small' onClick={this._print} style={{ float: 'right' }}>
<i className='fa fa-print' /> Print
</Button>
<TripTitle>Field Trip Plan: {schoolName} to {endLocation}</TripTitle>
</Header>
<TripInfoList>
<li><b>Teacher</b>: <Val>{teacherName}</Val> ({schoolName}, Grade: <Val>{grade}</Val>)</li>
<li><b>Teacher Address</b>: <Val>{address}</Val></li>
<li><b>Phone</b>: <Val>{phoneNumber}</Val> / <b>Fax</b>: <Val>{faxNumber}</Val></li>
<li><b>Email</b>: <Val>{emailAddress}</Val></li>
<li><b>Students Age 7 and Over</b>: {numStudents || 0}</li>
<li><b>Students Age 6 and Under</b>: {numFreeStudents || 0}</li>
<li><b>Chaperones</b>: {numChaperones || 0}</li>
{classpassId && <li><b>Class Pass Hop Card #</b>: {classpassId}</li>}
<li><i>Request submitted: {timeStamp}</i></li>
</TripInfoList>

{tripStructure.map(({ title, trip, tripAbsentMessage }, i) => (
<TripContainer key={i}>
<h2>{title}</h2>
{trip
? trip.groupItineraries?.map((groupItin, i) => {
const itinerary = JSON.parse(lzwDecode(groupItin.itinData))
return (
<TripBody key={i}>
<ItineraryContainer>
<h3>{groupItin.passengers} passengers on following itinerary:</h3>
<PrintableItineraryContainer>
<PrintableItinerary
config={config}
itinerary={itinerary}
LegIcon={LegIcon}
/>
<TripSummary itinerary={itinerary} />
</PrintableItineraryContainer>
</ItineraryContainer>
</TripBody>
)
})
: <TripBody><i>{tripAbsentMessage}</i></TripBody>
}
</TripContainer>
))}
</PrintLayout>
)
}
}

// connect to the redux store

const mapStateToProps = (state, ownProps) => {
const requestId = parseInt(state.router.location.query.requestId)
const { requests } = state.callTaker.fieldTrip
const request = requests.data.find(req => req.id === requestId)
return {
config: state.otp.config,
request,
requestId,
session: state.callTaker.session
}
}

const mapDispatchToProps = {
fetchFieldTripDetails: fieldTripActions.fetchFieldTripDetails,
initializeModules: callTakerActions.initializeModules,
receivedFieldTrips: fieldTripActions.receivedFieldTrips
}

export default connect(mapStateToProps, mapDispatchToProps)(PrintFieldTripLayout)
77 changes: 77 additions & 0 deletions lib/components/admin/print-styled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import styled from 'styled-components'

import TripDetails from '../narrative/connected-trip-details'

// This file contains styles specific for rendering PrintFieldTripLayout.
// They generally mimic the styles found in the OTP native client.

export const PrintLayout = styled.div`
font-size: 16px;
line-height: 115%;
margin: 8px;
`

export const Header = styled.div``

export const TripTitle = styled.h1`
border-bottom: 3px solid gray;
font-size: 30px;
font-weight: bold;
`

export const TripInfoList = styled.ul`
font-size: 16px;
list-style: none;
margin-top: 1em;
padding: 0;
`

export const Val = styled.span`
:empty:before {
content: 'N/A';
}
`

// The styles below mirror those found in OTP native client.
export const TripContainer = styled.div`
background: #ddd;
margin-top: 1em;
& > h2 {
font-size: 20px;
font-weight: bold;
margin: 0;
padding: 4px;
}
`

export const TripBody = styled.div`
padding: 8px;
`

export const ItineraryContainer = styled.div`
border: 3px solid #444;
margin-top: .5em;
& > h3 {
background: #444;
color: white;
font-size: 18px;
font-weight: bold;
margin: 0;
padding: 4px;
}
`

export const PrintableItineraryContainer = styled.div`
background: white;
padding: 12px;
`

export const TripSummary = styled(TripDetails)`
background: #eee;
border: 1px solid #bbb;
border-radius: 0;
margin-top: 15px;
padding: 5px;
`
10 changes: 3 additions & 7 deletions lib/components/app/print-layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { routingQuery } from '../../actions/api'
import DefaultMap from '../map/default-map'
import TripDetails from '../narrative/connected-trip-details'
import { ComponentContext } from '../../util/contexts'
import { addPrintViewClassToRootHtml, clearClassFromRootHtml } from '../../util/print'
import { getActiveItinerary } from '../../util/state'

class PrintLayout extends Component {
Expand Down Expand Up @@ -38,20 +39,15 @@ class PrintLayout extends Component {
const { location, parseUrlQueryString } = this.props
// Add print-view class to html tag to ensure that iOS scroll fix only applies
// to non-print views.
const root = document.getElementsByTagName('html')[0]
root.setAttribute('class', 'print-view')
addPrintViewClassToRootHtml()
// Parse the URL query parameters, if present
if (location && location.search) {
parseUrlQueryString()
}
}

/**
* Remove class attribute from html tag on clean up.
*/
componentWillUnmount () {
const root = document.getElementsByTagName('html')[0]
root.removeAttribute('class')
clearClassFromRootHtml()
}

render () {
Expand Down
Loading

0 comments on commit 1ed767a

Please sign in to comment.