add rotation preview

This commit is contained in:
Maxim 2022-08-18 18:13:54 +03:00
parent 622bdb383f
commit 4551481c0f
8 changed files with 157 additions and 115 deletions

View file

@ -33,7 +33,7 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color: propColor } = props;
const { users } = event;
const trackMouse = true;
const trackMouse = false;
const [mouseX, setMouseX] = useState<number>(0);

View file

@ -3,15 +3,12 @@ import React, { FC, useMemo, useState, useEffect, useRef, useCallback } from 're
import { HorizontalGroup, LoadingPlaceholder } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { observer } from 'mobx-react';
import { CSSTransitionGroup } from 'react-transition-group'; // ES6
import ScheduleSlot from 'components/ScheduleSlot/ScheduleSlot';
import Text from 'components/Text/Text';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Rotation as RotationType, Schedule, Event } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { useStore } from 'state/useStore';
import { usePrevious } from 'utils/hooks';
import styles from './Rotation.module.css';
@ -30,15 +27,13 @@ interface RotationProps {
onClick: () => void;
}
const Rotation: FC<RotationProps> = observer((props) => {
const Rotation: FC<RotationProps> = (props) => {
const { events, layerIndex, rotationIndex, startMoment, currentTimezone, color, onClick } = props;
const [animate, setAnimate] = useState<boolean>(true);
const [width, setWidth] = useState<number | undefined>();
const [transparent, setTransparent] = useState<boolean>(false);
const store = useStore();
const startMomentString = useMemo(() => getFromString(startMoment), [startMoment]);
const prevStartMomentString = usePrevious(startMomentString);
@ -92,7 +87,6 @@ const Rotation: FC<RotationProps> = observer((props) => {
<div
className={cx('slots', { slots__animate: animate, slots__transparent: transparent })}
style={{ transform: `translate(${x * 100}%, 0)` }}
ref={slots}
>
{events.map((event, index) => {
return (
@ -110,7 +104,7 @@ const Rotation: FC<RotationProps> = observer((props) => {
})}
</div>
) : (
<div className={cx('empty')} />
<Empty />
)
) : (
<HorizontalGroup align="center" justify="center">
@ -120,6 +114,10 @@ const Rotation: FC<RotationProps> = observer((props) => {
</div>
</div>
);
});
};
const Empty = () => {
return <div className={cx('empty')} />;
};
export default Rotation;

View file

@ -1,4 +1,4 @@
import React, { FC, useCallback, useEffect, useState } from 'react';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { dateTime, DateTime } from '@grafana/data';
import {
@ -22,21 +22,25 @@ import Text from 'components/Text/Text';
import UserGroups from 'components/UserGroups/UserGroups';
import WithConfirm from 'components/WithConfirm/WithConfirm';
import RemoteSelect from 'containers/RemoteSelect/RemoteSelect';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Rotation, Schedule, Shift } from 'models/schedule/schedule.types';
import { getTzOffsetString } from 'models/timezone/timezone.helpers';
import { Timezone } from 'models/timezone/timezone.types';
import { User } from 'models/user/user.types';
import { makeRequest } from 'network';
import { getDateTime, getUTCString } from 'pages/schedule/Schedule.helpers';
import { SelectOption } from 'state/types';
import { useStore } from 'state/useStore';
import { useDebouncedCallback } from 'utils/hooks';
import { RotationCreateData } from './RotationForm.types';
import styles from './RotationForm.module.css';
interface RotationFormProps {
layerIndex: number;
layerPriority: number;
onHide: () => void;
startMoment: dayjs.Dayjs;
currentTimezone: Timezone;
scheduleId: Schedule['id'];
shiftId: Shift['id'] | 'new';
@ -46,10 +50,10 @@ interface RotationFormProps {
const cx = cn.bind(styles);
const startOfDay = dayjs().startOf('day');
const startOfDay = dayjs().startOf('day').add(1, 'day');
const RotationForm: FC<RotationFormProps> = observer((props) => {
const { onHide, onCreate, currentTimezone, scheduleId, onUpdate, layerIndex, shiftId } = props;
const { onHide, onCreate, startMoment, currentTimezone, scheduleId, onUpdate, layerPriority, shiftId } = props;
const [repeatEveryValue, setRepeatEveryValue] = useState<number>(1);
const [repeatEveryPeriod, setRepeatEveryPeriod] = useState<number>(0);
@ -88,9 +92,8 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
}
}, [shiftId]);
const handleCreate = useCallback(() => {
const params = {
title: 'Rotation ' + Math.floor(Math.random() * 100),
const params = useMemo(
() => ({
rotation_start: getUTCString(rotationStart, currentTimezone),
until: endLess ? null : getUTCString(rotationEnd, currentTimezone),
shift_start: getUTCString(shiftStart, currentTimezone),
@ -99,9 +102,25 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
interval: repeatEveryValue,
frequency: repeatEveryPeriod,
by_day: repeatEveryPeriod === 1 ? selectedDays : null,
priority_level: shiftId === 'new' ? layerIndex + 1 : shift?.priority_level,
};
priority_level: shiftId === 'new' ? layerPriority : shift?.priority_level,
}),
[
rotationStart,
currentTimezone,
rotationEnd,
shiftStart,
shiftEnd,
userGroups,
repeatEveryValue,
repeatEveryPeriod,
selectedDays,
shiftId,
layerPriority,
shift,
]
);
const handleCreate = useCallback(() => {
if (shiftId === 'new') {
store.scheduleStore.createRotation(scheduleId, false, params).then(() => {
onHide();
@ -113,18 +132,13 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
onUpdate();
});
}
}, [
repeatEveryValue,
repeatEveryPeriod,
selectedDays,
shiftStart,
shiftEnd,
rotationStart,
endLess,
rotationEnd,
userGroups,
layerIndex,
]);
}, [shiftId, params]);
const handleChange = useDebouncedCallback(() => {
store.scheduleStore.updateRotationPreview(scheduleId, shiftId, getFromString(startMoment), false, params);
}, 1000);
useEffect(handleChange, [params]);
useEffect(() => {
if (shift) {
@ -173,7 +187,7 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
<HorizontalGroup justify="space-between">
<Text size="medium">
<HorizontalGroup spacing="sm">
<span>[L{shiftId === 'new' ? layerIndex + 1 : shift?.priority_level}]</span>
<span>[L{shiftId === 'new' ? layerPriority : shift?.priority_level}]</span>
{shiftId === 'new' ? 'New Rotation' : shift?.title}
</HorizontalGroup>
</Text>

View file

@ -4,6 +4,7 @@ import { ValuePicker, IconButton, Icon, HorizontalGroup, Button, LoadingPlacehol
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
@ -11,7 +12,7 @@ import Rotation from 'containers/Rotation/Rotation';
import RotationForm from 'containers/RotationForm/RotationForm';
import { RotationCreateData } from 'containers/RotationForm/RotationForm.types';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Event, Schedule, Shift } from 'models/schedule/schedule.types';
import { Event, Layer, Schedule, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { SelectOption, WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -33,12 +34,7 @@ interface RotationsProps extends WithStoreProps {
interface RotationsState {
shiftIdToShowRotationForm?: Shift['id'];
layerIndexToShowRotationForm?: number;
}
interface Layer {
priority: Shift['priority_level'];
shifts: Array<{ shiftId: Shift['id']; events: Event[] }>;
layerPriority?: Layer['priority'];
}
@observer
@ -49,7 +45,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
render() {
const { scheduleId, startMoment, currentTimezone, onCreate, onUpdate, store, onClick } = this.props;
const { shiftIdToShowRotationForm, layerIndexToShowRotationForm } = this.state;
const { shiftIdToShowRotationForm, layerPriority } = this.state;
const base = 7 * 24 * 60; // in minutes
const diff = dayjs().tz(currentTimezone).diff(startMoment, 'minutes');
@ -58,32 +54,12 @@ class Rotations extends Component<RotationsProps, RotationsState> {
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
const shifts = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)];
const storeLayers = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[];
const layers: Layer[] | undefined = shifts
? shifts
.reduce((memo, shift) => {
const storeShift = store.scheduleStore.shifts[shift.shiftId];
let layer = memo.find((level) => level.priority === storeShift.priority_level);
if (!layer) {
layer = { priority: storeShift.priority_level, shifts: [] };
memo.push(layer);
}
layer.shifts.push(shift);
return memo;
}, [])
.sort((a, b) => {
if (a.priority > b.priority) {
return 1;
}
if (a.priority < b.priority) {
return -1;
}
return 0;
})
: undefined;
let layers = storeLayers;
if (store.scheduleStore.rotationPreview) {
layers = [...layers, { priority: 2, shifts: [store.scheduleStore.rotationPreview] }];
}
const options = layers
? layers.map((layer) => ({
@ -92,7 +68,9 @@ class Rotations extends Component<RotationsProps, RotationsState> {
}))
: [];
options.push({ label: 'New Layer', value: layers?.length || 0 });
const nextPriority = layers && layers.length ? layers[layers.length - 1].priority + 1 : 1;
options.push({ label: 'New Layer', value: nextPriority });
return (
<>
@ -158,7 +136,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
<div className={cx('rotations')}>
<Rotation
onClick={() => {
this.handleAddLayer(layers ? layers.length : 0);
this.handleAddLayer(nextPriority);
}}
events={[]}
layerIndex={0}
@ -175,7 +153,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
<div
className={cx('add-rotations-layer')}
onClick={() => {
this.handleAddLayer(layers ? layers.length : 0);
this.handleAddLayer(nextPriority);
}}
>
Add rotations layer +
@ -186,11 +164,10 @@ class Rotations extends Component<RotationsProps, RotationsState> {
<RotationForm
shiftId={shiftIdToShowRotationForm}
scheduleId={scheduleId}
layerIndex={layerIndexToShowRotationForm}
layerPriority={layerPriority}
startMoment={startMoment}
currentTimezone={currentTimezone}
onHide={() => {
this.setState({ shiftIdToShowRotationForm: undefined });
}}
onHide={this.handleRotationFormHide}
onUpdate={onUpdate}
onCreate={onCreate}
/>
@ -199,18 +176,23 @@ class Rotations extends Component<RotationsProps, RotationsState> {
);
}
handleRotationFormHide = () => {
const { store } = this.props;
store.scheduleStore.rotationPreview = undefined;
this.setState({ shiftIdToShowRotationForm: undefined, layerPriority: undefined });
};
onRotationClick = (shiftId: Shift['id']) => {
this.setState({ shiftIdToShowRotationForm: shiftId });
};
updateEvents = () => {};
handleAddLayer = (layerIndex: number) => {
this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: layerIndex });
handleAddLayer = (layerPriority: number) => {
this.setState({ shiftIdToShowRotationForm: 'new', layerPriority });
};
handleAddRotation = (option: SelectOption) => {
this.setState({ shiftIdToShowRotationForm: 'new', layerIndexToShowRotationForm: option.value });
this.setState({ shiftIdToShowRotationForm: 'new', layerPriority: option.value });
};
}

View file

@ -69,23 +69,10 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
<div className={cx('rotations')}>
{shifts && shifts.length ? (
shifts.map(({ shiftId, events }, index) => (
<Rotation
key={index}
events={events}
startMoment={startMoment}
currentTimezone={currentTimezone}
/*layerIndex={0}
rotationIndex={0}*/
/>
<Rotation key={index} events={events} startMoment={startMoment} currentTimezone={currentTimezone} />
))
) : (
<Rotation
events={[]}
startMoment={startMoment}
currentTimezone={currentTimezone}
/*layerIndex={0}
rotationIndex={0}*/
/>
<Rotation events={[]} startMoment={startMoment} currentTimezone={currentTimezone} />
)}
</div>
</div>

View file

@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import { Event } from './schedule.types';
import { Event, Shift } from './schedule.types';
export const getFromString = (moment: dayjs.Dayjs) => {
return moment.format('YYYY-MM-DD');
@ -23,3 +23,24 @@ export const fillGaps = (events: Event[]) => {
return newEvents;
};
export const splitToShiftsAndFillGaps = (events: Event[]) => {
const shifts: Array<{ shiftId: Shift['id']; events: Event[] }> = [];
for (const [i, event] of events.entries()) {
if (event.shift?.pk) {
let shift = shifts.find((shift) => shift.shiftId === event.shift?.pk);
if (!shift) {
shift = { shiftId: event.shift.pk, events: [] };
shifts.push(shift);
}
shift.events.push(event);
}
}
shifts.forEach((shift) => {
shift.events = fillGaps(shift.events);
});
return shifts;
};

View file

@ -11,8 +11,8 @@ import { makeRequest } from 'network';
import { RootStore } from 'state';
import { SelectOption } from 'state/types';
import { fillGaps } from './schedule.helpers';
import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event } from './schedule.types';
import { fillGaps, splitToShiftsAndFillGaps } from './schedule.helpers';
import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer } from './schedule.types';
const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
@ -67,11 +67,17 @@ export class ScheduleStore extends BaseStore {
events: {
[scheduleId: string]: {
[type: string]: {
[startMoment: string]: Array<{ shiftId: string; events: Event[] }>;
[startMoment: string]: Array<{ shiftId: string; events: Event[] }> | Layer[];
};
};
} = {};
@observable.shallow
rotationPreview?: { shiftId: Shift['id']; events: Event[] };
@observable.shallow
finalPreview?: Array<{ shiftId: Shift['id']; events: Event[] }>;
@observable
scheduleToScheduleEvents: {
[id: string]: ScheduleEvent[];
@ -187,6 +193,27 @@ export class ScheduleStore extends BaseStore {
return response;
}
async updateRotationPreview(
scheduleId: Schedule['id'],
shiftId: Shift['id'] | 'new',
fromString: string,
isOverride: boolean,
params: Partial<Shift>
) {
const type = isOverride ? 3 : 2;
const typeString = isOverride ? 'override' : 'rotation';
const response = await makeRequest(`/oncall_shifts/preview/`, {
params: { date: fromString },
data: { type, schedule: scheduleId, ...params },
method: 'POST',
}).catch(this.onApiError);
this.rotationPreview = { shiftId: shiftId, events: fillGaps(response.rotation.filter((event) => !event.is_gap)) };
this.finalPreview = splitToShiftsAndFillGaps(response.final);
}
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
const response = await makeRequest(`/oncall_shifts/${shiftId}`, {
data: { ...params },
@ -297,21 +324,9 @@ export class ScheduleStore extends BaseStore {
method: 'GET',
});
const events = type !== 'final' ? fillGaps(response.events) : response.events;
const shifts: Array<{ shiftId: Shift['id']; events: Event[] }> = [];
for (const [i, event] of response.events.entries()) {
if (event.shift?.pk) {
let shift = shifts.find((shift) => shift.shiftId === event.shift?.pk);
if (!shift) {
shift = { shiftId: event.shift.pk, events: [] };
shifts.push(shift);
}
shift.events.push(event);
}
}
const shifts = splitToShiftsAndFillGaps(response.events);
// merge users on frontend side, we don't need it now
/*shifts.forEach((shift) => {
for (let i = 0; i < shift.events.length; i++) {
const iEvent = shift.events[i];
@ -327,11 +342,31 @@ export class ScheduleStore extends BaseStore {
}
});*/
shifts.forEach((shift) => {
shift.events = fillGaps(shift.events);
});
const layers: Layer[] | undefined =
type === 'rotation'
? shifts
.reduce((memo, shift) => {
const storeShift = this.shifts[shift.shiftId];
let layer = memo.find((level) => level.priority === storeShift.priority_level);
if (!layer) {
layer = { priority: storeShift.priority_level, shifts: [] };
memo.push(layer);
}
layer.shifts.push(shift);
//console.log(type, shifts);
return memo;
}, [])
.sort((a, b) => {
if (a.priority > b.priority) {
return 1;
}
if (a.priority < b.priority) {
return -1;
}
return 0;
})
: undefined;
this.events = {
...this.events,
@ -339,7 +374,7 @@ export class ScheduleStore extends BaseStore {
...this.events[scheduleId],
[type]: {
...this.events[scheduleId]?.[type],
[fromString]: shifts,
[fromString]: layers ? layers : shifts,
},
},
};

View file

@ -89,3 +89,8 @@ export interface Events {
name: string;
type: number; //?
}
export interface Layer {
priority: Shift['priority_level'];
shifts: Array<{ shiftId: Shift['id']; events: Event[] }>;
}