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
@@ -0,0 +1,62 @@
import { fetchWeatherCheck, requestWeatherAssessment, WEATHER_PATHS } from './weatherRepository';
import { IWeatherPosition, Nullable } from './weatherTypes';
import { createFakeHttpClient } from '../../shared/testing/fakeHttpClient';
const API = 'https://api.tnsu.kz';
const PERIOD = { start: '2026-10-01', end: '2026-10-07' };
const position = (profession: string, assessment: Nullable<string> = null): IWeatherPosition => ({
project: 'ТЕСТ',
profession,
date: '2026-10-01',
work_type: 'Бетонирование',
hours: 8,
hours_type: null,
headcount: 2,
shift: '1',
shift_start: '08:00',
shift_end: '20:00',
weather: null,
assessment,
comment: null,
downtime_act_recommended: null,
downtime_act_reason: null
});
describe('адреса сверки погоды (1:1 со stroy-bot-main)', () => {
it('период — Unix-секунды UTC: 00:00:00 первого дня и 23:59:59 последнего', () => {
expect(WEATHER_PATHS.check(API, 51, PERIOD)).toEqual(
'https://api.tnsu.kz/Report/StroyBot/WeatherCheck?start=1790812800&end=1791417599&projectId=51'
);
});
it('оценка ИИ', () => {
expect(WEATHER_PATHS.assess(API)).toEqual('https://api.tnsu.kz/Report/StroyBot/WeatherCheck/Assess');
});
});
describe('запросы', () => {
it('сверка — GET, ответ отдаётся как есть', async () => {
const body = { project: 'ТЕСТ', days: [], positions: [], notes: [] };
const client = createFakeHttpClient([{ match: 'WeatherCheck?', response: { body } }]);
expect(await fetchWeatherCheck(client, API, 51, PERIOD)).toEqual(body);
expect(client.requests[0].request.method).toBeUndefined();
});
it('ошибка API пробрасывается', async () => {
const client = createFakeHttpClient([{ match: 'WeatherCheck?', response: { status: 500, body: 'boom' } }]);
await expect(fetchWeatherCheck(client, API, 51, PERIOD)).rejects.toThrow('500');
});
it('оценка — POST со всеми позициями в теле { positions }', async () => {
const result = { assessed: 1, skipped: 0, positions: [position('Бетонщик', 'В пределах нормы')] };
const client = createFakeHttpClient([{ match: '/Assess', response: { body: result } }]);
expect(await requestWeatherAssessment(client, API, [position('Бетонщик')])).toEqual(result);
const sent = client.requests[0].request;
expect(sent.method).toEqual('POST');
expect(sent.headers).toEqual({ accept: '*/*', 'content-type': 'application/json' });
expect(JSON.parse(String(sent.body))).toEqual({ positions: [position('Бетонщик')] });
});
});