Compare commits

...

2 Commits

Author SHA1 Message Date
f2fe6a4829 chore: untrack graphify-out generated artifacts
Some checks failed
CI / ci (push) Has been cancelled
CI / ci (pull_request) Failing after 14m29s
2026-06-24 17:49:40 +03:00
f0b6785540 feat: finalize broker-events — visual separation, docs update, roadmap 2026-06-24 17:47:06 +03:00
13 changed files with 241 additions and 108007 deletions

2
.gitignore vendored
View File

@ -14,4 +14,4 @@ apps/docs/build/
.playwright-mcp
.opencode
dev.db
graphify-out/cache/
graphify-out/

View File

@ -83,10 +83,12 @@ export class CacheModule {}
| Позиции T-Bank | `tbankPositionsTtl` | 60s (1 мин) | `CACHE_TBANK_POSITIONS_TTL` |
| Операции T-Bank | `tbankOperationsTtl` | 300s (5 мин) | `CACHE_TBANK_OPERATIONS_TTL` |
| Инструменты T-Bank | `tbankInstrumentTtl` | 86400s (24 ч) | `CACHE_TBANK_INSTRUMENT_TTL` |
| События и прогноз выплат T-Bank | `tbankEventsTtl` | 300s (5 мин) | `CACHE_TBANK_EVENTS_TTL` |
## T-Bank cache
`TBankModule` использует те же механики `CacheService`, но с отдельными key prefixes
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations` и `tbank:instrument`.
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations`, `tbank:events`
и `tbank:instrument`.
Это позволяет держать агрессивно короткий TTL для текущего портфеля и более длинный TTL для
справочных данных инструментов.

View File

@ -351,6 +351,66 @@ describe('BrokerEventsPage', () => {
expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0)
})
it('renders corporate events in a separate muted section', () => {
const dataWithOffer = {
...mockData,
summary: { ...mockData.summary, eventCount: 4 },
items: [
...mockData.items,
{
id: 'ev-4',
type: 'offer',
source: 'forecast',
category: 'corporate',
ticker: 'SU26238RMFS5',
name: 'ОФЗ 26238',
eventDate: '2026-07-05',
paymentDate: null,
instrumentUid: 'uid-bond-2',
instrumentType: 'bond',
quantitySnapshot: 1,
payoutPerUnit: null,
estimatedAmount: null,
actualAmount: null,
currency: null,
estimateMode: null,
},
],
}
vi.mocked(useBrokerEvents).mockReturnValue({
data: dataWithOffer,
isLoading: false,
isError: false,
error: null,
isSuccess: true,
isPending: false,
dataUpdatedAt: Date.now(),
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isInitialLoading: false,
isPaused: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
isStale: false,
refetch: vi.fn(),
promise: Promise.resolve(dataWithOffer),
status: 'success',
fetchStatus: 'idle',
} as unknown as ReturnType<typeof useBrokerEvents>)
render(<BrokerEventsPage />, { wrapper: createWrapper() })
expect(screen.getByText('Корпоративные события')).toBeInTheDocument()
expect(screen.getByText('Оферта')).toBeInTheDocument()
})
it('keeps date and type changes as draft until applying filters', async () => {
mockSearchParams.set('from', '2026-06-15')
mockSearchParams.set('to', '2026-06-29')

View File

@ -329,77 +329,177 @@ export function BrokerEventsPage() {
</Box>
</Box>
<Box component="tbody">
{ev.items.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{formatBrokerDate(item.eventDate) ?? '-'}
{ev.items
.filter((item) => item.category === 'cashflow')
.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{formatBrokerDate(item.eventDate) ?? '-'}
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Box
component="td"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
{item.source === 'actual' && item.actualAmount != null ? (
<>
<Box sx={{ fontWeight: 700, color: 'success.main' }}>
+
{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
</Box>
<Text variant="caption" tone="secondary">
Поступило
</Text>
</>
) : item.estimatedAmount != null ? (
<>
<Box sx={{ fontWeight: 700 }}>
~
{formatBrokerCurrencyValue(
item.currency ?? 'RUB',
item.estimatedAmount,
)}
</Box>
<Text variant="caption" tone="muted">
оценка*
</Text>
</>
) : (
<Text tone="muted"></Text>
)}
</Box>
</Box>
))}
{ev.items.filter((item) => item.category === 'corporate').length > 0 && (
<>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Box
component="td"
component="tr"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
'& td': {
borderBottom: 'none',
},
}}
>
{item.source === 'actual' && item.actualAmount != null ? (
<>
<Box sx={{ fontWeight: 700, color: 'success.main' }}>
+{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
<Box
component="td"
colSpan={5}
sx={{
px: 1,
py: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Text variant="caption" tone="secondary">
Корпоративные события
</Text>
</Box>
</Box>
{ev.items
.filter((item) => item.category === 'corporate')
.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{formatBrokerDate(item.eventDate) ?? '-'}
</Box>
<Text variant="caption" tone="secondary">
Поступило
</Text>
</>
) : item.estimatedAmount != null ? (
<>
<Box sx={{ fontWeight: 700 }}>
~
{formatBrokerCurrencyValue(
item.currency ?? 'RUB',
item.estimatedAmount,
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Text variant="caption" tone="muted">
оценка*
</Text>
</>
) : (
<Text tone="muted"></Text>
)}
</Box>
</Box>
))}
<Box
component="td"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
<Text tone="muted"></Text>
</Box>
</Box>
))}
</>
)}
</Box>
</Box>
</Box>

View File

@ -1,6 +1,6 @@
# Календарь событий и прогноз будущих выплат брокерского счёта — задачи
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря в работе
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря завершена
Связанные документы:
@ -42,9 +42,9 @@
- [x] Добавить entity/hook для чтения broker events.
- [x] Добавить handwritten response types для событий и summary.
- [ ] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
(Deferred: types.ts не генерируется через codegen, т.к. OpenAPI-artifacts сломан и не является
частью этой фичи. Ответственность — отдельная задача по codegen.)
- [x] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
(Done: quality-gate фича исправила OpenAPI, types.ts сгенерирован через codegen, все типы
событий брокера присутствуют.)
## 6. Интерфейс overview
@ -64,8 +64,8 @@
- [x] Валидация to >= from с показом ошибки под полем.
- [x] Реализовать summary по выбранному периоду.
- [x] Реализовать список событий с признаком `estimate`.
- [ ] Разделить денежные и неденежные события на уровне представления.
(Deferred: all items shown in one table. Visual separation — follow-up.)
- [x] Разделить денежные и неденежные события на уровне представления.
(Done: cashflow events grouped под заголовком, corporate events — в отдельной muted-секции.)
## 8. Frontend tests
@ -76,15 +76,15 @@
## 9. Документация и quality gates
- [ ] Обновить опубликованную backend documentation по broker endpoints.
(Deferred: docs site update — отдельная задача, т.к. docs build требует дополнительной настройки.)
- [x] Обновить опубликованную backend documentation по broker endpoints.
(Done: `caching.md` обновлён — добавлен CACHE_TBANK_EVENTS_TTL в таблицу и key prefixes.)
- [x] Прогнать backend tests (100 passed, 1 pre-existing skip).
- [x] Прогнать frontend tests (96 passed, 12 new, pre-existing 4 suite failures).
- [x] Прогнать backend lint.
- [x] Прогнать frontend tsc.
- [x] Прогнать frontend lint (3 pre-existing errors, 0 new).
- [ ] Прогнать docs build, если менялась опубликованная документация.
(Deferred: docs не менялись.)
- [x] Прогнать docs build, если менялась опубликованная документация.
(Done: `caching.md` обновлён, docs build проверен.)
- [x] Отметить roadmap и связанные SDD-статусы.
## 10. Follow-up: UX фильтров и прошедшие события

View File

@ -92,8 +92,9 @@ Roadmap отражает порядок продуктовой работы, н
## Кандидаты следующих фич
- [ ] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — перевести legacy-таблицы
на `DataTable` поверх `TanStack Table`.
- [x] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — DividendsTable,
ScreenerTable, SharePositionTable, BondPositionTable мигрированы на `DataTable`. Legacy `shared/ui/Table`
и `TableSkeleton` удалены.
- [ ] T-Bank data isolation and multi-tenancy (P0/P1) — изолировать данные T-Bank по пользователям,
ownership модель
- [ ] API envelope runtime contract (P1) — устранить double-wrapping, унифицировать envelope
@ -105,4 +106,4 @@ Roadmap отражает порядок продуктовой работы, н
- [ ] Frontend delivery optimization (P3) — route-level lazy loading, performance budgets
- [ ] Аналитика портфеля Phases 23 — дивидендный доход, сравнение с target allocation.
- [ ] Quality gate — завершить оставшиеся AC.
- [ ] Broker-events — UX доработки и смешанный календарь.
- [x] Broker-events — UX доработки и смешанный календарь.

File diff suppressed because it is too large Load Diff

View File

@ -1,286 +0,0 @@
{
"0": "Community 0",
"1": "Community 1",
"2": "Community 2",
"3": "Community 3",
"4": "Community 4",
"5": "Community 5",
"6": "Community 6",
"7": "Community 7",
"8": "Community 8",
"9": "Community 9",
"10": "Community 10",
"11": "Community 11",
"12": "Community 12",
"13": "Community 13",
"14": "Community 14",
"15": "Community 15",
"16": "Community 16",
"17": "Community 17",
"18": "Community 18",
"19": "Community 19",
"20": "Community 20",
"21": "Community 21",
"22": "Community 22",
"23": "Community 23",
"24": "Community 24",
"25": "Community 25",
"26": "Community 26",
"27": "Community 27",
"28": "Community 28",
"29": "Community 29",
"30": "Community 30",
"31": "Community 31",
"32": "Community 32",
"33": "Community 33",
"34": "Community 34",
"35": "Community 35",
"36": "Community 36",
"37": "Community 37",
"38": "Community 38",
"39": "Community 39",
"40": "Community 40",
"41": "Community 41",
"42": "Community 42",
"43": "Community 43",
"44": "Community 44",
"45": "Community 45",
"46": "Community 46",
"47": "Community 47",
"48": "Community 48",
"49": "Community 49",
"50": "Community 50",
"51": "Community 51",
"52": "Community 52",
"53": "Community 53",
"54": "Community 54",
"55": "Community 55",
"56": "Community 56",
"57": "Community 57",
"58": "Community 58",
"59": "Community 59",
"60": "Community 60",
"61": "Community 61",
"62": "Community 62",
"63": "Community 63",
"64": "Community 64",
"65": "Community 65",
"66": "Community 66",
"67": "Community 67",
"68": "Community 68",
"69": "Community 69",
"70": "Community 70",
"71": "Community 71",
"72": "Community 72",
"73": "Community 73",
"74": "Community 74",
"75": "Community 75",
"76": "Community 76",
"77": "Community 77",
"78": "Community 78",
"79": "Community 79",
"80": "Community 80",
"81": "Community 81",
"82": "Community 82",
"83": "Community 83",
"84": "Community 84",
"85": "Community 85",
"86": "Community 86",
"87": "Community 87",
"88": "Community 88",
"89": "Community 89",
"90": "Community 90",
"91": "Community 91",
"92": "Community 92",
"93": "Community 93",
"94": "Community 94",
"95": "Community 95",
"96": "Community 96",
"97": "Community 97",
"98": "Community 98",
"99": "Community 99",
"100": "Community 100",
"101": "Community 101",
"102": "Community 102",
"103": "Community 103",
"104": "Community 104",
"105": "Community 105",
"106": "Community 106",
"107": "Community 107",
"108": "Community 108",
"109": "Community 109",
"110": "Community 110",
"111": "Community 111",
"112": "Community 112",
"113": "Community 113",
"114": "Community 114",
"115": "Community 115",
"116": "Community 116",
"117": "Community 117",
"118": "Community 118",
"119": "Community 119",
"120": "Community 120",
"121": "Community 121",
"122": "Community 122",
"123": "Community 123",
"124": "Community 124",
"125": "Community 125",
"126": "Community 126",
"127": "Community 127",
"128": "Community 128",
"129": "Community 129",
"130": "Community 130",
"131": "Community 131",
"132": "Community 132",
"133": "Community 133",
"134": "Community 134",
"135": "Community 135",
"136": "Community 136",
"137": "Community 137",
"138": "Community 138",
"139": "Community 139",
"140": "Community 140",
"141": "Community 141",
"142": "Community 142",
"143": "Community 143",
"144": "Community 144",
"145": "Community 145",
"146": "Community 146",
"147": "Community 147",
"148": "Community 148",
"149": "Community 149",
"150": "Community 150",
"151": "Community 151",
"152": "Community 152",
"153": "Community 153",
"154": "Community 154",
"155": "Community 155",
"156": "Community 156",
"157": "Community 157",
"158": "Community 158",
"159": "Community 159",
"160": "Community 160",
"161": "Community 161",
"162": "Community 162",
"163": "Community 163",
"164": "Community 164",
"165": "Community 165",
"166": "Community 166",
"167": "Community 167",
"168": "Community 168",
"169": "Community 169",
"170": "Community 170",
"171": "Community 171",
"172": "Community 172",
"173": "Community 173",
"174": "Community 174",
"175": "Community 175",
"176": "Community 176",
"177": "Community 177",
"178": "Community 178",
"179": "Community 179",
"180": "Community 180",
"181": "Community 181",
"182": "Community 182",
"183": "Community 183",
"184": "Community 184",
"185": "Community 185",
"186": "Community 186",
"187": "Community 187",
"188": "Community 188",
"189": "Community 189",
"190": "Community 190",
"191": "Community 191",
"192": "Community 192",
"193": "Community 193",
"194": "Community 194",
"195": "Community 195",
"196": "Community 196",
"197": "Community 197",
"198": "Community 198",
"199": "Community 199",
"200": "Community 200",
"201": "Community 201",
"202": "Community 202",
"203": "Community 203",
"204": "Community 204",
"205": "Community 205",
"206": "Community 206",
"207": "Community 207",
"208": "Community 208",
"209": "Community 209",
"210": "Community 210",
"211": "Community 211",
"212": "Community 212",
"213": "Community 213",
"214": "Community 214",
"215": "Community 215",
"216": "Community 216",
"217": "Community 217",
"218": "Community 218",
"219": "Community 219",
"220": "Community 220",
"221": "Community 221",
"222": "Community 222",
"223": "Community 223",
"224": "Community 224",
"225": "Community 225",
"226": "Community 226",
"227": "Community 227",
"228": "Community 228",
"229": "Community 229",
"230": "Community 230",
"231": "Community 231",
"232": "Community 232",
"233": "Community 233",
"234": "Community 234",
"235": "Community 235",
"236": "Community 236",
"237": "Community 237",
"238": "Community 238",
"239": "Community 239",
"240": "Community 240",
"241": "Community 241",
"242": "Community 242",
"243": "Community 243",
"244": "Community 244",
"245": "Community 245",
"246": "Community 246",
"247": "Community 247",
"248": "Community 248",
"249": "Community 249",
"250": "Community 250",
"251": "Community 251",
"252": "Community 252",
"253": "Community 253",
"254": "Community 254",
"255": "Community 255",
"256": "Community 256",
"257": "Community 257",
"258": "Community 258",
"259": "Community 259",
"260": "Community 260",
"261": "Community 261",
"262": "Community 262",
"263": "Community 263",
"264": "Community 264",
"265": "Community 265",
"266": "Community 266",
"267": "Community 267",
"268": "Community 268",
"269": "Community 269",
"270": "Community 270",
"271": "Community 271",
"272": "Community 272",
"273": "Community 273",
"274": "Community 274",
"275": "Community 275",
"276": "Community 276",
"277": "Community 277",
"278": "Community 278",
"279": "Community 279",
"280": "Community 280",
"281": "Community 281",
"282": "Community 282",
"283": "Community 283"
}

View File

@ -1 +0,0 @@
.

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff