Перейти к содержимому

Журнал явок

Что это

Журнал явок — главный экран кадрового учёта. Бухгалтер видит таблицу: в строках — сотрудники, в колонках — дни выбранного периода. В ячейке на пересечении — фактические интервалы прихода/ухода (или короткий буквенный код для отпуска/больничного/прогула).

Любой клик по явке открывает модалку «Окно явки»: слева — подтверждённое время (его правит менеджер), справа — то, что зафиксировала касса (read-only), плюс посчитанный заработок.

Журнал — единственный источник правды для зарплаты. Любая правка задним числом — нормальный сценарий и пишется в аудит.

Картина пользователя

┌────────────────────────────────────────────────────────────────────┐
│ Журнал явок 🔄 Обновить 📥 Download .xlsx│
│ 🔍 Поиск Настройки колонок Должности Типы явки Подразделения │
│ За период [Открытый период ▾] С [DD.MM.YYYY] По [DD.MM.YYYY] │
├────────────────────────────────────────────────────────────────────┤
│ Табельный ФИО Должн. Подразд. Часов Зарпл. │
│ ──────── 22.12.25 ────────── 23.12.25 ────────── 24.12.25 ──────── │
│ 5 Иванов И.И. Повар КРИСПИ 16:00 20000 │
│ ▸ 09:25 – не закрыта (фон ⬛ pink — пересечение) │
│ ▸ 09:00 – не закрыта │
│ 018 Юнусова Л.А. Менедж. МАК Орз 8:00 20000 │
│ ▸ 09:59 – не закрыта (текст 🟨 yellow — не закрыта) │
│ 141835 Юсупов И.М. Директ. КРИСПИ 8:00 20000 │
│ ▸ 07:51 – 0:03 – не принята (🟪 purple — PENDING) │
│ ▸ 00:42 – не закрыта │
│ ... │
├────────────────────────────────────────────────────────────────────┤
│ ⬛ Относятся к указанной дате 🟥 Пересечение 🟪 Более 24 ч │
│ 🟧 Отредактированные 🟨 Не закрытые │
└────────────────────────────────────────────────────────────────────┘
↓ клик по явке
┌──────────────────────────────────────────────────────────────┐
│ Окно явки ✕ │
├──────────────────────────────────────────────────────────────┤
│ ПОДТВЕРЖДЁННАЯ ЯВКА (можно править) │ ЗАРЕГИСТРИРОВАННАЯ │
│ │ (read-only) │
│ Приход [DD.MM.YYYY HH:MM 📅] │ Приход 22.12 08:33 │
│ Уход [Не закрыта] │ Уход Не закрыта │
│ Длительность [HH:MM] │ Длит. 05:29 │
│ Тип явки [Отработано 100% ▾] │ Заработок 4200 ₽ │
│ Подразделение [КРИСПИ Луч ▾] │ │
│ Должность [Повар ▾] │ │
├──────────────────────────────────────────────────────────────┤
│ Комментарий [_______________________] │
├──────────────────────────────────────────────────────────────┤
│ [Отмена] [Сохранить] │
└──────────────────────────────────────────────────────────────┘

Базовое

  • Auth. Все запросы — Authorization: Bearer <access_token>. ID корпорации берётся из токена, в параметры передавать не нужно. Получение/обновление токена:
    • POST /auth/login{login, password}{accessToken, refreshToken, ...}.
    • POST /auth/refresh{refreshToken} → новая пара токенов. Вызывать на 401.
    • POST /auth/switch-corporation — для пользователей с несколькими корпорациями.
    • POST /auth/logout — инвалидация сессии.
    • При 401 → попытка /auth/refresh. Если refresh тоже 401 → редирект на login.
  • Время. Все даты/времена в API — ISO-8601 в UTC (2026-04-15T08:00:00.000Z). UI конвертирует в локальный таймзонный режим сам — см. раздел «Часовой пояс».
  • Оптимистическая блокировка. На PATCH/DELETE/close требуется указать ожидаемую версию записи — её сервер сравнивает с актуальной и при расхождении возвращает 409 STALE_VERSION. См. блок ниже.
  • Деньги. Все денежные значения приходят строками ("4200.00"), чтобы не терять точность. Парсить в Decimal-библиотеку фронта.

Оптимистическая блокировка — что и куда передавать

Какая запись правится, узнаём из последнего полученного ответа: у каждой явки в DTO есть целочисленное поле version (стартует с 1, инкрементится сервером после каждого изменения). Это число и есть «версия», которую фронт должен прислать обратно при следующей правке.

Передать её можно одним из двух способов — оба эквивалентны, сервер сначала смотрит заголовок:

Способ 1 — заголовок If-Match (предпочтительно):

If-Match: 3

или, в стиле RFC 7232, с кавычками:

If-Match: "3"

Кавычки сервер снимает сам, регистр не важен. Значение — положительное целое число.

Способ 2 — поле version в теле запроса:

{
"version": 3,
"confirmedArrivalTime": "..."
}

Применяется, когда удобнее запихнуть всё в JSON (например, в DELETE без content-body это сложно — там лучше заголовок).

Жизненный цикл версии

1. GET /attendances/:id → { ..., "version": 3 }
2. UI открывает модалку, помнит → version=3
3. PATCH /attendances/:id → If-Match: 3
→ 200 OK { ..., "version": 4 }
4. UI обновляет своё состояние → version=4 (для следующего PATCH)

Что делать при 409 STALE_VERSION

Кто-то (или другая вкладка) уже изменил эту явку. Просто перезагрузить запись: повторный GET /:id → перерисовать модалку с новым version → пользователь смотрит изменения и пересохраняет. Не пытаться угадать новую версию и повторить PATCH автоматически — можно затереть чужую правку.

Если на старте вообще не передать ни заголовок, ни поле — сервер выполнит операцию без проверки версии. Так делать не надо — в журнале явок параллельные правки реальны (две вкладки бухгалтера, кассовое событие).

Эндпоинты

Все маршруты относительно базового пути API. Префикс — /staffing/attendances.

МетодМаршрутКогда вызывается из UI
GET/journal/by-subdivisionЗагрузка/обновление таблицы журнала
POST/journal/exportКнопка «Download» (xlsx)
POST/timesheet-t13/exportКнопка «Экспорт Т-13» (см. отдельный документ)
GET/:idОткрытие окна явки
PATCH/:idСохранение правок / Подтверждение или отклонение PENDING

В MVP UX создание и удаление явок из журнала не вызываются — явки создаются автоматически (касса прислала событие, либо назначена нерабочая плановая смена). Соответствующие технические эндпоинты существуют, но из таблицы и из модалки их не нужно дёргать.


Сценарий 1. Загрузить таблицу журнала

Запрос

GET /staffing/attendances/journal/by-subdivision?
dateFrom=2026-04-01T00:00:00.000Z&
dateTo=2026-04-30T23:59:59.000Z&
subdivisionIds=uuid1,uuid2&
statuses=OPEN,PENDING_OVERTIME_APPROVAL&
lateOnly=true&
search=Иванов&
sortBy=fullName&
sortOrder=asc
ПараметрТипОбяз.Описание
dateFromISO-8601 datetime в UTCНижняя граница периода (2026-04-01T00:00:00.000Z)
dateToISO-8601 datetime в UTCВерхняя граница. Если не передана — журнал «до сейчас»
subdivisionIdsUUID[]OR-фильтр по подразделениям. Допустимы оба формата: ?subdivisionIds=uuid1,uuid2 или ?subdivisionIds=uuid1&subdivisionIds=uuid2
positionIdsUUID[]OR-фильтр по должностям
attendanceTypeIdsUUID[]OR-фильтр по типам явки
statusesenum[]OPEN, CLOSED, OVERDUE, PENDING_OVERTIME_APPROVAL, PENDING_PROXY_CARD_APPROVAL, PENDING_OFF_SCHEDULE_APPROVAL
highlightsenum[]none, yellow, orange, purple, pink — фильтр по цвету
isManuallyEditedOnlyboolТолько отредактированные
lateOnlyboolТолько опоздания
crossSubdivisionOnlyboolТолько явки в «чужом» подразделении
cashShiftIdUUIDТолько явки конкретной кассовой смены
searchstringПрефикс по табельному + подстрока по ФИО
sortByenumpersonnelNumber|fullName|position|subdivision|workedHours|earnedAmount (default fullName)
sortOrderasc|descdefault asc

Ответ

{
"dateFrom": "2026-04-01T00:00:00.000Z",
"dateTo": "2026-04-30T23:59:59.000Z",
"grandTotals": {
"workedHours": "248.50",
"earnedAmount": "560000.00",
"closedAttendancesCount": 31
},
"rows": [
{
"employee": {
"id": "uuid",
"personnelNumber": "141835",
"firstName": "Ислам",
"lastName": "Юсупов",
"middleName": "Магомедович",
"fullName": "Юсупов И. М."
},
"position": { "id": "uuid", "name": "Директор фастфуда" },
"subdivision": { "id": "uuid", "name": "КРИСПИ Путина 26" },
"totals": {
"workedHours": "8.00",
"earnedAmount": "20000.00",
"closedAttendancesCount": 1
},
"attendances": [ /* массив явок, см. ниже */ ]
}
]
}

Один ряд таблицы = одна тройка (сотрудник × подразделение × должность). Если сотрудник за период работал на двух должностях или в двух подразделениях — это будут разные строки.

totals считаются только по закрытым явкам этой тройки. Открытые / просроченные / PENDING — в часы не идут.

Массив attendances — все явки этой тройки за период. Фронт сам раскладывает их по колонкам-дням, группируя по полю accountingDate (см. ниже).

Что в каждой явке

{
"id": "uuid",
"status": "PENDING_OVERTIME_APPROVAL",
"registeredArrivalTime": "2026-04-15T07:51:00.000Z",
"registeredDepartureTime":"2026-04-15T16:03:00.000Z",
"confirmedArrivalTime": null,
"confirmedDepartureTime": null,
"durationInMinutes": 492,
"earnedAmount": "4200.00",
"calculationStatus": "AUTO_CALCULATED",
"isManuallyEdited": false,
"crossSubdivision": false,
"confirmed": true,
"comment": null,
"highlight": "purple",
"scheduleEntryId": "uuid|null",
"lateMinutes": null,
"accountingDate": "2026-04-15T00:00:00.000Z",
"cashShiftId": "uuid|null",
"overrodeAutoAttendanceId": null,
"isDeleted": false,
"employee": { "id": "...", "firstName": "...", "lastName": "...", "middleName": null },
"position": { "id": "...", "name": "..." },
"subdivision": { "id": "...", "name": "..." },
"attendanceType": { "id": "...", "name": "Отработано", "shortName": "Я" },
"createdAt": "2026-04-15T08:00:00.000Z",
"version": 3,
"warnings": [
{ "code": "OVERTIME_LIMIT_EXCEEDED", "exceededByMinutes": 32 }
]
}

Что фронту делать с полями

ПолеКуда оно идёт в UI
accountingDateГруппировка по колонкам-дням. Формат YYYY-MM-DDT00:00:00.000Z — это UTC-полночь учётного дня подразделения, не локальная полночь. Брать только date-часть (YYYY-MM-DD), таймзонную конвертацию не делать — иначе ночные смены съедут в соседний день
confirmedArrivalTime ?? registeredArrivalTimeВремя в ячейке. Это «эффективное» время — что реально показывать
confirmedDepartureTime ?? registeredDepartureTimeТо же для ухода. Если оба null → «не закрыта»
highlightЦвет ячейки (см. раздел «Цвета и статусы»)
statusТекст метки в ячейке — см. таблицу «Текст в ячейке» в разделе «Цвета и статусы»
attendanceType (null, если тип не назначен — редкий кейс, для PENDING/OVERDUE до подтверждения)Опеределяет, рабочая это явка или неявка. earningRule в этом ref-объекте не приходит — хранить кэш AttendanceType[] из /staffing/attendance-types, маппить по attendanceTypeId (см. ниже)
attendanceType.shortNameДля неявок — короткий буквенный код в ячейке вместо времени
earnedAmountНе показывается в ячейке, отображается только в окне явки и в totals.earnedAmount для строки
crossSubdivisionДополнительная иконка-маркер «явка в чужом подразделении»
lateMinutesМетка «опоздание на N минут» (если > 0)
warnings[]Индикатор ⓘ с пояснением — см. таблицу кодов ниже
versionХранить и отдавать обратно при PATCH через If-Match
overrodeAutoAttendanceIdЕсли не null — пометка «перекрыла авто-явку отсутствия» (например, сотрудник вышел на работу, хотя стоял отпуск)

Как определять «рабочая явка vs неявка»

В payload явки приходит только ссылка attendanceType: {id, name, shortName}без earningRule. Чтобы решить, показывать время или буквенный код, фронт держит кэш типов явки:

  1. На загрузке экрана один раз дёрнуть GET /staffing/attendance-typesAttendanceTypeResponseDto[] (там есть earningRule).
  2. По attendance.attendanceType.id найти соответствующий элемент кэша.
  3. Если earningRule === "WORK" → показываем время; иначе — буквенный код shortName.

Значения earningRule: WORK, SICK_LEAVE, VACATION, BUSINESS_TRIP, ABSENCE, COMPENSATORY, CUSTOM. Всё, кроме WORK — «неявки».

Коды warnings[]

Если attendance.warnings[] непуст — рядом с явкой и в окне явки показать индикатор ⓘ. Тап/наведение — текст из таблицы.

codeЧто значитДополнительные поля
OVERTIME_LIMIT_EXCEEDEDПревышен допустимый лимит переработки по должностиexceededByMinutes — на сколько превышено
CUSTOM_RULE_HAS_NO_FORMULAТип явки с правилом CUSTOM, формула не определена. earnedAmount = 0, нужна ручная правка
OFF_SCHEDULE_AUTO_APPROVEDОткрыта вне расписания, но должность разрешает работу вне графика — факт зафиксирован в истории

Сценарий 2. Открыть «Окно явки»

Запрос

GET /staffing/attendances/:id

Возвращает ту же явку, что и в журнале, плюс массив auditLogs[] — история правок (можно не показывать в MVP, но он есть).

Что показывать слева (Подтверждённая явка)

Все поля editable, кроме «Длительность» — она пересчитывается фронтом и в API не уходит отдельным полем.

Поле UIЧто положить в инпутЧто отправлять в PATCH
ПриходconfirmedArrivalTime ?? registeredArrivalTimeconfirmedArrivalTime
УходconfirmedDepartureTime ?? registeredDepartureTime ?? «Не закрыта»confirmedDepartureTime
ДлительностьРазница Уход − Приход в формате HH:MM— (фронт пересчитывает в confirmedDepartureTime)
Тип явкиattendanceType.nameattendanceTypeId (выбор из селектора)
Подразделениеsubdivision.namesubdivisionId
Должностьposition.namepositionId
Комментарий (общий, внизу)commentcomment

Что показывать справа (Зарегистрированная явка) — read-only

Поле UIОткуда
ПриходregisteredArrivalTime
УходregisteredDepartureTime ?? «Не закрыта»
ДлительностьРазница (registeredDeparture ?? now) − registeredArrival, формат HH:MM
Заработок за явку, ₽earnedAmount

Сценарий 3. Сохранить правки

Запрос

PATCH /staffing/attendances/:id
If-Match: 3
Content-Type: application/json
{
"confirmedArrivalTime": "2026-04-15T08:00:00.000Z",
"confirmedDepartureTime":"2026-04-15T17:00:00.000Z",
"attendanceTypeId": "uuid",
"positionId": "uuid",
"subdivisionId": "uuid",
"comment": "Скорректировано по просьбе менеджера"
}

Отправлять только изменившиеся поля, остальные опустить.

Ответ

200 OK с обновлённой явкой (тот же DTO). У неё isManuallyEdited: true и новый version.

Ручной заработок (опционально)

Если бухгалтер хочет вручную выставить заработок (например, при calculationStatus = PENDING_EXTERNAL_CALCULATION для больничного):

{ "earnedAmount": "12500.50" }

После такого PATCH calculationStatus становится MANUALLY_OVERRIDDEN — автопересчёт по этой явке больше не запускается.

Ошибки

HTTPТело ответаКогдаЧто делать в UI
400{statusCode:400, message}Невалидные данные (например, departure ≤ arrival; смешали PENDING-approval с другими полями PATCH)Подсветить поле, показать message
401стандартный 401Токен истёкПопытка /auth/refresh; при повторном 401 — login
403стандартный 403Чужая корпорацияТоаст «Нет прав»
404стандартный 404Явка не найдена / удаленаЗакрыть модалку, обновить таблицу
409{statusCode:409, code:"STALE_VERSION", message}Версия устарелаТоаст «Запись изменена», кнопка «Обновить» → GET /:id
409{statusCode:409, message:"Журнал явки заблокирован до DATE"} (без поля code)Дата явки попала в заблокированный периодТоаст с датой блокировки из message

Различать два 409по полю body.code: если "STALE_VERSION" — это конфликт версии; если поля нет — другая ConflictException (lock-date или иное).


Сценарий 4. Подтвердить / отклонить PENDING-явку

Если у явки status — один из PENDING_*, в модалке вместо «Сохранить» показываем две кнопки: «Подтвердить» и «Отклонить».

На сервер уходит PATCH /:id только с одним PENDING-полем, без любых других правок (нельзя в одном запросе и подтвердить переработку, и поменять комментарий). Три поля approval взаимоисключающие — комбинировать их между собой тоже запрещено. Нарушение → 400 с сообщением валидатора. Можно отправить только сам approval + If-Match/version.

Статус явкиПоле в PATCHAPPROVE → результатREJECT → результат
PENDING_OFF_SCHEDULE_APPROVALoffScheduleApprovalOPEN или CLOSED (зависит от того, было ли время ухода)Soft-delete, earnedAmount = 0
PENDING_OVERTIME_APPROVALovertimeApprovalCLOSED, фактическое время ухода и заработок сохраняютсяCLOSED, время ухода обрезается до допустимого порога, заработок пересчитан
PENDING_PROXY_CARD_APPROVALproxyCardApprovalCLOSED или OPEN, флаг прокатки сохраняется в историиSoft-delete, earnedAmount = 0
PATCH /staffing/attendances/:id
If-Match: 3
Content-Type: application/json
{ "overtimeApproval": "APPROVE" }

Ответ — обновлённая явка с новым status и version.


Сценарий 5. Скачать Excel

Запрос

POST /staffing/attendances/journal/export
Content-Type: application/json
{
"dateFrom": "2026-04-01T00:00:00.000Z",
"dateTo": "2026-04-30T23:59:59.000Z",
"subdivisionIds": ["uuid"],
"search": "Иванов",
"columns": [
"personnelNumber",
"fullName",
"position",
"subdivision",
"workedHours",
"earnedAmount",
"date:2026-04-15",
"date:2026-04-16",
"date:2026-04-17"
]
}

В теле — те же фильтры, что в GET /journal/by-subdivision (любое подмножество), плюс обязательный массив columns[]:

  • Статические ключи: personnelNumber, fullName, position, subdivision, workedHours, earnedAmount, closedAttendancesCount.
  • Динамические ключи дат: date:YYYY-MM-DD за каждый день, который пользователь хочет видеть в файле.

Ответ

  • Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  • Content-Disposition: attachment; filename="attendance-journal.xlsx"
  • Бинарный xlsx.

Скачивание стандартным паттерном: fetchblob()URL.createObjectURL → клик по скрытой <a download>.

Ошибки экспорта

HTTPКогда
400Некорректный период (dateFrom > dateTo, dateFrom/dateTo отсутствует, период длиннее 366 дней), либо невалидный ключ колонки в columns[]
401 / 403Аутентификация / нет прав
404Подразделение/юрлицо из фильтра не найдены или относятся к чужой корпорации

Тело ошибки — стандартное {statusCode, message}. Большой Excel не блокирует UI: показать индикатор «формируется отчёт» и обрабатывать timeout на стороне фронта (рекомендованно ≥ 60 с).


Цвета и статусы

Подвал журнала (sticky)

Закреплён внизу экрана при скролле:

ЧипЗначениеПоле API
⬛ Относятся к указанной датеБазовое состояние ячейки (явка в своей дате)highlight = none
🟥 ПересечениеЯвка пересекается с другой неудалённой того же сотрудникаhighlight = pink
🟪 Более 24 часовПросрочка (OVERDUE), любой PENDING_*, или открытая дольше 24 чhighlight = purple
🟧 ОтредактированныеЗакрытая, кто-то поправил рукамиhighlight = orange
🟨 Не закрытыеОткрытая менее 24 чhighlight = yellow

Поле highlight приходит уже посчитанным. Фронту не нужно повторять логику — просто маппить enum → цвет.

Приоритет (если совпало несколько условий, сервер выбирает первый):

pink > purple > orange > yellow > none

Текст в ячейке

Сначала смотрим на тип явки (используя кэш AttendanceType[]), потом — на статус.

УсловиеЧто писать в ячейке
earningRule ≠ WORK (любой тип-«неявка»)Краткий код из attendanceType.shortNameБ, О, П, К. Время не показываем
WORK + CLOSEDHH:MM – HH:MM (или HH:MM в режиме «Длительность»)
WORK + OPENHH:MM – не закрыта
WORK + OVERDUEHH:MM – HH:MM ⚠️ авто-закрыта (явка реально закрыта планировщиком — confirmedDepartureTime есть, но это автомат, не сотрудник)
WORK + любой PENDING_*HH:MM – HH:MM – не принята

Статусы явки

СтатусЧто это значит
OPENСотрудник пришёл, но ещё не закрыл смену
CLOSEDСмена закрыта, заработок посчитан
OVERDUEСотрудник забыл закрыть, система авто-закрыла после превышения лимита переработки
PENDING_OVERTIME_APPROVALЗакрыта с переработкой больше допустимого — ждёт решения менеджера
PENDING_PROXY_CARD_APPROVALОткрыта по чужой карте — ждёт решения менеджера
PENDING_OFF_SCHEDULE_APPROVALОткрыта вне расписания — ждёт решения менеджера

Статус расчёта заработка

calculationStatusЧто это значит
AUTO_CALCULATEDСервер посчитал автоматически (ставка × часы × коэффициент типа явки)
PENDING_EXTERNAL_CALCULATIONЭто нерабочая явка (больничный/отпуск/командировка), формула отложена в зарплатную подсистему
MANUALLY_OVERRIDDENБухгалтер выставил сумму руками — автопересчёт по этой явке не запускается

Селекторы (откуда подгружать значения dropdown’ов)

Все запросы возвращают полный объект сущности — фронт показывает в dropdown’е name, а в PATCH/фильтры отправляет id. Удалённые (isDeleted: true) отфильтровываются автоматически или передачей ?showDeleted=false.

СелекторЭндпоинтЧто приходитЧто показать в UI
ДолжностиGET /staffing/positionsid, name, workScheduleType, hourlyRate/salary, allowedOvertimeMinutes, …name
ПодразделенияGET /organization/subdivisionsid, name, type, legalEntityId, parentId, …name
Типы явкиGET /staffing/attendance-typesid, name, shortName, earningRule, rateCoefficientname. Кэшировать целиком — фронт использует earningRule для определения «работа/неявка» (см. выше)

Пагинации у текущих списков нет — всё возвращается одним массивом. Если когда-то будет, эндпоинт будет принимать ?page / ?limit.

Пресеты периода

Dropdown «За период» — чистый фронт. Бэк знает только dateFrom и dateTo.

ПресетdateFromdateTo
Открытый период (default)начало текущего месяцане передаём → бэк интерпретирует как «сейчас»
Текущая неделяпонедельник этой недели 00:00воскресенье 23:59:59
Текущий месяц1-е число месяца 00:00последний день месяца 23:59:59
Произвольныйзначение поля «С»значение поля «По»

Настройки колонок и режим отображения ячейки

Оба контрола — чистый фронт, бэк всегда возвращает полный объект.

«Настройки колонок»

Пользователь выбирает, какие колонки показывать в таблице:

  • Статические: Табельный, ФИО, Должность, Подразделение, Часов, Зарплата.
  • Динамические: какие даты периода показывать.

Persistence на стороне фронта (localStorage или серверные user preferences — на усмотрение команды).

При экспорте в Excel этот же набор колонок передаётся в columns[] запроса /journal/export — чтобы файл совпадал с тем, что на экране.

«Часы работы» vs «Длительность»

Способ показа времени в ячейке (как в iiko):

РежимВид ячейки
Часы работы16:00 – 22:00
Длительность6:00

Оба значения уже есть в ответе API — фронт сам решает, какое выводить.


Часовой пояс и формат времени

  • Все даты в API — ISO-8601 UTC (...T...Z).
  • В UI показываем в часовом поясе корпорации — поле timezone в CorporationResponseDto (формат IANA, например Europe/Moscow). Получить: GET /organization/corporations/current или соответствующий эндпоинт настроек.
  • В SubdivisionResponseDto поля timezone сейчас нет — фактически таймзону хранит и применяет сервер, фронту достаточно корпоративной для отображения. Если когда-то понадобится разный TZ на подразделение, бэк добавит поле — фронт берёт его с приоритетом, fallback на корпоративный.
  • Поле accountingDate — это UTC-полночь учётного дня подразделения (YYYY-MM-DDT00:00:00.000Z). Используется только для группировки по колонкам-дням. Брать только date-часть (YYYY-MM-DD), не конвертировать в локальный TZ — иначе ночные смены съедут в соседний день.
  • Учётный день начинается с настраиваемого часа корпорации (по умолчанию 06:00). Ночные смены сами попадают в свой учётный день — фронту считать это не нужно.

Открытые вопросы (уточнить с дизайнером / PM)

  • Persistence «Настроек колонок» и режима ячейкиlocalStorage или серверные user preferences.
  • Визуальный паттерн warnings[] — иконка ⓘ / цветной dot / hover-tooltip / toast.
  • Сортировка по дневной колонке — сервер сортирует только по статическим колонкам. Стрелочки на заголовках дат — либо убрать, либо делать client-side в пределах загруженной страницы.
  • Кассовая смена как фильтр UI — в API параметр cashShiftId принимается, но в текущем макете шапки селектора нет. Зарезервировано на будущее.

Известные расхождения и план доработок

  • ✅ Семантика registered/confirmed на стороне офиса работает (LOCALIOFFICE-987 закрыта). На стороне кассы дорабатывается в LOCALIKASSA-284 — до её закрытия registeredDepartureTime может приходить под старым именем поля.
  • crossSubdivision, lateMinutes, scheduleEntryId, cashShiftId, confirmed, calculationStatus — все в ответе (LOCALIOFFICE-991, LOCALIOFFICE-992, LOCALIOFFICE-993, LOCALIOFFICE-999 закрыты).
  • ✅ Resolve PENDING (overtime / proxy card / off-schedule) — все три типа поддержаны единым PATCH (LOCALIOFFICE-997).
  • highlight рассчитывается сервером, фронту достаточно маппить enum → цвет.
  • ⚠️ Коэффициент типа явки в earnedAmount уже применяется (LOCALIOFFICE-988), но в окне явки коэффициент пока не показан отдельной строкой. Можно добавить позже.
  • ⚠️ Дата блокировки журнала на корпорации (LOCALIOFFICE-989): PATCH за дату до блокировки → 409 без code (различать со STALE_VERSION по полю body.code). Фронт показывает тоаст с датой блокировки из текста ошибки.
  • ⚠️ В SubdivisionResponseDto пока нет поля timezone — для UI-конвертации фронт берёт корпоративный timezone из CorporationResponseDto. Если в будущем понадобится таймзона на подразделение, бэк добавит — фронт переключится.
  • ⚠️ requiresManualAttendanceConfirmation корпорации — поле есть, в расчёте не учитывается. Будет активировано отдельной задачей; в MVP игнорируем.
  • ⚠️ overrodeAutoAttendanceId (LOCALIOFFICE-996): если не пустое — UI показывает пометку «перекрыла авто-явку отсутствия».