feat: weather conditions check from stroy-bot-main

Ports the weather check added to stroy-bot-main on 2026-10-16: daily
station weather, report positions with weather per shift and the AI
assessment of working conditions. Opens as ?page=weather&start=&end=
instead of the #weathercheck hash, and hub dates survive the way back.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Тимур Городецкий
2026-10-20 09:47:02 +05:00
parent b18cf50efd
commit d606f8371c
33 changed files with 1365 additions and 12 deletions
@@ -1,10 +1,15 @@
import * as React from 'react';
import { buildSearch, IRoute, parseRoute } from './routes';
interface INavigateOptions {
/** Заменить текущую запись истории (replaceState), а не добавить новую. */
replace?: boolean;
}
interface IRouterValue {
route: IRoute;
/** Перейти в раздел: меняет адрес (history.pushState) и перерисовывает приложение. */
navigate: (route: IRoute) => void;
navigate: (route: IRoute, options?: INavigateOptions) => void;
/** Полный адрес раздела — для настоящих ссылок (Ctrl+клик, «открыть в новой вкладке»). */
buildHref: (route: IRoute) => string;
}
@@ -29,8 +34,12 @@ export const RouterProvider: React.FC<{ children: React.ReactNode }> = ({ childr
return () => window.removeEventListener('popstate', onPopState);
}, []);
const navigate = React.useCallback((next: IRoute): void => {
window.history.pushState(undefined, '', buildHref(next));
const navigate = React.useCallback((next: IRoute, options: INavigateOptions = {}): void => {
if (options.replace) {
window.history.replaceState(undefined, '', buildHref(next));
} else {
window.history.pushState(undefined, '', buildHref(next));
}
setRoute(next);
window.scrollTo(0, 0);
}, []);
@@ -17,4 +17,20 @@ describe('parseRoute', () => {
it('адрес только с projectId=7', () => {
expect(parseRoute('?projectId=7')).toEqual({ page: 'hub', projectId: '7' });
});
it('сверка погоды с периодом', () => {
expect(parseRoute('?page=weather&projectId=7&start=2026-10-01&end=2026-10-07')).toEqual({
page: 'weather',
projectId: '7',
start: '2026-10-01',
end: '2026-10-07'
});
});
});
describe('buildSearch', () => {
it('параметры прошлого маршрута не переносятся, посторонние параметры страницы остаются', () => {
expect(buildSearch({ page: 'weather', projectId: '7', start: '2026-10-01' }, '?debug=true&reportId=r1')).toEqual(
'?debug=true&page=weather&projectId=7&start=2026-10-01'
);
});
});
+26 -3
View File
@@ -2,7 +2,17 @@
// SharePoint (?page=objects&projectId=5), поэтому ссылку на любой раздел можно
// скопировать, открыть в новой вкладке, а «назад» в браузере работает как раньше.
export const PAGE_IDS = ['hub', 'objects', 'works', 'reports', 'report', 'import', 'resources', 'binder'] as const;
export const PAGE_IDS = [
'hub',
'objects',
'works',
'reports',
'report',
'import',
'resources',
'binder',
'weather'
] as const;
export type PageId = (typeof PAGE_IDS)[number];
@@ -16,6 +26,10 @@ export interface IRoute {
reportId?: string;
/** Форма отчёта: создать на основе существующего (GUID Dataverse). */
copyFromId?: string;
/** Главная и сверка погоды: начало периода (YYYY-MM-DD). */
start?: string;
/** Главная и сверка погоды: конец периода (YYYY-MM-DD). */
end?: string;
}
export const ROUTE_PARAMS = {
@@ -23,10 +37,19 @@ export const ROUTE_PARAMS = {
projectId: 'projectId',
objectId: 'objectId',
reportId: 'reportId',
copyFromId: 'copyFromId'
copyFromId: 'copyFromId',
start: 'start',
end: 'end'
} as const;
const ROUTE_VALUE_KEYS: Array<Exclude<keyof IRoute, 'page'>> = ['projectId', 'objectId', 'reportId', 'copyFromId'];
const ROUTE_VALUE_KEYS: Array<Exclude<keyof IRoute, 'page'>> = [
'projectId',
'objectId',
'reportId',
'copyFromId',
'start',
'end'
];
function isPageId(value: string | undefined): value is PageId {
return value !== undefined && (PAGE_IDS as readonly string[]).indexOf(value) !== -1;