Created
July 8, 2021 09:44
-
-
Save tamunoibi/8d8a668a78f8384f67e8478033834cff to your computer and use it in GitHub Desktop.
This file contains 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
/** @jsxFrag React.Fragment */ | |
/** @jsx jsx */ | |
import React, { useEffect, useRef, useState } from "react"; | |
import { css, jsx } from "@emotion/core"; | |
import Moment from "moment-timezone"; | |
import { gql, useMutation, useQuery } from "@apollo/client"; | |
import uuidv4 from "uuid/v4"; | |
import { | |
compressToEncodedURIComponent, | |
decompressFromEncodedURIComponent, | |
} from "lz-string"; | |
import { ModalContainer, ModalHeader } from "../toolBox/MenuModal"; | |
import Calendar from "../calendar/Calendar2"; | |
import Button from "../toolBox/Button"; | |
import OutsideAlerter from "../clickOutside/OutsideAlerter"; | |
import { colors } from "../../helpers/styles"; | |
import { | |
getNextHalforWhole, | |
getTimeSlots, | |
timeValidator, | |
} from "../../helpers/duration"; | |
import produce from "immer"; | |
import { getDuration } from "../../helpers/duration"; | |
import { round } from "../leave/functions"; | |
import ToolTip from "../toolBox/ToolTip"; | |
import Modal from "../toolBox/Modal"; | |
import Loader from "../loader/Loader"; | |
import { format24as12, format12as24 } from "../../helpers/functions"; | |
import EmptyState from "../toolBox/EmptyState"; | |
function getActivityRequireds({ activityRequireds, updateList, activityType }) { | |
let newActivityRequireds = activityRequireds; | |
updateList.forEach((updateMade) => { | |
const startTime = updateMade.startTime; | |
const endTime = updateMade.endTime; | |
if (updateMade.removed) { | |
newActivityRequireds = newActivityRequireds.filter( | |
(a) => a.id !== updateMade.id | |
); | |
} else if (updateMade.id.includes("-")) { | |
const newRow = { | |
activityType, | |
id: updateMade.id, | |
startTime, | |
endTime, | |
fteAmount: updateMade.fteAmount, | |
error: updateMade.error, | |
}; | |
newActivityRequireds = [...newActivityRequireds, newRow]; | |
} else { | |
const index = newActivityRequireds.findIndex( | |
(a) => a.id === updateMade.id | |
); | |
newActivityRequireds = produce(newActivityRequireds, (draftState) => { | |
draftState[index] = { | |
...newActivityRequireds[index], | |
startTime, | |
endTime, | |
fteAmount: updateMade.fteAmount, | |
error: updateMade.error, | |
}; | |
}); | |
} | |
}); | |
return newActivityRequireds; | |
} | |
export function Color({ color }) { | |
return ( | |
<span | |
css={css` | |
float: left; | |
display: inline-block; | |
width: 18px; | |
height: 18px; | |
border: 1px solid #e2e2e2; | |
border-radius: 3px; | |
box-sizing: border-box; | |
background: white; | |
padding: 2px; | |
margin-top: 4px; | |
`} | |
> | |
<div | |
css={css` | |
width: 12px; | |
height: 12px; | |
background: ${color}; | |
border-radius: 1px; | |
`} | |
/> | |
</span> | |
); | |
} | |
function MenuHeader({ title, active, error }) { | |
return ( | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
height: 17px; | |
line-height: 17px; | |
font-size: 14px; | |
color: ${error | |
? colors.red | |
: active | |
? colors.blue | |
: "rgba(0,0,0,0.54)"}; | |
`} | |
> | |
{title} | |
</div> | |
); | |
} | |
function MenuText({ children, action, customStyle }) { | |
return ( | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
height: 33px; | |
line-height: 33px; | |
position: relative; | |
border-bottom: ${action ? "1px solid #e2e2e2" : "0px"}; | |
font-size: 16px; | |
font-weight: 300; | |
cursor: pointer; | |
`} | |
style={customStyle} | |
onClick={action} | |
> | |
{children} | |
</div> | |
); | |
} | |
function MenuInput({ | |
name, | |
type, | |
min, | |
placeholder, | |
value, | |
onSelect, | |
onChange, | |
onBlur, | |
items, | |
error, | |
disabled, | |
format12h, | |
}) { | |
const [menu, setMenu] = useState(false); | |
const [focus, setFocus] = useState(false); | |
const ref = useRef(); | |
let menuItems; | |
if (items) { | |
menuItems = items.map((item) => { | |
return ( | |
<li | |
css={css` | |
cursor: pointer; | |
:hover { | |
background: rgba(0, 0, 0, 0.05); | |
} | |
`} | |
key={item} | |
onClick={() => { | |
onSelect(item); | |
setMenu(false); | |
}} | |
> | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
height: 26px; | |
line-height: 26px; | |
border-radius: 3px; | |
cursor: pointer; | |
font-size: 14px; | |
color: rgba(0, 0, 0, 0.54); | |
text-align: center; | |
box-sizing: border-box; | |
font-weight: 300; | |
:hover { | |
background: rgba(0, 0, 0, 0.05); | |
font-weight: 500; | |
} | |
`} | |
> | |
{item} | |
</div> | |
</li> | |
); | |
}); | |
} | |
const fullFocus = focus || menu; | |
return ( | |
<div | |
css={css` | |
position: relative; | |
`} | |
> | |
<MenuHeader title={name} active={fullFocus} error={error} /> | |
<input | |
css={css` | |
float: left; | |
width: 100%; | |
height: 33px; | |
line-height: 33px; | |
border: 0px; | |
border-bottom: 1px solid; | |
border-color: ${error | |
? colors.red | |
: fullFocus | |
? colors.blue | |
: "#e2e2e2"}; | |
font-size: 16px; | |
font-weight: 300; | |
font-family: "Museo Sans"; | |
:focus { | |
outline: 0; | |
} | |
`} | |
disabled={disabled} | |
type={type} | |
min={min} | |
placeholder={placeholder} | |
value={value} | |
onChange={(e) => onChange(e.target.value)} | |
onFocus={() => { | |
setFocus(true); | |
if (items) { | |
setMenu(true); | |
} | |
}} | |
onKeyDown={(e) => { | |
if (e.keyCode === 9) { | |
setMenu(false); | |
} | |
}} | |
onBlur={() => { | |
setFocus(false); | |
onBlur(); | |
}} | |
onMouseOut={() => { | |
onBlur(); | |
}} | |
/> | |
{menu && items && ( | |
<OutsideAlerter action={() => setMenu(false)}> | |
<div | |
css={css` | |
position: absolute; | |
top: 54px; | |
left: 1px; | |
width: ${format12h ? "96px" : "78px"}; | |
max-height: 214px; | |
background: white; | |
border-radius: 5px; | |
overflow-y: scroll; | |
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25); | |
z-index: 1; | |
::-webkit-scrollbar { | |
display: none; | |
} | |
`} | |
ref={ref} | |
> | |
<ul | |
css={css` | |
list-style: none; | |
margin: 0px; | |
padding: 0px; | |
padding-left: 6px; | |
padding-right: 6px; | |
padding-top: 9px; | |
padding-bottom: 6px; | |
box-sizing: border-box; | |
float: left; | |
width: 100%; | |
`} | |
> | |
{menuItems} | |
</ul> | |
</div> | |
</OutsideAlerter> | |
)} | |
</div> | |
); | |
} | |
function Selector({ items, selectedId, setId }) { | |
const [menu, setMenu] = useState(false); | |
const menuItems = items.map((item) => { | |
return ( | |
<li key={item.id}> | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
height: 26px; | |
line-height: 26px; | |
border-radius: 3px; | |
cursor: pointer; | |
font-weight: 300; | |
:hover { | |
font-weight: 500; | |
background: rgba(0, 0, 0, 0.05); | |
} | |
`} | |
onClick={() => { | |
setId(item.id); | |
setMenu(false); | |
}} | |
> | |
<div | |
css={css` | |
float: left; | |
width: calc(100% - 24px); | |
font-size: 14px; | |
color: rgba(0, 0, 0, 0.54); | |
padding-left: 6px; | |
box-sizing: border-box; | |
`} | |
> | |
{item.name} | |
</div> | |
<div | |
css={css` | |
float: right; | |
width: 24px; | |
`} | |
> | |
<Color color={item.color} /> | |
</div> | |
</div> | |
</li> | |
); | |
}); | |
const item = items.filter((i) => i.id === selectedId)[0]; | |
let color; | |
let name; | |
if (item) { | |
color = item.color; | |
name = item.name; | |
} | |
return ( | |
<> | |
<MenuHeader title="Activity type" active={menu} /> | |
<MenuText | |
customStyle={{ borderColor: menu && colors.blue }} | |
action={() => setMenu(true)} | |
> | |
{name ? ( | |
<div> | |
<div | |
css={css` | |
float: left; | |
width: 24px; | |
padding-top: 4px; | |
`} | |
> | |
<Color color={color} /> | |
</div> | |
<div | |
css={css` | |
float: left; | |
width: calc(100% - 50px); | |
`} | |
> | |
{name} | |
</div> | |
<div | |
css={css` | |
float: right; | |
width: 26px; | |
`} | |
> | |
<span className="bi_interface-bottom" /> | |
</div> | |
</div> | |
) : ( | |
<div | |
css={css` | |
color: rgba(0, 0, 0, 0.38); | |
`} | |
> | |
Select Activity Type | |
</div> | |
)} | |
</MenuText> | |
{menu && ( | |
<OutsideAlerter action={() => setMenu(false)}> | |
<div | |
css={css` | |
position: absolute; | |
top: 119px; | |
width: 400px; | |
max-height: 226px; | |
background: white; | |
border-radius: 5px; | |
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25); | |
z-index: 3; | |
overflow-y: scroll; | |
::-webkit-scrollbar { | |
display: none; | |
} | |
`} | |
> | |
{menuItems.length === 0 ? ( | |
<EmptyState | |
icon="icon-activities_outline" | |
title="No activity types" | |
message={ | |
<div> | |
<span> | |
This board doesn't have any active or scheduled activity | |
types. | |
</span> | |
</div> | |
} | |
size="small" | |
customStyle={{ | |
border: 0, | |
paddingTop: 24, | |
paddingBottom: 24, | |
paddingLeft: 12, | |
paddingRight: 12, | |
}} | |
iconStyle={{ fontSize: 23 }} | |
/> | |
) : ( | |
<ul | |
css={css` | |
list-style: none; | |
margin: 0px; | |
padding: 0px; | |
padding-left: 6px; | |
padding-right: 6px; | |
padding-top: 9px; | |
padding-bottom: 6px; | |
box-sizing: border-box; | |
float: left; | |
width: 100%; | |
`} | |
> | |
{menuItems} | |
</ul> | |
)} | |
</div> | |
</OutsideAlerter> | |
)} | |
</> | |
); | |
} | |
function DateSelector({ date, dotDates, selectDates, requiredTotal }) { | |
const [calendar, setCalendar] = useState(false); | |
return ( | |
<> | |
<MenuHeader title="Date" active={calendar} /> | |
<MenuText | |
action={() => setCalendar(true)} | |
customStyle={{ borderColor: calendar && colors.blue }} | |
> | |
{Moment(date).format("dddd, D MMMM")} | |
</MenuText> | |
<div | |
css={css` | |
float: left; | |
height: 20px; | |
line-height: 20px; | |
font-size: 14px; | |
font-weight: 300; | |
color: rgba(0, 0, 0, 0.54); | |
`} | |
> | |
Required: {round(requiredTotal / 60, 2)} hours | |
</div> | |
{calendar && ( | |
<OutsideAlerter action={() => setCalendar(false)}> | |
<div | |
css={css` | |
position: absolute; | |
width: 342px; | |
height: 302px; | |
background: white; | |
border-radius: 5px; | |
padding: 8px; | |
text-align: center; | |
z-index: 3; | |
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.25); | |
box-sizing: border-box; | |
top: 196px; | |
`} | |
> | |
<Calendar | |
initialDate={Moment(date)} | |
specificDaysSelected={[date]} | |
setSpecificDay={(d) => { | |
selectDates(d); | |
setCalendar(false); | |
}} | |
dots={dotDates} | |
/> | |
</div> | |
</OutsideAlerter> | |
)} | |
</> | |
); | |
} | |
function Info({ title, text }) { | |
return ( | |
<> | |
<MenuHeader title={title} active={false} /> | |
<MenuText customStyle={{ cursor: "default" }}>{text}</MenuText> | |
</> | |
); | |
} | |
const TIME_SLOTS = getTimeSlots(); | |
const END_TIME_SLOTS = [...TIME_SLOTS.filter((t) => t !== "00:00"), "00:00+1"]; | |
const TIME_SLOTS_12H = getTimeSlots(true); | |
function RequirementCreator({ | |
id, | |
s, | |
e, | |
a, | |
errorMessage, | |
manageItems, | |
items, | |
disabled, | |
format12h, | |
}) { | |
const [startTime, setStartTime] = useState( | |
format12h && s ? format24as12(s) : s | |
); | |
const [endTime, setEndTime] = useState(format12h && e ? format24as12(e) : e); | |
const [amount, setAmount] = useState(a); | |
useEffect(() => { | |
const start = format12h && s ? format24as12(s) : s; | |
if (start !== startTime) { | |
setStartTime(start); | |
} | |
const end = format12h && e ? format24as12(e) : e; | |
if (end !== endTime) { | |
setEndTime(end); | |
} | |
if (a !== amount) { | |
setAmount(a); | |
} | |
}, [s, e, a]); | |
function handleSelect(value, type) { | |
if (type === "startTime") { | |
setStartTime(value); | |
handleBlur(value); | |
} | |
if (type === "endTime") { | |
setEndTime(value); | |
handleBlur(null, value); | |
} | |
} | |
function handleBlur(altStartTime, altEndTime) { | |
let start = altStartTime ? altStartTime : startTime; | |
let end = altEndTime ? altEndTime : endTime; | |
if (format12h) { | |
start = format12as24(start); | |
end = format12as24(end); | |
} | |
// check if valid time | |
const validStart = timeValidator(start); | |
const validEnd = timeValidator(end); | |
if (validStart.error) { | |
setStartTime(format12h && s ? format24as12(s) : s); | |
return; | |
} else { | |
if (validStart !== startTime) { | |
setStartTime(format12h ? format24as12(validStart) : validStart); | |
} | |
start = validStart; | |
} | |
if (validEnd.error) { | |
setEndTime(format12h && e ? format24as12(e) : e); | |
return; | |
} else { | |
if (validEnd !== endTime) { | |
setEndTime(format12h ? format24as12(validEnd) : validEnd); | |
} | |
end = validEnd; | |
} | |
let sameEnd = e === end; | |
if (end === "00:00+1" && e === "00:00") { | |
sameEnd = true; | |
} | |
const isSame = s === start && sameEnd && a === amount; | |
if (start && end && amount > 0 && !isSame) { | |
let st = Moment(start, "HH:mm"); | |
let et = Moment(end, "HH:mm"); | |
let overlap; | |
items | |
.filter((i) => i.id !== id) | |
.forEach((item) => { | |
const itemSt = Moment(item.startTime, "HH:mm").toISOString(); | |
const itemEt = Moment(item.endTime, "HH:mm").toISOString(); | |
if ( | |
st.isBetween(itemSt, itemEt, "", "()") || | |
et.isBetween(itemSt, itemEt, "", "()") || | |
(st.isSameOrBefore(itemSt) && et.isAfter(itemSt)) | |
) { | |
overlap = true; | |
} | |
}); | |
let error; | |
if (overlap) { | |
error = "Time intervals can't overlap each other"; | |
} | |
if (st.isSame(et)) { | |
error = "End time can't be equal to start time"; | |
} | |
if (st.isAfter(et)) { | |
error = "End time can't be before start time"; | |
} | |
manageItems({ | |
id, | |
startTime: start, | |
endTime: end, | |
fteAmount: amount, | |
error, | |
}); | |
} | |
} | |
function removeItem() { | |
manageItems({ | |
id, | |
startTime, | |
endTime, | |
fteAmount: amount, | |
removed: true, | |
}); | |
} | |
const startTimesToFilter = []; | |
const endTimesToFilter = []; | |
// if there is a startTime | |
// everything same or before startTime can be filtered | |
// if there is no startTime | |
// anything can be | |
let cutOffTime; | |
let endCutOffTime; | |
items.forEach((item) => { | |
if (item.id !== id && item.startTime && item.endTime) { | |
let time = Moment(item.startTime, "HH:mm").toISOString(); | |
let endLoopTime = Moment(item.endTime, "HH:mm").toISOString(); | |
if (item.endTime === "00:00" || item.endTime === "00:00+1") { | |
endLoopTime = Moment(time) | |
.endOf("day") | |
.toISOString(); | |
} | |
while (Moment(time).isBefore(endLoopTime)) { | |
const st = Moment(time).format("HH:mm"); | |
startTimesToFilter.push(st); | |
const next = getNextHalforWhole(st, format12h); | |
const times = next.split(":"); | |
time = Moment(time) | |
.hours(times[0]) | |
.minutes(times[1]); | |
if (next === "00:00") { | |
time = time.add(1, "day"); | |
} | |
endTimesToFilter.push(Moment(time).format("HH:mm")); | |
} | |
if (startTime) { | |
// if item is after, all endtimes after that time should be removed | |
const times = format12h | |
? format12as24(startTime).split(":") | |
: startTime.split(":"); | |
let selectedStartTime = Moment() | |
.hours(times[0]) | |
.minutes(times[1]); | |
if (Moment(item.startTime, "HH:mm").isAfter(selectedStartTime)) { | |
// remove any items after item.startTime | |
if (cutOffTime) { | |
if (Moment(item.startTime, "HH:mm").isBefore(cutOffTime)) { | |
cutOffTime = Moment(item.startTime, "HH:mm").toISOString(); | |
} | |
} else { | |
cutOffTime = Moment(item.startTime, "HH:mm").toISOString(); | |
} | |
} | |
} | |
if (endTime) { | |
// if item is before the endtime, all items before that end time should be removed | |
const times = format12h | |
? format12as24(endTime).split(":") | |
: endTime.split(":"); | |
let selectedEndTime = Moment() | |
.hours(times[0]) | |
.minutes(times[1]); | |
if (endTime === "00:00+1") { | |
selectedEndTime = Moment().endOf("day"); | |
} | |
if (Moment(item.endTime, "HH:mm").isBefore(selectedEndTime)) { | |
// remove any items after item.startTime | |
if (endCutOffTime) { | |
if (Moment(item.endTime, "HH:mm").isAfter(endCutOffTime)) { | |
endCutOffTime = Moment(item.endTime, "HH:mm").toISOString(); | |
} | |
} else { | |
endCutOffTime = Moment(item.endTime, "HH:mm").toISOString(); | |
} | |
} | |
} | |
} | |
}); | |
let startTimesSlots = TIME_SLOTS.filter((time, index) => { | |
let filterOut; | |
if (endTime) { | |
// if (endTime !== "00:00+1" && endTime !== "12:00am") { | |
if ( | |
Moment(time, "HH:mm").isSameOrAfter( | |
Moment(endTime, format12h ? "h:mma" : "HH:mm") | |
) | |
) { | |
filterOut = true; | |
} | |
const times = time.split(":"); | |
let compareTime = Moment(endCutOffTime) | |
.hours(times[0]) | |
.minutes(times[1]); | |
if (times[1] === "00+1") { | |
compareTime = Moment(endCutOffTime).endOf("day"); | |
} | |
if (endCutOffTime && Moment(compareTime).isBefore(endCutOffTime)) { | |
filterOut = true; | |
} | |
// } | |
// else { | |
// filterOut = index === 0 ? true : false; | |
// } | |
} | |
return !startTimesToFilter.includes(time) && !filterOut; | |
}); | |
let endTimesSlots = END_TIME_SLOTS.filter((time) => { | |
let filterOut; | |
if (startTime) { | |
let formattedTime = Moment(time, "HH:mm"); | |
if (time === "00:00+1") { | |
formattedTime = Moment().endOf("day"); | |
} | |
if ( | |
formattedTime.isSameOrBefore( | |
Moment(startTime, format12h ? "h:mma" : "HH:mm") | |
) | |
) { | |
filterOut = true; | |
} | |
const times = time.split(":"); | |
let compareTime = Moment(cutOffTime) | |
.hours(times[0]) | |
.minutes(times[1]); | |
if (times[1] === "00+1") { | |
compareTime = Moment(cutOffTime).endOf("day"); | |
} | |
if (cutOffTime && Moment(compareTime).isAfter(cutOffTime)) { | |
filterOut = true; | |
} | |
} | |
return !endTimesToFilter.includes(time) && !filterOut; | |
}); | |
if (format12h) { | |
startTimesSlots = startTimesSlots.map((t) => format24as12(t)); | |
endTimesSlots = endTimesSlots.map((t) => format24as12(t)); | |
} | |
return ( | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
margin-bottom: 20px; | |
`} | |
> | |
<div | |
css={css` | |
float: left; | |
width: 88px; | |
margin-right: 16px; | |
`} | |
> | |
<MenuInput | |
name="Start time" | |
placeholder="From" | |
value={startTime} | |
onSelect={(value) => handleSelect(value, "startTime")} | |
onChange={setStartTime} | |
items={startTimesSlots} | |
onBlur={handleBlur} | |
error={errorMessage} | |
disabled={disabled} | |
format12h={format12h} | |
/> | |
</div> | |
<div | |
css={css` | |
float: left; | |
width: 88px; | |
margin-right: 16px; | |
`} | |
> | |
<MenuInput | |
name="End time" | |
placeholder="To" | |
value={endTime} | |
onSelect={(value) => handleSelect(value, "endTime")} | |
onChange={setEndTime} | |
items={endTimesSlots} | |
onBlur={handleBlur} | |
error={errorMessage} | |
disabled={disabled} | |
format12h={format12h} | |
/> | |
</div> | |
<div | |
css={css` | |
float: left; | |
width: 92px; | |
`} | |
> | |
<MenuInput | |
name="People" | |
type="number" | |
placeholder="# required" | |
value={amount} | |
onChange={setAmount} | |
onBlur={handleBlur} | |
min={0} | |
disabled={disabled} | |
/> | |
</div> | |
{startTime && endTime && amount && ( | |
<div | |
css={css` | |
float: right; | |
width: 24px; | |
color: rgba(0, 0, 0, 0.38); | |
font-size: 18.75px; | |
padding-top: 32px; | |
:hover { | |
color: ${colors.red}; | |
} | |
`} | |
onClick={removeItem} | |
> | |
<div className="tooltip"> | |
<span className="bi_interface-circle-cross" /> | |
<span | |
className="tooltiptext" | |
css={css` | |
left: auto !important; | |
right: 3px !important; | |
top: 28px !important; | |
::after { | |
left: 52px; | |
} | |
`} | |
> | |
Remove | |
</span> | |
</div> | |
</div> | |
)} | |
{errorMessage && ( | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
color: ${colors.red}; | |
font-size: 14px; | |
height: 20px; | |
line-height: 20px; | |
`} | |
> | |
{errorMessage} | |
</div> | |
)} | |
</div> | |
); | |
} | |
export default function RequiredModalContainer({ | |
currentDate, | |
activityTypes, | |
typeSelected, | |
handleClose, | |
refetchActivitiesQuery, | |
workloadPreferences, | |
format12h, | |
fullScreen, | |
}) { | |
const [dateSelected, setDateSelected] = useState(currentDate); | |
const [datesSelected, setDatesSelected] = useState([currentDate]); | |
const [updateList, setUpdateList] = useState([]); | |
const [loading, setLoading] = useState(false); | |
const [type, setType] = useState(typeSelected ? typeSelected : ""); | |
const [confirmUpdate, setConfirmUpdate] = useState(false); | |
// probably remove | |
const [types, setTypes] = useState( | |
activityTypes.map((t) => { | |
return { id: t.id, d: [currentDate], r: [] }; | |
}) | |
); | |
const [saveActivityRequireds, { data }] = useMutation( | |
SAVE_ACTIVITY_REQUIREDS, | |
{ | |
onCompleted: () => { | |
refetchActivitiesQuery().then((res) => { | |
setLoading(false); | |
setUpdateList([]); | |
setDatesSelected([dateSelected]); | |
setConfirmUpdate(false); | |
}); | |
}, | |
} | |
); | |
useEffect(() => { | |
setDatesSelected([dateSelected]); | |
}, [dateSelected]); | |
const selectedItem = activityTypes.filter((i) => i.id === type)[0]; | |
async function saveRequireds(callback, activityRequireds) { | |
setLoading(true); | |
const datesWithActions = datesSelected.map((date) => { | |
const d = Moment(date).format("DD-MM-YYYY"); | |
let actions = []; | |
if (d !== Moment(dateSelected).format("DD-MM-YYYY")) { | |
if (activityRequireds.length > 0) { | |
actions = activityRequireds.map((u) => { | |
let s = u.startTime; | |
let e = u.endTime; | |
return { | |
s, | |
e, | |
d, | |
a: u.fteAmount, | |
}; | |
}); | |
} else { | |
actions = [{ d }]; | |
} | |
} else { | |
actions = updateList.map((u) => { | |
let s = u.startTime; | |
let e = u.endTime; | |
if (u.removed) { | |
// delete | |
if (u.id.includes("-")) { | |
return null; | |
} else { | |
return { id: u.id, d, deleted: true }; | |
} | |
} else if (u.id.includes("-")) { | |
// create | |
return { | |
s, | |
e, | |
d, | |
a: u.fteAmount, | |
}; | |
} else { | |
// update | |
return { | |
id: u.id, | |
s, | |
e, | |
d, | |
a: u.fteAmount, | |
}; | |
} | |
}); | |
} | |
return actions.filter((a) => a); | |
}); | |
const batch = { | |
t: type, | |
d: datesWithActions, | |
ds: Moment(dateSelected).format("DD-MM-YYYY"), | |
}; | |
console.log({ batch }); | |
const databall = JSON.stringify(batch); | |
const compressed = compressToEncodedURIComponent(databall); | |
await saveActivityRequireds({ | |
variables: { | |
date: dateSelected, | |
batch: compressed, | |
}, | |
}); | |
if (callback) { | |
callback(); | |
} | |
} | |
return ( | |
<RequiredModal | |
activityTypes={activityTypes} | |
type={type} | |
setType={setType} | |
date={currentDate} | |
dateSelected={dateSelected} | |
setDateSelected={setDateSelected} | |
datesSelected={datesSelected} | |
setDatesSelected={setDatesSelected} | |
saveRequireds={saveRequireds} | |
types={types} | |
setTypes={setTypes} | |
buttonLoading={loading} | |
handleClose={handleClose} | |
updateList={updateList} | |
setUpdateList={setUpdateList} | |
workloadPreferences={workloadPreferences} | |
confirmUpdate={confirmUpdate} | |
setConfirmUpdate={setConfirmUpdate} | |
format12h={format12h} | |
fullScreen={fullScreen} | |
/> | |
); | |
} | |
export const REQUIREDS_QUERY = gql` | |
query activityRequireds( | |
$activityTypeId: ID! | |
$startDate: DateTime! | |
$endDate: DateTime! | |
) { | |
activityRequireds( | |
activityTypeId: $activityTypeId | |
startDate: $startDate | |
endDate: $endDate | |
) { | |
id | |
startTime | |
endTime | |
date | |
fteAmount | |
activityType { | |
id | |
} | |
} | |
} | |
`; | |
function RequiredModal({ | |
type, | |
setType, | |
dateSelected, | |
setDateSelected, | |
datesSelected, | |
setDatesSelected, | |
activityTypes, | |
types, | |
setTypes, | |
buttonLoading, | |
saveRequireds, | |
handleClose, | |
updateList, | |
setUpdateList, | |
workloadPreferences, | |
confirmUpdate, | |
setConfirmUpdate, | |
format12h, | |
fullScreen, | |
}) { | |
const [modal, setModal] = useState(false); | |
const [chooseUpdate, setChooseUpdate] = useState(false); | |
const { loading, error, data, refetch } = useQuery(REQUIREDS_QUERY, { | |
variables: { | |
activityTypeId: type, | |
startDate: Moment(dateSelected) | |
.startOf("month") | |
.toISOString(), | |
endDate: Moment(dateSelected) | |
.endOf("month") | |
.toISOString(), | |
}, | |
}); | |
function handleSelectType(newType) { | |
if (updateList.length > 0) { | |
// discard modal | |
setModal({ | |
action: () => { | |
setType(newType); | |
setChooseUpdate(false); | |
}, | |
}); | |
} else { | |
setType(newType); | |
} | |
} | |
function handleSelectDate(newDate) { | |
if (updateList.length > 0) { | |
// discard modal | |
setModal({ | |
action: () => { | |
setDateSelected(newDate); | |
setChooseUpdate(false); | |
}, | |
}); | |
} else { | |
setDateSelected(newDate); | |
} | |
} | |
let prevRef = useRef({}); | |
useEffect(() => { | |
if (!buttonLoading && prevRef.current === true) { | |
refetch().then((res) => { | |
console.log(res); | |
}); | |
} | |
prevRef.current = buttonLoading; | |
}, [buttonLoading]); | |
const index = types.findIndex((t) => t.id === type); | |
function selectDates(dates) { | |
const newTypes = produce(types, (draftState) => { | |
draftState[index].d = dates; | |
}); | |
setTypes(newTypes); | |
} | |
function manageRows(item) { | |
let newList; | |
const index = updateList.findIndex((u) => u.id === item.id); | |
if (index > -1) { | |
// replace | |
newList = produce(updateList, (draftState) => { | |
draftState[index] = item; | |
}); | |
} else { | |
// add | |
newList = [...updateList, item]; | |
} | |
setUpdateList(newList); | |
} | |
// the requireds from the database | |
let activityRequireds = []; | |
if (!loading) { | |
activityRequireds = data.activityRequireds.filter((required) => { | |
return required.date === Moment(dateSelected).format("DD-MM-YYYY"); | |
}); | |
} | |
activityRequireds = getActivityRequireds({ | |
activityRequireds, | |
updateList, | |
activityType: types[index], | |
}); | |
const sortedRows = type | |
? [...activityRequireds] | |
.sort((x, y) => { | |
if ( | |
Moment(x.startTime, "HH:mm").toISOString() > | |
Moment(y.startTime, "HH:mm").toISOString() | |
) { | |
return 1; | |
} else if ( | |
Moment(x.startTime, "HH:mm").toISOString() < | |
Moment(y.startTime, "HH:mm").toISOString() | |
) { | |
return -1; | |
} | |
return 0; | |
}) | |
.filter((r) => !r.removed) | |
: []; | |
let disabled; | |
types.forEach((type) => { | |
type.r.forEach((row) => { | |
if (row.error) { | |
disabled = true; | |
} | |
}); | |
}); | |
let requiredTotal = 0; | |
const requirementRows = [ | |
...sortedRows, | |
{ id: uuidv4(), startTime: "", endTime: "", fteAmount: "" }, | |
].map((row, index) => { | |
if (row.startTime && row.endTime) { | |
const duration = | |
getDuration(row.startTime, row.endTime).asMilliseconds() / 1000 / 60; | |
requiredTotal = requiredTotal + duration * row.fteAmount; | |
} | |
return ( | |
<RequirementCreator | |
key={row.id} | |
items={sortedRows} | |
manageItems={manageRows} | |
id={row.id} | |
s={row.startTime ? row.startTime : ""} | |
e={row.endTime ? row.endTime : ""} | |
a={row.fteAmount} | |
errorMessage={row.error} | |
disabled={!type} | |
format12h={format12h} | |
/> | |
); | |
}); | |
const prevCount = useRef(); | |
const requirementRowsRef = useRef(); | |
useEffect(() => { | |
if (prevCount.current !== requirementRows.length) { | |
const input = | |
requirementRowsRef.current?.lastChild?.firstChild?.firstChild | |
?.lastChild; | |
if (input) { | |
input.focus(); | |
} | |
} | |
prevCount.current = requirementRows.length; | |
}, [requirementRows]); | |
const dotDates = []; | |
if (!loading && data.activityRequireds) { | |
data.activityRequireds.forEach((r) => { | |
dotDates.push(r.date); | |
}); | |
} | |
return ( | |
<div aria-label="activity-details-required-modal"> | |
<ModalContainer customStyle={{ overflow: "hidden", zIndex: 13 }}> | |
<ModalHeader customStyle={{ zIndex: 7 }}> | |
{confirmUpdate && ( | |
<span | |
css={css` | |
position: absolute; | |
left: 17px; | |
color: rgba(0, 0, 0, 0.54); | |
cursor: pointer; | |
`} | |
className="bi_interface-arrow-left" | |
onClick={() => setConfirmUpdate(false)} | |
/> | |
)} | |
Required Staffing{" "} | |
<ToolTip | |
customStyle={{ left: -221, width: 240, top: 37 }} | |
customArrowStyle={{ left: 225 }} | |
text="This is where you can use your forecast to set the number of required people at exact time intervals over the day. The required staffing values can be adjusted either one day at a time or for several days at once." | |
/> | |
<span | |
css={css` | |
position: absolute; | |
right: 17px; | |
color: rgba(0, 0, 0, 0.54); | |
cursor: pointer; | |
`} | |
className="bi_interface-cross" | |
onClick={() => { | |
if (updateList.length > 0) { | |
setModal({ action: () => handleClose() }); | |
} else { | |
handleClose(); | |
} | |
}} | |
/> | |
</ModalHeader> | |
{confirmUpdate ? ( | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
padding: 24px 16px; | |
padding-top: 12px; | |
box-sizing: border-box; | |
`} | |
> | |
<Calendar | |
initialDate={Moment(dateSelected)} | |
specificDaysSelected={datesSelected} | |
setSpecificDay={(d) => { | |
const date = Moment(d).toISOString(); | |
if (datesSelected.includes(date)) { | |
setDatesSelected(datesSelected.filter((dt) => dt !== date)); | |
} else { | |
setDatesSelected([...datesSelected, date]); | |
} | |
}} | |
size="medium" | |
dots={dotDates} | |
/> | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
margin-top: 24px; | |
height: 20px; | |
line-height: 20px; | |
font-size: 14px; | |
`} | |
> | |
<div | |
css={css` | |
float: left; | |
font-weight: 300; | |
color: rgba(0, 0, 0, 0.54); | |
`} | |
> | |
{datesSelected.length} date | |
{datesSelected.length !== 1 ? "s" : ""} selected | |
</div> | |
<div | |
css={css` | |
float: right; | |
color: ${colors.blue}; | |
:hover { | |
text-decoration: underline; | |
cursor: pointer; | |
} | |
`} | |
onClick={() => setDatesSelected([])} | |
> | |
Clear | |
</div> | |
</div> | |
</div> | |
) : ( | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
padding: 24px 16px; | |
box-sizing: border-box; | |
`} | |
> | |
<Selector | |
items={activityTypes} | |
selectedId={type} | |
setId={handleSelectType} | |
/> | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
margin-top: 24px; | |
margin-bottom: 24px; | |
`} | |
> | |
<div | |
css={css` | |
float: left; | |
width: 192px; | |
margin-right: 16px; | |
`} | |
> | |
<DateSelector | |
date={dateSelected} | |
selectDates={(newDate) => handleSelectDate(newDate)} | |
dotDates={dotDates} | |
requiredTotal={requiredTotal} | |
/> | |
</div> | |
<div | |
css={css` | |
float: left; | |
width: 192px; | |
`} | |
> | |
<Info | |
title={ | |
<span> | |
Time zone{" "} | |
<ToolTip | |
customStyle={{ width: 240, left: -190, top: 22 }} | |
customArrowStyle={{ left: 194 }} | |
text="This time zone is used to control how the required staffing values are saved. The user display time zone determines how the graph is displayed. You can change this time zone through the workload power-up settings." | |
/> | |
</span> | |
} | |
text={workloadPreferences?.activityRequirementTimezone} | |
/> | |
</div> | |
</div> | |
<div | |
css={css` | |
float: left; | |
width: 100%; | |
height: 316px; | |
overflow-y: scroll; | |
overflow-x: hidden; | |
::-webkit-scrollbar { | |
display: none; | |
} | |
`} | |
ref={requirementRowsRef} | |
> | |
{loading ? <Loader /> : requirementRows} | |
</div> | |
</div> | |
)} | |
<div | |
css={css` | |
position: absolute; | |
width: 100%; | |
height: ${chooseUpdate ? "128px" : "70px"}; | |
bottom: 0; | |
border-top: 1px solid #e2e2e2; | |
padding-top: 12px; | |
padding-left: 16px; | |
padding-right: 16px; | |
box-sizing: border-box; | |
background: white; | |
border-bottom-left-radius: 5px; | |
border-bottom-right-radius: 5px; | |
`} | |
> | |
{chooseUpdate ? ( | |
<div> | |
<Button | |
name={`Update for ${Moment(datesSelected[0]).format( | |
"dddd DD MMM" | |
)}`} | |
theme="blue-border" | |
action={() => saveRequireds(() => setChooseUpdate(false))} | |
customStyle={{ width: "100%", marginBottom: 12 }} | |
loading={buttonLoading} | |
/> | |
<Button | |
customStyle={{ width: "100%" }} | |
name="Update for Multiple Days" | |
theme="blue-border" | |
action={() => { | |
setConfirmUpdate(true); | |
setChooseUpdate(false); | |
}} | |
/> | |
</div> | |
) : ( | |
<div> | |
<Button | |
name="Cancel" | |
theme="grey-border" | |
action={() => { | |
if (updateList.length > 0) { | |
setModal({ action: () => handleClose() }); | |
} else { | |
handleClose(); | |
} | |
}} | |
/>{" "} | |
<Button | |
customStyle={{ float: "right" }} | |
name="Update" | |
theme="blue-border" | |
action={ | |
confirmUpdate | |
? () => saveRequireds(null, activityRequireds) | |
: () => setChooseUpdate(true) | |
} | |
disabled={ | |
disabled || | |
updateList.length === 0 || | |
datesSelected.length === 0 || | |
updateList.filter((u) => u.error).length > 0 | |
} | |
loading={buttonLoading} | |
/> | |
</div> | |
)} | |
</div> | |
</ModalContainer> | |
{modal && ( | |
<Modal | |
title="Discard updates" | |
body={ | |
<p> | |
Are you sure you want to discard the updates made to the required | |
staffing for this activity? | |
<br /> | |
<strong> | |
<u>You can't undo this action.</u> | |
</strong> | |
</p> | |
} | |
color={colors.red} | |
buttonOneName="Cancel" | |
buttonTwoName="Discard" | |
buttonOneAction={() => setModal(false)} | |
buttonTwoAction={() => { | |
modal.action(); | |
setModal(false); | |
setUpdateList([]); | |
}} | |
buttonOneTheme={"grey-border"} | |
buttonTwoTheme={"red"} | |
customLayerStyle={{ | |
width: window.innerWidth, | |
height: window.innerHeight, | |
zIndex: 30, | |
top: fullScreen ? 0 : -84, | |
left: fullScreen | |
? 0 | |
: window.innerWidth < 1920 | |
? "-" + (window.innerWidth - 952) / 2 + "px" | |
: "-" + (window.innerWidth / 4 + 4) + "px", | |
}} | |
customStyle={{ zIndex: 31 }} | |
/> | |
)} | |
<div | |
css={css` | |
position: fixed; | |
width: 100%; | |
height: calc(100% + 80px); | |
background: rgba(0, 0, 0, 0.2); | |
left: 0; | |
top: 0; | |
z-index: 12; | |
`} | |
aria-label="activity-details-required-modal-close-layer" | |
onClick={() => { | |
if (updateList.length > 0) { | |
setModal({ action: () => handleClose() }); | |
} else { | |
handleClose(); | |
} | |
}} | |
/> | |
</div> | |
); | |
} | |
const SAVE_ACTIVITY_REQUIREDS = gql` | |
mutation saveActivityRequireds($date: DateTime!, $batch: Json!) { | |
saveActivityRequireds(date: $date, batch: $batch) { | |
id | |
startTime | |
endTime | |
date | |
fteAmount | |
activityType { | |
id | |
} | |
} | |
} | |
`; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment