pinkmine/frontend/src/dashboard/widgets/calendar-next-events.tsx

231 lines
7.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { observer } from 'mobx-react-lite';
import { Instance, onSnapshot, types } from 'mobx-state-tree';
import React from 'react';
import * as Luxon from 'luxon';
import * as DashboardStoreNs from '../dashboard-store';
import { DateTime } from 'luxon';
import { IssueHref } from '../../misc-components/issue-href';
// Описание основных и вспомогательных моделей данных для календаря:
export const CalendarEvent = types.model({
from: types.string,
fromTimestamp: types.number,
to: types.string,
toTimestamp: types.number,
fullDay: types.boolean,
description: types.string,
});
export type ICalendarEvent = Instance<typeof CalendarEvent>;
export const FormattedDateTime = types.model({
fromDate: types.string,
fromTime: types.string,
toDate: types.string,
toTime: types.string,
interval: types.string,
});
export type IFormattedDateTime = Instance<typeof FormattedDateTime>;
export const IssueAndEvent = types.model({
issue: types.frozen<any>(),
event: CalendarEvent,
});
export type IIssueAndEvent = Instance<typeof IssueAndEvent>;
type InDay = {
order: number;
relativeDate: string | null;
formattedDate: string;
events: string[];
};
export const DATE_FORMAT = 'dd.MM.yyyy';
export const TIME_FORMAT = 'HH:mm';
function getEventKey(e: IIssueAndEvent): string {
const description: string = e.event.description.replaceAll(/\s+/g, '_');
const fromKey = String(e.event.fromTimestamp);
const toKey = String(e.event.toTimestamp);
return `${e.issue.id}-${fromKey}-${toKey}-${description}`;
}
function getFormattedDateTime(
issueAndEvent: IIssueAndEvent,
): IFormattedDateTime {
const from = DateTime.fromMillis(issueAndEvent.event.fromTimestamp);
const to = DateTime.fromMillis(issueAndEvent.event.toTimestamp);
const fromDate: string = from.isValid ? from.toFormat(DATE_FORMAT) : '-';
const fromTime: string = from.isValid ? from.toFormat(TIME_FORMAT) : '-';
const toDate: string = to.isValid ? to.toFormat(DATE_FORMAT) : '-';
const toTime: string = to.isValid ? to.toFormat(TIME_FORMAT) : '-';
let interval: string;
if (issueAndEvent.event.fullDay) {
interval = 'весь день';
} else if (toTime != '-') {
interval = `${fromTime} - ${toTime}`;
} else {
interval = `${fromTime}`;
}
return FormattedDateTime.create({
fromDate: fromDate,
fromTime: fromTime,
toDate: toDate,
toTime: toTime,
interval: interval,
});
}
function getRelativeDate(issueAndEvent: IIssueAndEvent): string | null {
const from = Luxon.DateTime.fromMillis(issueAndEvent.event.fromTimestamp);
return from.toRelativeCalendar();
}
function getStartOfDayTimestamp(issueAndEvent: IIssueAndEvent): number {
const from = Luxon.DateTime.fromMillis(issueAndEvent.event.fromTimestamp);
return from.startOf('day').toMillis();
}
/** Стор данных для виджета "Календарь следующих событий" */
export const EventsStore = types
.model({
events: types.array(IssueAndEvent),
})
.views((self) => {
return {
/** Геттер мапы событий в формате eventKey -> issue+event */
eventsMap: (): Record<string, IIssueAndEvent> => {
return self.events.reduce((acc, issueAndEvent) => {
const key = getEventKey(issueAndEvent);
acc[key] = issueAndEvent;
return acc;
}, {} as Record<string, IIssueAndEvent>);
},
/**
* Геттер сгруппированных по дням данных.
*
* Ключ группы определяется по timestamp-у указывающему на начало дня
*
* Дополнительные поля relativeDate и formattedDate для отображения в виджете подробной
* информации об этой группе событий
*/
orderedByDates: (): InDay[] => {
const res: Record<number, InDay> = self.events.reduce(
(acc, issueAndEvent) => {
const order = getStartOfDayTimestamp(issueAndEvent);
const formattedDate = DateTime.fromMillis(
issueAndEvent.event.fromTimestamp,
).toFormat(DATE_FORMAT);
if (!acc[order]) {
acc[order] = {
order: order,
relativeDate: getRelativeDate(issueAndEvent),
formattedDate: formattedDate,
events: [],
};
}
const key = getEventKey(issueAndEvent);
acc[order].events.push(key);
return acc;
},
{} as Record<number, InDay>,
);
return Object.values(res).sort((a, b) => a.order - b.order);
},
/**
* Мапа событий с отформатированными значениями даты и времени
*
* * Ключ - это ключ события
* * Значение - вспомогательная информация для человеко-читаемого вывода даты и времени
*/
formattedDateTimes: (): Record<string, IFormattedDateTime> => {
const res: Record<string, IFormattedDateTime> = self.events.reduce(
(acc, event) => {
const key = getEventKey(event);
acc[key] = getFormattedDateTime(event);
return acc;
},
{} as Record<string, IFormattedDateTime>,
);
return res;
},
};
})
.actions((self) => {
return {
/** Сеттер основных данных */
setEvents: (events: any): void => {
self.events = events;
},
};
});
export type IEventsStore = Instance<typeof EventsStore>;
/**
* Компонент для отображения данных из стора EventsStore
*
* @see {EventsStore}
*/
export const CalendarList = observer(
(props: { store: IEventsStore }): JSX.Element => {
const list = props.store.orderedByDates().map((events) => {
const keyOfGroup = `${events.order}-${events.relativeDate}`;
const item = (
<div key={keyOfGroup}>
<p title={events.formattedDate}>{events.relativeDate}:</p>
<ul>
{events.events.map((keyOfEvent) => {
const events = props.store.eventsMap();
const formatted = props.store.formattedDateTimes();
if (!events[keyOfEvent] && !formatted[keyOfEvent]) return <></>;
const issue = events[keyOfEvent].issue;
return (
<li key={keyOfEvent}>
{formatted[keyOfEvent].interval}:{' '}
<IssueHref
id={issue.id}
subject={events[keyOfEvent].event.description}
tracker={issue.tracker.name}
url={issue.url.url}
/>
</li>
);
})}
</ul>
</div>
);
return item;
});
return <>{list}</>;
},
);
export type Props = {
store: DashboardStoreNs.IWidget;
};
/**
* Основной компонент календаря
*
* Он нужен для преобразования стора абстрактного виджета в стор специфичный для
* календаря
*/
export const CalendarNextEvents = observer((props: Props): JSX.Element => {
const calendarListStore = EventsStore.create();
onSnapshot(props.store, (storeState) => {
if (storeState.data) {
calendarListStore.setEvents(storeState.data);
}
});
return (
<>
<CalendarList store={calendarListStore} />
</>
);
});