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

Add all day report indicators #3071

Closed
wants to merge 10 commits into from
Closed
24 changes: 16 additions & 8 deletions client/src/components/CustomDateInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ const CustomDateInput = ({
withTime,
value,
onChange,
onBlur
onBlur,
fullWidth,
allDay
}) => {
const inputRef = useRef()
const rightElement = showIcon && CalendarIcon(inputRef.current)
const width = 8 + (showIcon ? 3 : 0) + (withTime ? 3 : 0)
const style = { width: `${width}em`, fontSize: "1.1em" }
const dateFormats = withTime
? Settings.dateFormats.forms.input.withTime
: Settings.dateFormats.forms.input.date
const style = { width: fullWidth ? "100%" : `${width}em`, fontSize: "1.1em" }
const dateFormats =
withTime && !allDay
? Settings.dateFormats.forms.input.withTime
: Settings.dateFormats.forms.input.date
const inputFormat = dateFormats[0]
const timePickerProps = !withTime
? {}
? undefined
: {
precision: TimePrecision.MINUTE,
selectAllOnFocus: true
Expand Down Expand Up @@ -70,6 +73,7 @@ const CustomDateInput = ({
timePickerProps={timePickerProps}
popoverProps={{ usePortal: false }}
disabled={disabled}
fill={fullWidth}
/>
)
}
Expand All @@ -84,12 +88,16 @@ CustomDateInput.propTypes = {
PropTypes.instanceOf(Date)
]),
onChange: PropTypes.func,
onBlur: PropTypes.func
onBlur: PropTypes.func,
fullWidth: PropTypes.bool,
allDay: PropTypes.bool
}
CustomDateInput.defaultProps = {
disabled: false,
showIcon: true,
withTime: false
withTime: false,
fullWidth: false,
allDay: true
}

export default CustomDateInput
4 changes: 1 addition & 3 deletions client/src/components/ReportSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,7 @@ const ReportSummaryRow = ({ report }) => {
<Col md={12}>
{report.engagementDate && (
<Label bsStyle="default" className="engagement-date">
{moment(report.engagementDate).format(
Report.getEngagementDateFormat()
)}
{Report.getFormattedEngagementDate(report)}
</Label>
)}
</Col>
Expand Down
7 changes: 1 addition & 6 deletions client/src/components/ReportTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import UltimatePaginationTopDown from "components/UltimatePaginationTopDown"
import _get from "lodash/get"
import _isEqual from "lodash/isEqual"
import { Report } from "models"
import moment from "moment"
import PropTypes from "prop-types"
import React, { useEffect, useRef, useState } from "react"
import { Table } from "react-bootstrap"
Expand Down Expand Up @@ -172,11 +171,7 @@ const ReportTable = ({
/>
</td>
{showStatus && <td>{report.state}</td>}
<td>
{moment(report.engagementDate).format(
Report.getEngagementDateFormat()
)}
</td>
<td>{Report.getFormattedEngagementDate(report)}</td>
</tr>
))}
</tbody>
Expand Down
6 changes: 4 additions & 2 deletions client/src/components/aggregations/ReportsMapWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
import Leaflet from "components/Leaflet"
import _escape from "lodash/escape"
import _isEmpty from "lodash/isEmpty"
import { Location } from "models"
import { Location, Report } from "models"
import PropTypes from "prop-types"
import React, { useMemo } from "react"

Expand All @@ -25,7 +25,9 @@ const ReportsMapWidget = ({
values.forEach(report => {
if (Location.hasCoordinates(report.location)) {
let label = _escape(report.intent || "<undefined>") // escape HTML in intent!
label += `<br/>@ <b>${_escape(report.location.name)}</b>` // escape HTML in locationName!
label += `<br/>@ <b>${_escape(
report.location.name
)} ${Report.getAllDayIndicator(report)}</b>` // escape HTML in locationName!
markerArray.push({
id: report.uuid,
lat: report.location.lat,
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/aggregations/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export function reportsToEvents(reports) {
(r.location && r.location.name) ||
""
return {
title: who + "@" + where,
title: `${who} @ ${where} - ${Report.getAllDayIndicator(r)}`,
start: moment(r.engagementDate).format("YYYY-MM-DD HH:mm"),
end: moment(r.engagementDate)
.add(r.duration, "minutes")
Expand Down
2 changes: 1 addition & 1 deletion client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ fieldset.danger {
}

#duration {
width: 10em;
width: 100%;
}

.shortcut-list {
Expand Down
33 changes: 30 additions & 3 deletions client/src/models/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,26 @@ export default class Report extends Model {
.nullable()
.required("You must provide the Date of Engagement")
.default(null),
duration: yup.number().nullable().default(null),
duration: yup
.number()
.nullable()
.test(
"duration",
"You must provide duration when engagement time(hour:minute) is provided",
function(duration) {
const { engagementDate } = this.parent
if (
!engagementDate ||
moment(engagementDate).isSame(
moment(engagementDate).startOf("day")
)
) {
return true
}
return !!duration
}
)
.default(null),
// not actually in the database, but used for validation:
cancelled: yup
.boolean()
Expand Down Expand Up @@ -570,16 +589,24 @@ export default class Report extends Model {
)
}

static isEngagementAllDay(report) {
return !report.duration
}

static getAllDayIndicator(report) {
return Report.isEngagementAllDay(report) ? " (all day)" : ""
}

static getFormattedEngagementDate(report) {
if (!report?.engagementDate) {
return ""
}

const start = moment(report.engagementDate)
if (!report.duration) {
if (Report.isEngagementAllDay(report)) {
return Settings.engagementsIncludeTimeAndDuration
? start.format(Settings.dateFormats.forms.displayLong.date) +
" (all day)"
Report.getAllDayIndicator(report)
: start.format(Report.getEngagementDateFormat())
}

Expand Down
187 changes: 187 additions & 0 deletions client/src/pages/reports/EngagementDateFormPartial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Checkbox } from "@blueprintjs/core"
import CustomDateInput from "components/CustomDateInput"
import * as FieldHelper from "components/FieldHelper"
import { FastField, Field } from "formik"
import { Report } from "models"
import moment from "moment"
import PropTypes from "prop-types"
import React, { useState } from "react"
import { Col, ControlLabel, FormGroup, HelpBlock } from "react-bootstrap"
import Settings from "settings"
import utils from "utils"

const futureEngagementHint = (
<HelpBlock>
<span className="text-success">This will create a planned engagement</span>
</HelpBlock>
)

function isStartOfDay(date) {
return date && moment(date).isSame(moment(date).startOf("day"))
}

const EngagementDatePartialFormWithDuration = ({
setFieldValue,
setFieldTouched,
validateFieldDebounced,
initialValues,
edit,
values
}) => {
const [isAllDay, setIsAllDay] = useState(() =>
getInitalAllDayState(edit, initialValues)
)

return (
<FormGroup>
<Col xsHidden sm={2} componentClass={ControlLabel}>
Engagement planning
</Col>
<Col sm={3} style={{ marginLeft: "15px" }}>
<Field
name="engagementDate"
component={FieldHelper.SpecialField}
onChange={value => {
const sval = value ? moment(value).startOf("minute").toDate() : null
setFieldTouched("engagementDate", true, false) // onBlur doesn't work when selecting a date
setFieldValue("engagementDate", sval, true)
if (!sval) {
setIsAllDay(true)
} else if (!isStartOfDay(sval)) {
setIsAllDay(false)
}
}}
onBlur={() => setFieldTouched("engagementDate")}
widget={
<CustomDateInput
id="engagementDate"
withTime
fullWidth
allDay={isAllDay}
/>
}
vertical
>
{Report.isFuture(values.engagementDate) && futureEngagementHint}
</Field>
</Col>
<Col sm={2} style={{ marginLeft: "15px", whiteSpace: "nowrap" }}>
<Field
name="duration"
label="Duration (minutes)"
component={FieldHelper.InputField}
inputType="number"
onWheelCapture={event => event.currentTarget.blur()} // Prevent scroll action on number input
onChange={event => {
const safeVal =
utils.preventNonPositiveAndLongDigits(event.target.value, 4) ||
null
setFieldTouched("duration", true, false)
setFieldValue("duration", safeVal, false)
validateFieldDebounced("duration")
setIsAllDay(false)
}}
vertical
disabled={isAllDay}
/>
</Col>
<Col
sm={1}
style={{
marginTop: "2.2em",
maxWidth: "max-content",
display: "flex",
alignItems: "center",
whiteSpace: "nowrap"
}}
id="all-day-col"
>
<Checkbox
checked={isAllDay}
label="All Day"
id="all-day"
onChange={e => {
setIsAllDay(e.target.checked)
if (e.target.checked) {
setFieldValue("duration", null, true)
validateFieldDebounced("duration")
if (values.engagementDate) {
setFieldValue(
"engagementDate",
moment(values.engagementDate).startOf("day").toDate(),
false
)
}
}
}}
/>
</Col>
</FormGroup>
)
}

function getInitalAllDayState(edit, initialValues) {
if (!edit || !initialValues.engagementDate) {
return true
} else if (!isStartOfDay(initialValues.engagementDate)) {
return false
} else {
return initialValues.duration === null
}
}

EngagementDatePartialFormWithDuration.propTypes = {
setFieldValue: PropTypes.func.isRequired,
setFieldTouched: PropTypes.func.isRequired,
validateFieldDebounced: PropTypes.func.isRequired,
values: PropTypes.object.isRequired,
initialValues: PropTypes.instanceOf(Report).isRequired,
edit: PropTypes.bool.isRequired
}

const EngagementDateFormPartial = ({
setFieldValue,
setFieldTouched,
validateFieldDebounced,
initialValues,
edit,
values
}) => {
if (!Settings.engagementsIncludeTimeAndDuration) {
return (
<FastField
name="engagementDate"
component={FieldHelper.SpecialField}
onChange={value => {
const val = value ? moment(value).startOf("day").toDate() : null
setFieldValue("engagementDate", val, true)
}}
widget={<CustomDateInput id="engagementDate" />}
>
{Report.isFuture(values.engagementDate) && futureEngagementHint}
</FastField>
)
}

return (
<EngagementDatePartialFormWithDuration
setFieldValue={setFieldValue}
setFieldTouched={setFieldTouched}
validateFieldDebounced={validateFieldDebounced}
values={values}
initialValues={initialValues}
edit={edit}
/>
)
}

EngagementDateFormPartial.propTypes = {
setFieldValue: PropTypes.func.isRequired,
setFieldTouched: PropTypes.func.isRequired,
validateFieldDebounced: PropTypes.func.isRequired,
values: PropTypes.object.isRequired,
initialValues: PropTypes.instanceOf(Report).isRequired,
edit: PropTypes.bool.isRequired
}

export default EngagementDateFormPartial
Loading