use utc offset to group users
This commit is contained in:
parent
23d706cc7f
commit
c3d5bcc2d8
7 changed files with 86 additions and 117 deletions
|
|
@ -73,6 +73,7 @@
|
|||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: opacity 200ms ease, left 200ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.avatar-group_inactive {
|
||||
|
|
@ -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')} />}
|
||||
|
|
@ -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/`, {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue