Skip to content

Instantly share code, notes, and snippets.

@tamunoibi
Created July 8, 2021 09:44
Show Gist options
  • Save tamunoibi/8d8a668a78f8384f67e8478033834cff to your computer and use it in GitHub Desktop.
Save tamunoibi/8d8a668a78f8384f67e8478033834cff to your computer and use it in GitHub Desktop.
/** @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