Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,27 @@

## [Не выпущено]

Пока пусто. Сюда дописывается изменение, заметное пользователю, — тем же PR,
которым оно вносится, а не по памяти в день выпуска. Заголовки внутри:
«Добавлено», «Изменено», «Исправлено», «Убрано».
### Исправлено

- **Занятое время больше не считается занятием** — когда в YClients занимают
слот одним именем («Кац», «Сапожникова») или оставляют себе памятку
(«ДИАГНОСТИКИ, если нет никого…», «если Филиппов отменится — никого не
ставить»), приложение считало это занятием и приписывало полную ставку:
10 сентября выходило «Занятий 7, итог 10 500 ₽» там, где YClients начислил
9 000 ₽. Такие записи видны в дне, как и раньше, но больше не входят ни в
одну цифру — ни в «Занятий», ни в «Проведено», ни в «Заработано», ни в
«Ожидается», ни в «Потеряно на отменах». За сентябрь это четыре приписанных
занятия и 6 000 ₽.
- **Занятое время видно целиком** — слоты, подписанные в комментарии записи
(«Пирогов», «Сапожникова»), в приложении не показывались вовсе: в журнале
время занято, а в дне на этом месте была пустота. Теперь такая подпись видна
на своём месте — и, как и остальное занятое время, ни в одну цифру не
считается.
- **Уведомление снова открывает нужный экран** — если телефон успел выгрузить
приложение из памяти, нажатие на уведомление открывало обычный календарь:
ни дня из напоминания о занятии, ни «О программе» с готовой кнопкой
«Обновить». Из-за этого про новую версию приходилось узнавать заново,
вручную нажимая «Проверить обновления».

## [0.2.5] — 2026-09-01

Expand Down
54 changes: 54 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
Ближайшая работа — [`docs/next/`](docs/next/README.md): скриншоты и крайние
случаи обновления. Здесь — карта целиком.

> ⚡ **Горячее, не забыть:** [Интерфейс дня — известные проблемы](#интерфейс-дня--известные-проблемы)
> — 4 бага с боевого телефона после 0.2.5 (архив завален расхождением
> «Пришёл ↔ Оплачено», плашки с двумя людьми не нажимаются, баннер
> просвечивает записью, кривые края в диалоге отличий).

## Сделано

Крупные куски, закрытые целиком. Развёрнуто — по ссылкам, здесь только чтобы
Expand Down Expand Up @@ -129,6 +134,55 @@
- [ ] Список того, что осталось непокрытым, — в `docs/debt.md`, чтобы дыра
была видна, а не забыта

### Интерфейс дня — известные проблемы

Замечено 01.09.2026 на боевом телефоне после выхода 0.2.5. Все четыре — про
экран дня и диалог расхождений с архивом.

- [ ] **Архив завален расхождением «Пришёл ↔ Оплачено».** Архив импортирован
до 0.2.5, отдельного «оплачено» в нём нет: YClients теперь отдаёт
`Оплачено`, архив помнит `Пришёл`, и `ArchiveSyncCompare.differs`
(`app/src/main/java/ru/greemlab/neiro/ui/calendar/ArchiveSyncCompare.kt`)
метит почти каждый архивный день как несовпадающий. Диалог «Обновить
архив?» в таком виде затрёт все ручные правки. Развилка, нужен выбор
владельца:
1. Разовая миграция при загрузке архива: где в архиве `Пришёл`, а
близнец в YClients `Оплачено` и остальные поля совпадают — поднять
статус в архиве до `Оплачено`. Единый источник, но необратимо —
нужен снимок архива и откат.
2. Не считать `Пришёл` и `Оплачено` расхождением для дней, записанных
до 0.2.5 (нужна метка версии на архивном дне). Дешевле, но два
представления одного факта живут рядом.
3. Сравнивать только «проведён / не проведён», оплату из архивного
diff убрать вовсе. Дёшево, но настоящее расхождение по оплате в
архиве больше не увидеть.
Пересекается с бэклогом «Мастер слияния при конфликте архив ↔ YClients».
- [ ] **Плашки с двумя людьми не нажимаются.** Замена и «интенсив накрыл
занятие» (`ExpandableReplacementSlot`, `ExpandableIntensiveCoverSlot` в
`app/src/main/java/ru/greemlab/neiro/ui/components/daydetails/ScheduleSlotItem.kt`)
получают только `onContentClick`, а `onStudentStatusChange` из
`DayScheduleTimeline` в них не проброшен вовсе — быстрый тап «пришёл /
оплатил» по «!» для таких слотов мёртв даже в архиве. Плюс
горизонтальный `pointerInput` на всей плашке и `indication = null` —
нажатие без отклика, ощущается как неинтерактивное. Пробросить статус в
свёрнутую карточку и в раскрытые дочерние, дать отклик на тап.
- [ ] **Предупреждение о расхождении просвечивает первой записью.** Шапка
диалога дня (`DayDetailsContent` в
`app/src/main/java/ru/greemlab/neiro/ui/components/DayDetailsDialog.kt`)
залита только `panelScrim` (плотно сверху, прозрачно снизу), а лента
`DayScheduleTimeline` край не гасит — `fadingEdges` на её прокрутку не
наложен. Слот, уехавший под шапку, виден сквозь баннер и иконку «!».
Наложить `fadingEdges(top = headerHeight, bottom = footerHeight)` на
прокрутку ленты — как уже сделано в диалогах статистики дня.
- [ ] **Кривые края и нет полосы прокрутки в «Отличия архива от YClients».**
`ArchiveMismatchDetailsDialog` (тот же `DayDetailsDialog.kt`) гасит верх
и низ безусловно (`fadingEdges(top, bottom)`), поэтому первая и
последняя строки полупрозрачны даже когда список докручен до упора —
читается как обрезанный текст. Взять `fadingScrollEdges(scrollState)`
(уже есть в `app/src/main/java/ru/greemlab/neiro/ui/util/fadingEdges.kt`)
и добавить тонкую полосу прокрутки, чтобы было видно, что список длинный
и насколько прокручен.

### Бэклог

- [ ] Виджет «Расписание на сегодня»
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/ru/greemlab/neiro/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,19 @@ class MainActivity : ComponentActivity() {
highlightSlotKey = savedInstanceState.getString(STATE_HIGHLIGHT_SLOT_KEY)
notificationDeepLinkVersion = savedInstanceState.getInt(STATE_DEEP_LINK_VERSION, 0)
openAbout = savedInstanceState.getBoolean(STATE_OPEN_ABOUT, false)
} else {
applyNotificationExtras(intent)
}

// Extras разбираются всегда, а не только при пустом savedInstanceState.
// Система убивает процесс, а задачу оставляет в недавних: тап по
// уведомлению воссоздаёт активити со старым состоянием и новым
// intent'ом, и `onNewIntent` при этом не приходит — экземпляр новый.
// Пока разбор стоял в ветке `else`, такой тап открывал обычный
// календарь: ни дня из напоминания о занятии, ни «О программе» с
// готовой кнопкой «Обновить». Повторный разбор того же intent'а при
// обычном пересоздании ничего не сдвигает — [applyNotificationExtras]
// поднимает версию только при изменении полей.
applyNotificationExtras(intent)

setContent {
val deepLinkVersion = notificationDeepLinkVersion
NeiroApp(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ object PastSessionsArchiveCollector {
fun sessionCount(entries: List<String>): Int =
entries.count { raw ->
val session = SessionParser.parse(raw)
!session.isEffectivelyDeleted() && session.status != AttendanceStatus.CANCELLED
// День, в котором осталось только занятое время, не архивируем.
!session.isNotCounted &&
!session.isEffectivelyDeleted() &&
session.status != AttendanceStatus.CANCELLED
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ object UpcomingSessionsCollector {

for (raw in entries) {
val session = SessionParser.parse(raw)
// Запись без услуги — не занятие: напоминать не о чем.
if (session.isNotCounted) continue
// Если сессия удалена целиком (или все дети в интенсиве), пропускаем.
if (session.isEffectivelyDeleted()) continue
if (session.status == AttendanceStatus.CANCELLED) continue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ object PushEventCalendarApplier {
// Интенсивы правит только полный синк — слияние по детям нетривиально (§5.6).
if (SessionParser.isIntensive(raw)) return dayData

// Запись без услуги занятием не становится: сервер про услуги ничего
// не знает, и его событие вернуло бы её в счётчики. Признак ставит
// только разбор записи при синхронизации.
if (SessionParser.parse(raw).isNotCounted) return dayData

// Событие «пришёл» не отменяет уже известную оплату: про `paid_full`
// сервер не знает, и понижение статуса вернуло бы деньги дня в
// ожидаемые до следующего полного синка (01.09.2026).
Expand Down
Loading
Loading