add color coding

This commit is contained in:
Maxim 2022-08-19 15:12:22 +03:00
parent af7128c591
commit 874be7fc90
10 changed files with 175 additions and 146 deletions

View file

@ -3,47 +3,6 @@ import dayjs from 'dayjs';
import { Shift } from 'models/schedule/schedule.types';
import { User } from 'models/user/user.types';
export const getRandomTimeslots = (count = 6, layerIndex, rotationIndex) => {
const slots = [];
for (let i = 0; i < count; i++) {
const start = dayjs()
.startOf('day')
.add(i * 4, 'hour');
const end = dayjs()
.startOf('day')
.add(i * 4 + 2, 'hour');
//const inactive = end.isBefore(dayjs());
const inactive = false;
slots.push({
start,
end,
inactive,
users: [getRandomUser() /*, getRandomUser()*/],
color: getColor(layerIndex, rotationIndex),
});
}
return slots;
};
const L1_COLORS = ['#3D71D9', '#1A6BE8', '#6D609C', '#50639C', '#8214A0', '#44449F', '#4D3B72', '#273C6C'];
const L2_COLORS = ['#3CB979', '#A49E7C', '#188343', '#746D46', '#84362A', '#464121', '#521913', '#414130'];
const L3_COLORS = ['#377277', '#797B83', '#638282', '#626779', '#364E4E', '#47494F', '#423220', '#44321D'];
const OVERRIDE_COLORS = ['#C69B06', '#797B83', '#638282', '#626779'];
export const getOverrideColor = (index: number) => {
return OVERRIDE_COLORS[index];
};
const COLORS = [L1_COLORS, L2_COLORS, L3_COLORS, OVERRIDE_COLORS];
export const getColor = (layerIndex: number, rotationIndex: number) => {
return COLORS[layerIndex]?.[rotationIndex];
};
const USERS = [
'Innokentii Konstantinov',
'Ildar Iskhakov',

View file

@ -1,6 +1,6 @@
.root {
height: 28px;
background: #3274d9;
background: #595959;
border-radius: 2px;
position: relative;
display: flex;

View file

@ -30,7 +30,7 @@ interface ScheduleSlotProps {
const cx = cn.bind(styles);
const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color: propColor } = props;
const { index, layerIndex, rotationIndex, event, startMoment, currentTimezone, color } = props;
const { users } = event;
const trackMouse = false;
@ -62,7 +62,6 @@ const ScheduleSlot: FC<ScheduleSlotProps> = observer((props) => {
const inactive = false;
const color = propColor || getColor(layerIndex, rotationIndex);
const title = getTitle(storeUser);
return (

View file

@ -195,7 +195,7 @@ const RotationForm: FC<RotationFormProps> = observer((props) => {
<Text size="medium">
<HorizontalGroup spacing="sm">
<span>[L{shiftId === 'new' ? layerPriority : shift?.priority_level}]</span>
{shiftId === 'new' ? 'New Rotation' : shift?.title}
{shiftId === 'new' ? 'New Rotation' : shift?.id}
</HorizontalGroup>
</Text>
<HorizontalGroup>

View file

@ -11,14 +11,12 @@ import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
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 { getColor, getFromString } from 'models/schedule/schedule.helpers';
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';
import { getColor, getLabel, getRandomTimeslots, getRandomUser } from './Rotations.helpers';
import styles from './Rotations.module.css';
const cx = cn.bind(styles);
@ -54,50 +52,9 @@ class Rotations extends Component<RotationsProps, RotationsState> {
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
const storeLayers = store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[];
console.log('store.scheduleStore.rotationPreview', store.scheduleStore.rotationPreview);
let layers = storeLayers;
if (store.scheduleStore.rotationPreview) {
layers = [...layers];
const isNew = store.scheduleStore.rotationPreview.shifts[0].shiftId === 'new';
const priority = store.scheduleStore.rotationPreview.priority;
let added = false;
layers = layers.reduce((memo, layer, index) => {
if (isNew) {
if (layer.priority === priority) {
const newLayer = { ...layer };
newLayer.shifts = [...layer.shifts, ...store.scheduleStore.rotationPreview.shifts];
memo[index] = newLayer;
added = true;
}
} else {
const oldShiftIndex = layer.shifts.findIndex(
(shift) => shift.shiftId === store.scheduleStore.rotationPreview.shifts[0].shiftId
);
if (oldShiftIndex > -1) {
const newLayer = { ...layer };
newLayer.shifts = [...layer.shifts];
newLayer.shifts[oldShiftIndex] = store.scheduleStore.rotationPreview.shifts[0];
memo[index] = newLayer;
added = true;
}
}
return layers;
}, layers);
if (!added) {
layers.push(store.scheduleStore.rotationPreview);
}
}
const layers = store.scheduleStore.rotationPreview
? store.scheduleStore.rotationPreview
: (store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[]);
const options = layers
? layers.map((layer) => ({
@ -127,7 +84,7 @@ class Rotations extends Component<RotationsProps, RotationsState> {
</div>
<div className={cx('rotations-plus-title')}>
{layers && layers.length ? (
layers.map((layer) => (
layers.map((layer, layerIndex) => (
<div key={layer.priority}>
<div className={cx('layer')}>
<div className={cx('layer-title')}>
@ -146,8 +103,9 @@ class Rotations extends Component<RotationsProps, RotationsState> {
onClick={() => {
this.onRotationClick(shiftId);
}}
color={getColor(layerIndex, rotationIndex)}
events={events}
layerIndex={layer.priority - 1}
layerIndex={layerIndex}
rotationIndex={rotationIndex}
startMoment={startMoment}
currentTimezone={currentTimezone}

View file

@ -3,12 +3,13 @@ import React, { Component, useEffect } from 'react';
import { Button, HorizontalGroup, Icon, Input, ValuePicker } from '@grafana/ui';
import cn from 'classnames/bind';
import dayjs from 'dayjs';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { getFromString } from 'models/schedule/schedule.helpers';
import { Schedule } from 'models/schedule/schedule.types';
import { getColor, getFromString } from 'models/schedule/schedule.helpers';
import { Layer, Schedule } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
import { withMobXProviderContext } from 'state/withStore';
@ -47,8 +48,15 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
? store.scheduleStore.finalPreview
: store.scheduleStore.events[scheduleId]?.['final']?.[getFromString(startMoment)];
const layers = store.scheduleStore.rotationPreview
? store.scheduleStore.rotationPreview
: (store.scheduleStore.events[scheduleId]?.['rotation']?.[getFromString(startMoment)] as Layer[]);
const currentTimeHidden = currentTimeX < 0 || currentTimeX > 1;
console.log(toJS(shifts));
console.log(toJS(layers));
return (
<>
<div className={cx('root')}>
@ -70,9 +78,26 @@ class ScheduleFinal extends Component<ScheduleFinalProps, ScheduleOverridesState
<TimelineMarks startMoment={startMoment} />
<div className={cx('rotations')}>
{shifts && shifts.length ? (
shifts.map(({ shiftId, events }, index) => (
<Rotation key={index} events={events} startMoment={startMoment} currentTimezone={currentTimezone} />
))
shifts.map(({ shiftId, events }, index) => {
const layerIndex = layers
? layers.findIndex((layer) => layer.shifts.some((shift) => shift.shiftId === shiftId))
: -1;
const rotationIndex =
layerIndex > -1 ? layers[layerIndex].shifts.findIndex((shift) => shift.shiftId === shiftId) : -1;
console.log(layerIndex, rotationIndex);
return (
<Rotation
key={index}
events={events}
startMoment={startMoment}
currentTimezone={currentTimezone}
color={getColor(layerIndex, rotationIndex)}
/>
);
})
) : (
<Rotation events={[]} startMoment={startMoment} currentTimezone={currentTimezone} />
)}

View file

@ -9,7 +9,7 @@ import TimelineMarks from 'components/TimelineMarks/TimelineMarks';
import Rotation from 'containers/Rotation/Rotation';
import { RotationCreateData } from 'containers/RotationForm/RotationForm.types';
import ScheduleOverrideForm from 'containers/RotationForm/ScheduleOverrideForm';
import { getFromString } from 'models/schedule/schedule.helpers';
import { getFromString, getOverrideColor } from 'models/schedule/schedule.helpers';
import { Schedule, Shift } from 'models/schedule/schedule.types';
import { Timezone } from 'models/timezone/timezone.types';
import { WithStoreProps } from 'state/types';
@ -66,11 +66,11 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
<TimelineMarks startMoment={startMoment} />
<div className={cx('rotations')}>
{shifts && shifts.length ? (
shifts.map(({ shiftId, events }, index) => (
shifts.map(({ shiftId, events }, rotationIndex) => (
<Rotation
key={index}
key={rotationIndex}
events={events}
color="#C69B06"
color={getOverrideColor(rotationIndex)}
startMoment={startMoment}
currentTimezone={currentTimezone}
onClick={() => {
@ -81,7 +81,6 @@ class ScheduleOverrides extends Component<ScheduleOverridesProps, ScheduleOverri
) : (
<Rotation
events={[]}
color="#C69B06"
startMoment={startMoment}
currentTimezone={currentTimezone}
onClick={() => {

View file

@ -1,6 +1,6 @@
import dayjs from 'dayjs';
import { Event, Shift } from './schedule.types';
import { Event, Layer, ScheduleType, Shift } from './schedule.types';
export const getFromString = (moment: dayjs.Dayjs) => {
return moment.format('YYYY-MM-DD');
@ -16,7 +16,19 @@ export const fillGaps = (events: Event[]) => {
if (nextEvent) {
if (nextEvent.start !== event.end) {
newEvents.push({ start: event.end, end: nextEvent.start, is_gap: true });
newEvents.push({
start: event.end,
end: nextEvent.start,
is_gap: true,
users: [],
all_day: false,
shift: null,
missing_users: [],
is_empty: true,
calendar_type: ScheduleType.API,
priority_level: null,
source: 'web',
});
}
}
}
@ -25,13 +37,13 @@ export const fillGaps = (events: Event[]) => {
};
export const splitToShiftsAndFillGaps = (events: Event[]) => {
const shifts: Array<{ shiftId: Shift['id']; events: Event[] }> = [];
const shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; 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: [] };
shift = { shiftId: event.shift.pk, priority: event.priority_level, events: [] };
shifts.push(shift);
}
shift.events.push(event);
@ -44,3 +56,98 @@ export const splitToShiftsAndFillGaps = (events: Event[]) => {
return shifts;
};
export const splitToLayers = (
shifts: Array<{ shiftId: Shift['id']; priority: Shift['priority_level']; events: Event[] }>
) => {
return shifts
.reduce((memo, shift) => {
let layer = memo.find((level) => level.priority === shift.priority);
if (!layer) {
layer = { priority: shift.priority, 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;
});
};
export const enrichLayers = (
layers: Layer[],
newEvents: Event[],
shiftId: Shift['id'] | 'new',
priority: Shift['priority_level']
) => {
const updatingLayer = {
priority,
shifts: [{ shiftId: shiftId, events: fillGaps(newEvents.filter((event: Event) => !event.is_gap)) }],
};
const isNew = updatingLayer.shifts[0].shiftId === 'new';
let added = false;
layers = layers.reduce((memo, layer, index) => {
if (isNew) {
if (layer.priority === priority) {
const newLayer = { ...layer };
newLayer.shifts = [...layer.shifts, ...updatingLayer.shifts];
memo[index] = newLayer;
added = true;
}
} else {
const oldShiftIndex = layer.shifts.findIndex((shift) => shift.shiftId === updatingLayer.shifts[0].shiftId);
if (oldShiftIndex > -1) {
const newLayer = { ...layer };
newLayer.shifts = [...layer.shifts];
newLayer.shifts[oldShiftIndex] = updatingLayer.shifts[0];
memo[index] = newLayer;
added = true;
}
}
return layers;
}, layers);
if (!added) {
layers.push(updatingLayer);
}
return layers;
};
const L1_COLORS = ['#3D71D9', '#6D609C', '#4D3B72', '#8214A0'];
const L2_COLORS = ['#3CB979', '#188343', '#84362A', '#521913'];
const L3_COLORS = ['#377277', '#638282', '#364E4E', '#423220'];
const OVERRIDE_COLORS = ['#C69B06', '#C2C837'];
const COLORS = [L1_COLORS, L2_COLORS, L3_COLORS, OVERRIDE_COLORS];
export const getColor = (layerIndex: number, rotationIndex: number) => {
const normalizedLayerIndex = layerIndex % COLORS.length;
const normalizedRotationIndex = rotationIndex % COLORS[normalizedLayerIndex]?.length;
return COLORS[normalizedLayerIndex]?.[normalizedRotationIndex];
};
export const getOverrideColor = (rotationIndex: number) => {
const normalizedRotationIndex = rotationIndex % OVERRIDE_COLORS.length;
return OVERRIDE_COLORS[normalizedRotationIndex];
};

View file

@ -11,7 +11,7 @@ import { makeRequest } from 'network';
import { RootStore } from 'state';
import { SelectOption } from 'state/types';
import { fillGaps, splitToShiftsAndFillGaps } from './schedule.helpers';
import { enrichLayers, fillGaps, getFromString, splitToLayers, splitToShiftsAndFillGaps } from './schedule.helpers';
import { Events, Rotation, RotationType, Schedule, ScheduleEvent, Shift, Event, Layer } from './schedule.types';
const DEFAULT_FORMAT = 'YYYY-MM-DDTHH:mm:ss';
@ -73,7 +73,7 @@ export class ScheduleStore extends BaseStore {
} = {};
@observable
rotationPreview?: Layer;
rotationPreview?: Layer[];
@observable
finalPreview?: Array<{ shiftId: Shift['id']; events: Event[] }>;
@ -202,19 +202,25 @@ export class ScheduleStore extends BaseStore {
) {
const type = isOverride ? 3 : 2;
const typeString = isOverride ? 'override' : 'rotation';
const response = await makeRequest(`/oncall_shifts/preview/`, {
params: { date: fromString },
data: { type, schedule: scheduleId, shift_pk: shiftId === 'new' ? undefined : shiftId, ...params },
method: 'POST',
}).catch(this.onApiError);
this.rotationPreview = {
priority: params.priority_level,
shifts: [{ shiftId: shiftId, events: fillGaps(response.rotation.filter((event) => !event.is_gap)) }],
};
this.finalPreview = splitToShiftsAndFillGaps(response.final).filter((shift) => shift.shiftId !== shiftId);
if (isOverride) {
} else {
const layers = enrichLayers(
[...(this.events[scheduleId]?.['rotation']?.[fromString] as Layer[])],
response.rotation,
shiftId,
params.priority_level
);
this.rotationPreview = layers;
}
this.finalPreview = splitToShiftsAndFillGaps(response.final); /*.filter((shift) => shift.shiftId !== shiftId);*/
}
async updateRotation(shiftId: Shift['id'], params: Partial<Shift>) {
@ -345,31 +351,7 @@ export class ScheduleStore extends BaseStore {
}
});*/
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);
return memo;
}, [])
.sort((a, b) => {
if (a.priority > b.priority) {
return 1;
}
if (a.priority < b.priority) {
return -1;
}
return 0;
})
: undefined;
const layers = type === 'rotation' ? splitToLayers(shifts) : undefined;
this.events = {
...this.events,

View file

@ -71,16 +71,16 @@ export type RotationType = 'final' | 'rotation' | 'override';
export interface Event {
all_day: boolean;
calendar_type: 0;
calendar_type: ScheduleType;
end: string;
is_empty: boolean;
is_gap: boolean;
missing_users: [];
missing_users: Array<{ display_name: User['username']; pk: User['pk'] }>;
priority_level: number;
shift: { pk: string };
shift: { pk: Shift['id'] | null };
source: string;
start: string;
users: [{ display_name: User['username']; pk: User['pk'] }];
users: Array<{ display_name: User['username']; pk: User['pk'] }>;
}
export interface Events {