Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calltaker: Print group itineraries #398

Merged
merged 13 commits into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, session } = this.props
const cancelled = request.status === 'cancelled'
const printFieldTripLink = `/#/printFieldTrip/?requestId=${request.id}&sessionId=${session.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,
session: state.callTaker.session
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: session could be sessionId instead since session isn't used except to get the sessionId field.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 6ef72b9.

}
}

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,
ItineraryBody,
ItineraryContainer,
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>
<ItineraryBody>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got a little confused by this since we use itinerary-body to render an itinerary. Maybe rename it ItineraryBodyContainer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the class name in the old client that's why. Renamed in 6ef72b9.

<PrintableItinerary
config={config}
itinerary={itinerary}
LegIcon={LegIcon}
/>
<TripSummary itinerary={itinerary} />
</ItineraryBody>
</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 ItineraryBody = 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