Created
April 13, 2020 13:58
-
-
Save chrisoverstreet/65be6efcb3b7afb7830e219f1c6e732f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// @flow | |
import React from 'react'; | |
import css from 'styled-jsx/css'; | |
import classnames from 'classnames'; | |
import { useDispatch, useSelector } from 'react-redux'; | |
import { useApolloClient } from '@apollo/react-hooks'; | |
import pluralize from 'pluralize'; | |
import PropTypes from 'prop-types'; | |
import userHasLoginRole from '../../utils/userHasLoginRole'; | |
import userHasPermission from '../../utils/userHasPermission'; | |
import { USER_PERMISSION } from '../../constants/userPermissions'; | |
import { USER_ROLE } from '../../constants/userRoles'; | |
import addLeadingZeros from '../../utils/format/addLeadingZeros'; | |
import daysHrsMinsSecsToSeconds from '../../utils/format/daysHrsMinsSecsToSeconds'; | |
import secondsToDaysHrsMinsSecs from '../../utils/format/secondsToDaysHrsMinsSecs'; | |
import { Heading, Text, Icon, Modal, Button, Input } from '../homee-ui'; | |
import { | |
SPACING, | |
FONT_SIZE, | |
LINE_HEIGHT, | |
COLOR, | |
FONT_WEIGHT, | |
} from '../homee-ui/theme'; | |
import SubBucketIcon from './SubBucketIcon'; | |
import { | |
setInEditMode, | |
updateEdits, | |
updateJob, | |
} from '../../redux/actions/jobActions'; | |
import { SUB_BUCKET } from '../../constants/subBuckets'; | |
import type { Job } from '../../constants/workbenchTypes'; | |
import type { State } from '../../redux/store'; | |
type Props = {| | |
job: Job, | |
|}; | |
const icon = css.resolve` | |
.icon { | |
font-size: ${FONT_SIZE.s16}; | |
line-height: ${LINE_HEIGHT.s13}; | |
color: ${COLOR.blue}; | |
margin-left: ${SPACING.sm}px; | |
} | |
`; | |
const button = css.resolve` | |
button { | |
margin-right: ${SPACING.xs}px; | |
} | |
`; | |
const text = css.resolve` | |
p { | |
font-size: ${FONT_SIZE.s11} !important; | |
line-height: ${LINE_HEIGHT.s11} !important; | |
font-weight: ${FONT_WEIGHT.semi} !important; | |
} | |
`; | |
const TimeClock = ({ job }: Props) => { | |
const [isEdit, setIsEdit] = React.useState<boolean>(false); | |
const [openModal, setModalOpen] = React.useState<boolean>(false); | |
const client = useApolloClient(); | |
const dispatch = useDispatch(); | |
const authorized = useSelector( | |
({ user }: State) => | |
userHasLoginRole(user, USER_ROLE.internal) && | |
userHasPermission(user, USER_PERMISSION.USER_PERMISSION_IMPERSONATE), | |
); | |
const edits = useSelector(state => state.job.edits); | |
const isUpdating = useSelector(state => state.job.isUpdating); | |
const note = useSelector(state => state.job.edits?.newElapsedTimeNote || ''); | |
let elapsedTime = job.invoice?.elapsedTime ?? 0; | |
if (isEdit && edits.newElapsedTime) { | |
elapsedTime = edits.newElapsedTime; | |
} | |
const { days, hours, minutes, seconds } = secondsToDaysHrsMinsSecs( | |
elapsedTime, | |
); | |
const [editedDays, setEditedDays] = React.useState<string>(days.toString()); | |
const [editedHours, setEditedHours] = React.useState<string>( | |
hours.toString(), | |
); | |
const [editedMinutes, setEditedMinutes] = React.useState<string>( | |
minutes.toString(), | |
); | |
const [editedSeconds, setEditedSeconds] = React.useState<string>( | |
seconds.toString(), | |
); | |
const showEditButton = authorized && job.subBucket !== SUB_BUCKET.in_progress; | |
const onChange = (event: SyntheticEvent<HTMLInputElement>) => { | |
const { name, value } = event.currentTarget; | |
const formattedValue = value.replace(/[^0-9]+/g, ''); | |
switch (name) { | |
case 'days': | |
setEditedDays(formattedValue); | |
break; | |
case 'hours': | |
setEditedHours(formattedValue); | |
break; | |
case 'minutes': | |
setEditedMinutes(formattedValue); | |
break; | |
case 'seconds': | |
setEditedSeconds(formattedValue); | |
break; | |
default: | |
} | |
const valueAsInt = parseInt(value, 10); | |
const newElapsedTime = daysHrsMinsSecsToSeconds({ | |
days: name === 'days' ? valueAsInt : days, | |
hours: name === 'hours' ? valueAsInt : hours, | |
minutes: name === 'minutes' ? valueAsInt : minutes, | |
seconds: name === 'seconds' ? valueAsInt : seconds, | |
}); | |
dispatch(updateEdits({ newElapsedTime })); | |
}; | |
const onBlur = (event: SyntheticEvent<HTMLInputElement>) => { | |
const { name, value } = event.currentTarget; | |
if (name === 'hours' && parseInt(value, 10) > 23) { | |
setEditedDays(prev => ((parseInt(prev, 10) || 0) + 1).toString()); | |
setEditedHours(prev => (parseInt(prev, 10) - 24).toString()); | |
} | |
}; | |
// timer in edit state | |
const renderEditInput = () => { | |
return ( | |
<div className="edit-input"> | |
<div className="day-editor"> | |
<Text className={text.className}>+</Text> | |
<Input | |
placeholder={addLeadingZeros(days)} | |
disabled={!isEdit} | |
maxLength="2" | |
name="days" | |
id="days" | |
type="text" | |
onBlur={onBlur} | |
onChange={onChange} | |
value={editedDays} | |
/> | |
<Text className={text.className}>days</Text> | |
</div> | |
<div className="input-values"> | |
<Input | |
placeholder={addLeadingZeros(hours)} | |
disabled={!isEdit} | |
maxLength="2" | |
name="hours" | |
id="hours" | |
type="text" | |
onBlur={onBlur} | |
onChange={onChange} | |
value={editedHours} | |
/> | |
<Text>:</Text> | |
<Input | |
placeholder={addLeadingZeros(minutes)} | |
disabled={!isEdit} | |
maxLength="2" | |
name="minutes" | |
id="minutes" | |
type="text" | |
onBlur={onBlur} | |
onChange={onChange} | |
value={editedMinutes} | |
/> | |
<Text>:</Text> | |
<Input | |
placeholder={addLeadingZeros(seconds)} | |
disabled={!isEdit} | |
maxLength="2" | |
name="seconds" | |
id="seconds" | |
type="text" | |
onBlur={onBlur} | |
onChange={onChange} | |
value={editedSeconds} | |
/> | |
</div> | |
<div className="edit-buttons"> | |
<Button | |
className={button.className} | |
isLoading={false} | |
onClick={() => { | |
setModalOpen(true); | |
}} | |
size="small" | |
type="button" | |
variant="primary" | |
> | |
Save | |
</Button> | |
<Button | |
className={button.className} | |
onClick={() => { | |
setIsEdit(false); | |
dispatch(setInEditMode(false)); | |
}} | |
size="small" | |
type="button" | |
variant="secondary" | |
> | |
Cancel | |
</Button> | |
</div> | |
<Modal | |
isOpen={openModal} | |
onRequestClose={() => setModalOpen(false)} | |
size="small" | |
subTitle="Include a reason for editing the timer. Changing the timer will affect | |
labor pricing." | |
title="Edit Timer" | |
> | |
{/* reason for editing timer */} | |
<form onSubmit={event => event.preventDefault()}> | |
<Input | |
disabled={isUpdating} | |
id="edit-timer-note" | |
label="Enter reason for editing timer" | |
name="edit-timer-note" | |
onChange={(event: SyntheticEvent<HTMLInputElement>) => { | |
const { value } = event.currentTarget; | |
dispatch( | |
updateEdits({ newElapsedTimeNote: value || undefined }), | |
); | |
}} | |
type="text" | |
value={note} | |
/> | |
<div className="modal-buttons"> | |
<Button | |
className={button.className} | |
disabled={isUpdating || note.length < 3} | |
onClick={async () => { | |
dispatch( | |
updateEdits({ newElapsedTimeNote: 'Required note' }), | |
); | |
await dispatch(updateJob(job.id, client)); | |
setIsEdit(false); | |
setModalOpen(false); | |
}} | |
size="large" | |
isLoading={isUpdating} | |
type="submit" | |
variant="primary" | |
> | |
Save | |
</Button> | |
<Button | |
className={button.className} | |
onClick={() => { | |
setModalOpen(false); | |
}} | |
size="large" | |
type="button" | |
variant="negative" | |
> | |
Cancel | |
</Button> | |
</div> | |
</form> | |
</Modal> | |
{text.styles} | |
{button.styles} | |
<style jsx> | |
{` | |
.day-editor, | |
.input-values :global(input) { | |
text-align: center; | |
} | |
.day-editor { | |
display: flex; | |
align-items: center; | |
max-width: 80px; | |
margin-bottom: ${SPACING.xs}px; | |
} | |
.input-values { | |
display: flex; | |
align-items: center; | |
max-width: 150px; | |
} | |
.edit-buttons { | |
display: flex; | |
margin-top: ${SPACING.md}px; | |
} | |
.modal-buttons { | |
display: flex; | |
margin-top: ${SPACING.md}px; | |
justify-content: flex-end; | |
} | |
`} | |
</style> | |
</div> | |
); | |
}; | |
// Timer | |
const renderTimer = () => { | |
return ( | |
<React.Fragment> | |
<div className="days"> | |
{days > 0 && ( | |
<Text size="small" weight="semi"> | |
+{pluralize('day', days, true)} | |
</Text> | |
)} | |
<Heading variant="h4"> | |
{addLeadingZeros(hours)}:{addLeadingZeros(minutes)}: | |
{addLeadingZeros(seconds)} | |
</Heading> | |
</div> | |
{showEditButton && ( | |
<button | |
className="edit-icon" | |
onClick={() => { | |
setIsEdit(true); | |
dispatch(setInEditMode(true)); | |
}} | |
type="button" | |
> | |
<Icon icon="pencil" className={icon.className} /> | |
</button> | |
)} | |
{icon.styles} | |
{button.styles} | |
<style jsx> | |
{` | |
.edit-icon { | |
align-items: center; | |
background-color: transparent; | |
border: none; | |
display: flex; | |
height: ${LINE_HEIGHT.s24}; | |
} | |
`} | |
</style> | |
</React.Fragment> | |
); | |
}; | |
return ( | |
<div className="root"> | |
<div | |
className={ | |
!isEdit | |
? 'status-icon-wrapper' | |
: classnames('isEdit', 'status-icon-wrapper') | |
} | |
> | |
<SubBucketIcon | |
subBucket={job.subBucket} | |
className={classnames(icon.className, 'icon')} | |
/> | |
</div> | |
{authorized && isEdit ? renderEditInput() : renderTimer()} | |
{icon.styles} | |
{button.styles} | |
<style jsx> | |
{` | |
.root { | |
display: flex !important; | |
align-items: flex-end; | |
} | |
.status-icon-wrapper { | |
height: ${LINE_HEIGHT.s24}; | |
display: flex; | |
margin-right: ${SPACING.sm}px; | |
align-items: center; | |
} | |
.isEdit { | |
height: ${LINE_HEIGHT.s64} !important; | |
} | |
`} | |
</style> | |
</div> | |
); | |
}; | |
TimeClock.propTypes = { | |
job: PropTypes.object.isRequired, | |
}; | |
export default TimeClock; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment