add color coding
This commit is contained in:
parent
af7128c591
commit
874be7fc90
10 changed files with 175 additions and 146 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.root {
|
||||
height: 28px;
|
||||
background: #3274d9;
|
||||
background: #595959;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue