use utc offset to group users

This commit is contained in:
Maxim 2022-09-08 15:42:12 +03:00
parent 23d706cc7f
commit c3d5bcc2d8
7 changed files with 86 additions and 117 deletions

View file

@ -73,6 +73,7 @@
border-radius: 8px;
text-align: center;
transition: opacity 200ms ease, left 200ms ease;
pointer-events: none;
}
.avatar-group_inactive {

View file

@ -10,11 +10,12 @@ import Text from 'components/Text/Text';
import { IsOncallIcon } from 'icons';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { useStore } from 'state/useStore';
import styles from './UsersTimezones.module.css';
interface UsersTimezonesProps {
users: User[];
userIds: Array<User['pk']>;
tz: Timezone;
onTzChange: (tz: Timezone) => void;
onCallNow: Array<Partial<User>>;
@ -27,11 +28,26 @@ const hoursToSplit = 3;
const jLimit = 24 / hoursToSplit;
const UsersTimezones: FC<UsersTimezonesProps> = (props) => {
const { users, tz, onTzChange, onCallNow } = props;
const { userIds, tz, onTzChange, onCallNow } = props;
const store = useStore();
const [count, setCount] = useState<number>(0);
const [currentMoment, setCurrentMoment] = useState<dayjs.Dayjs>(dayjs().tz(tz));
useEffect(() => {
userIds.forEach((userId) => {
if (!store.userStore.items[userId]) {
store.userStore.updateItem(userId);
}
});
}, [userIds]);
const users = useMemo(
() => userIds.map((userId) => store.userStore.items[userId]).filter(Boolean),
[userIds, store.userStore.items]
);
useEffect(() => {
setCurrentMoment(currentMoment.tz(tz).startOf('minute'));
}, [tz]);
@ -121,9 +137,10 @@ const UserAvatars = (props: UserAvatarsProps) => {
const userGroups = useMemo(() => {
return users
.reduce((memo, user) => {
let group = memo.find((group) => group.timezone === user.timezone);
const userUtcOffset = dayjs().tz(user.timezone).utcOffset();
let group = memo.find((group) => group.utcOffset === userUtcOffset);
if (!group) {
group = { timezone: user.timezone, users: [] };
group = { utcOffset: userUtcOffset, users: [] };
memo.push(group);
}
group.users.push(user);
@ -131,13 +148,10 @@ const UserAvatars = (props: UserAvatarsProps) => {
return memo;
}, [])
.sort((a, b) => {
const aOffset = dayjs().tz(a.timezone).utcOffset();
const bOffset = dayjs().tz(b.timezone).utcOffset();
if (aOffset > bOffset) {
if (a.utcOffset > b.utcOffset) {
return 1;
}
if (aOffset < bOffset) {
if (a.utcOffset < b.utcOffset) {
return -1;
}
@ -145,28 +159,22 @@ const UserAvatars = (props: UserAvatarsProps) => {
});
}, [users]);
const getAvatarClickHandler = useCallback((timezone: Timezone) => {
return () => {
onTzChange(timezone);
};
}, []);
const [activeTimezone, setActiveTimezone] = useState<Timezone | undefined>(undefined);
const [activeUtcOffset, setActiveUtcOffset] = useState<number | undefined>(undefined);
return (
<div className={cx('user-avatars')}>
{userGroups.map((group) => {
const userCurrentMoment = dayjs(currentMoment).tz(group.timezone);
const userCurrentMoment = dayjs(currentMoment).tz(group.users[0].timezone); // TODO try using group.utcOffset
const diff = userCurrentMoment.diff(userCurrentMoment.startOf('day'), 'minutes');
const xPos = (diff / (60 * 24)) * 100;
return (
<AvatarGroup
activeTimezone={activeTimezone}
timezone={group.timezone}
onSetActiveTimezone={setActiveTimezone}
onClick={getAvatarClickHandler(group.timezone)}
activeUtcOffset={activeUtcOffset}
utcOffset={group.utcOffset}
onSetActiveUtcOffset={setActiveUtcOffset}
onTzChange={onTzChange}
xPos={xPos}
users={group.users}
currentMoment={currentMoment}
@ -182,10 +190,10 @@ interface AvatarGroupProps {
users: User[];
xPos: number;
currentMoment: dayjs.Dayjs;
onClick: () => void;
timezone: Timezone;
onSetActiveTimezone: (timezone: Timezone) => void;
activeTimezone: Timezone;
utcOffset: number;
onSetActiveUtcOffset: (utcOffset: number | undefined) => void;
activeUtcOffset: number;
onTzChange: (timezone: Timezone) => void;
onCallNow: Array<Partial<User>>;
}
@ -198,14 +206,14 @@ const AvatarGroup = (props: AvatarGroupProps) => {
users: propsUsers,
currentMoment,
xPos,
onClick,
timezone,
onSetActiveTimezone,
activeTimezone,
onTzChange,
utcOffset,
onSetActiveUtcOffset,
activeUtcOffset,
onCallNow,
} = props;
const active = activeTimezone && activeTimezone === timezone;
const active = !isNaN(activeUtcOffset) && activeUtcOffset === utcOffset;
const translateLeft = -AVATAR_WIDTH / 2;
@ -225,15 +233,22 @@ const AvatarGroup = (props: AvatarGroupProps) => {
});
}, [propsUsers]);
const getAvatarClickHandler = useCallback((timezone: Timezone) => {
return () => {
onTzChange(timezone);
};
}, []);
const width = active ? users.length * AVATAR_WIDTH + (users.length - 1) * AVATAR_GAP : AVATAR_WIDTH;
return (
<div
className={cx('avatar-group', { [`avatar-group_inactive`]: activeTimezone && activeTimezone !== timezone })}
style={{ width: `${width + AVATAR_GAP}px`, left: `${xPos}%`, transform: `translate(${translateLeft}px, 0)` }}
onClick={onClick}
onMouseEnter={() => onSetActiveTimezone(timezone)}
onMouseLeave={() => onSetActiveTimezone(undefined)}
className={cx('avatar-group', {
[`avatar-group_inactive`]: !isNaN(activeUtcOffset) && activeUtcOffset !== utcOffset,
})}
style={{ width: `${width}px`, left: `${xPos}%`, transform: `translate(${translateLeft}px, 0)` }}
onMouseEnter={() => onSetActiveUtcOffset(utcOffset)}
onMouseLeave={() => onSetActiveUtcOffset(undefined)}
>
{users.map((user, index, array) => {
const isOncall = onCallNow.some((onCallUser) => user.pk === onCallUser.pk);
@ -254,6 +269,7 @@ const AvatarGroup = (props: AvatarGroupProps) => {
zIndex: array.length - index - 1,
/* opacity: userHour >= 9 && userHour < 18 ? 1 : 0.5,*/
}}
onClick={getAvatarClickHandler(user.timezone)}
>
<Avatar src={user.avatar} size="large" />
{isOncall && <IsOncallIcon className={cx('is-oncall-icon')} />}

View file

@ -8,6 +8,7 @@ import BaseStore from 'models/base_store';
import { EscalationChain } from 'models/escalation_chain/escalation_chain.types';
import { SlackChannel } from 'models/slack_channel/slack_channel.types';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { makeRequest } from 'network';
import { RootStore } from 'state';
import { SelectOption } from 'state/types';
@ -26,34 +27,6 @@ const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
let I = 0;
function getUsers() {
const rnd = Math.random();
/*
if (rnd > 0.66) {
return [];
}
*/
const users = [
'U5WE86241LNEA',
'U9XM1G7KTE3KW',
'UYKS64M6C59XM',
'UFFIRDUFXA6W3',
'UPRMSTP9LCADE',
'UR6TVJWZYV19M',
'UHRMQQ7KETPCS',
];
/* if (rnd > 0.33) {
return [users[Math.floor(Math.random() * users.length)], users[Math.floor(Math.random() * users.length)]];
}*/
return ['UPRMSTP9LCADE', 'UHRMQQ7KETPCS'];
return [users[Math.floor(Math.random() * users.length)]];
}
export class ScheduleStore extends BaseStore {
@observable
searchResult: { [key: string]: Array<Schedule['id']> } = {};
@ -67,6 +40,9 @@ export class ScheduleStore extends BaseStore {
@observable.shallow
relatedEscalationChains: { [id: string]: EscalationChain[] } = {};
@observable.shallow
relatedUsers: { [id: string]: { [key: string]: Event } } = {};
@observable.shallow
rotations: {
[id: string]: {
@ -276,55 +252,18 @@ export class ScheduleStore extends BaseStore {
return response;
};
async updateRotationMock(rotationId: Rotation['id'], fromString: string, currentTimezone: Timezone) {
if (this.rotations[rotationId]?.[fromString]) {
return;
}
const response = await new Promise((resolve, reject) => {
setTimeout(() => {
if (!fromString) {
fromString = dayjs().startOf('week').format('YYYY-MM-DDTHH:mm:ss.000Z');
}
let startMoment = dayjs(fromString);
const utcOffset = dayjs().tz(currentTimezone).utcOffset();
startMoment = startMoment.add(utcOffset, 'minutes');
//const startMoment = dayjs().utc().startOf('week');
const shifts = [];
for (let i = 0; i < 7; i++) {
const shiftDuration = (12 + Math.floor(Math.random() * 12)) * 60 * 60;
const gapDuration = 24 * 60 * 60 - shiftDuration;
shifts.push({
pk: I++,
start: startMoment.add(24 * i, 'hour'),
duration: shiftDuration,
users: getUsers(),
});
shifts.push({
pk: I++,
start: startMoment.add(24 * i, 'hour').add(shiftDuration, 'seconds'),
duration: gapDuration,
is_gap: true,
});
}
resolve({ id: rotationId, shifts });
}, 500);
updateRelatedUsers = async (id: Schedule['id']) => {
const { users } = await makeRequest(`/schedules/${id}/next_shifts_per_user`, {
method: 'GET',
});
this.rotations = {
...this.rotations,
[rotationId]: {
...this.rotations[rotationId],
[fromString]: response as Rotation,
},
this.relatedUsers = {
...this.relatedUsers,
[id]: users,
};
}
return users;
};
async updateOncallShifts(scheduleId: Schedule['id']) {
const { results } = await makeRequest(`/oncall_shifts/`, {

View file

@ -48,7 +48,7 @@ export const getTimezone = (user: User) => {
'Matvey Kukuy': 'Asia/Tel_Aviv',
};
return user.timezone || tzByName[user.username] || dayjs.tz.guess();
return user.timezone || tzByName[user.username] || 'UTC';
};
export const getUserNotificationsSummary = (user: User) => {

View file

@ -59,6 +59,16 @@ export class UserStore extends BaseStore {
[user.pk]: { ...user, timezone: getTimezone(user) },
};
// TODO comment
if (user.timezone) {
this.update(user.pk, { timezone: 'UTC' });
}
// TODO uncomment
/*if (!user.timezone) {
this.update(user.pk, { timezone: dayjs.tz.guess() });
}*/
this.currentUserPk = user.pk;
}
@ -68,7 +78,7 @@ export class UserStore extends BaseStore {
this.items = {
...this.items,
[user.pk]: user,
[user.pk]: { ...user, timezone: getTimezone(user) },
};
}

View file

@ -15,11 +15,11 @@ import ScheduleQuality from 'components/ScheduleQuality/ScheduleQuality';
import Text from 'components/Text/Text';
// import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
import UserTimezoneSelect from 'components/UserTimezoneSelect/UserTimezoneSelect';
import UsersTimezones from 'components/UsersTimezones/UsersTimezones';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import Rotations from 'containers/Rotations/Rotations';
import ScheduleFinal from 'containers/Rotations/ScheduleFinal';
import ScheduleOverrides from 'containers/Rotations/ScheduleOverrides';
import UsersTimezones from 'containers/UsersTimezones/UsersTimezones';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { AppFeature } from 'state/features';
@ -59,9 +59,9 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
const { store } = this.props;
const { startMoment } = this.state;
if (!store.hasFeature(AppFeature.WebSchedules)) {
/*if (!store.hasFeature(AppFeature.WebSchedules)) {
getLocationSrv().update({ query: { page: 'schedules' } });
}
}*/
store.userStore.updateItems();
@ -143,7 +143,9 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
<div className={cx('users-timezones')}>
<UsersTimezones
onCallNow={schedule?.on_call_now || []}
users={users || []}
userIds={
scheduleStore.relatedUsers[scheduleId] ? Object.keys(scheduleStore.relatedUsers[scheduleId]) : []
}
tz={currentTimezone}
onTzChange={this.handleTimezoneChange}
/>
@ -239,6 +241,7 @@ class SchedulePage extends React.Component<SchedulePageProps, SchedulePageState>
const { startMoment } = this.state;
store.scheduleStore.updateItem(scheduleId); // to refresh current oncall users
store.scheduleStore.updateRelatedUsers(scheduleId); // to refresh related users
return Promise.all([
store.scheduleStore.updateEvents(scheduleId, startMoment, 'rotation'),

View file

@ -57,9 +57,9 @@ class SchedulesPage extends React.Component<SchedulesPageProps, SchedulesPageSta
async componentDidMount() {
const { store } = this.props;
if (!store.hasFeature(AppFeature.WebSchedules)) {
/* if (!store.hasFeature(AppFeature.WebSchedules)) {
getLocationSrv().update({ query: { page: 'schedules' } });
}
} */
store.userStore.updateItems();
store.scheduleStore.updateItems();