Skip to content

Instantly share code, notes, and snippets.

@chrisoverstreet
Created April 13, 2020 13:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chrisoverstreet/65be6efcb3b7afb7830e219f1c6e732f to your computer and use it in GitHub Desktop.
Save chrisoverstreet/65be6efcb3b7afb7830e219f1c6e732f to your computer and use it in GitHub Desktop.
// @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