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; export const FormattedDateTime = types.model({ fromDate: types.string, fromTime: types.string, toDate: types.string, toTime: types.string, interval: types.string, }); export type IFormattedDateTime = Instance; export const IssueAndEvent = types.model({ issue: types.frozen(), event: CalendarEvent, }); export type IIssueAndEvent = Instance; 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 => { return self.events.reduce((acc, issueAndEvent) => { const key = getEventKey(issueAndEvent); acc[key] = issueAndEvent; return acc; }, {} as Record); }, /** * Геттер сгруппированных по дням данных. * * Ключ группы определяется по timestamp-у указывающему на начало дня * * Дополнительные поля relativeDate и formattedDate для отображения в виджете подробной * информации об этой группе событий */ orderedByDates: (): InDay[] => { const res: Record = 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, ); return Object.values(res).sort((a, b) => a.order - b.order); }, /** * Мапа событий с отформатированными значениями даты и времени * * * Ключ - это ключ события * * Значение - вспомогательная информация для человеко-читаемого вывода даты и времени */ formattedDateTimes: (): Record => { const res: Record = self.events.reduce( (acc, event) => { const key = getEventKey(event); acc[key] = getFormattedDateTime(event); return acc; }, {} as Record, ); return res; }, }; }) .actions((self) => { return { /** Сеттер основных данных */ setEvents: (events: any): void => { self.events = events; }, }; }); export type IEventsStore = Instance; /** * Компонент для отображения данных из стора 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 = (

{events.relativeDate}:

    {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 (
  • {formatted[keyOfEvent].interval}:{' '}
  • ); })}
); 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 ( <> ); });