Журнал явок
Что это
Журнал явок — главный экран кадрового учёта. Бухгалтер видит таблицу: в строках — сотрудники, в колонках — дни выбранного периода. В ячейке на пересечении — фактические интервалы прихода/ухода (или короткий буквенный код для отпуска/больничного/прогула).
Любой клик по явке открывает модалку «Окно явки»: слева — подтверждённое время (его правит менеджер), справа — то, что зафиксировала касса (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=33. 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| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
dateFrom | ISO-8601 datetime в UTC | ✅ | Нижняя граница периода (2026-04-01T00:00:00.000Z) |
dateTo | ISO-8601 datetime в UTC | — | Верхняя граница. Если не передана — журнал «до сейчас» |
subdivisionIds | UUID[] | — | OR-фильтр по подразделениям. Допустимы оба формата: ?subdivisionIds=uuid1,uuid2 или ?subdivisionIds=uuid1&subdivisionIds=uuid2 |
positionIds | UUID[] | — | OR-фильтр по должностям |
attendanceTypeIds | UUID[] | — | OR-фильтр по типам явки |
statuses | enum[] | — | OPEN, CLOSED, OVERDUE, PENDING_OVERTIME_APPROVAL, PENDING_PROXY_CARD_APPROVAL, PENDING_OFF_SCHEDULE_APPROVAL |
highlights | enum[] | — | none, yellow, orange, purple, pink — фильтр по цвету |
isManuallyEditedOnly | bool | — | Только отредактированные |
lateOnly | bool | — | Только опоздания |
crossSubdivisionOnly | bool | — | Только явки в «чужом» подразделении |
cashShiftId | UUID | — | Только явки конкретной кассовой смены |
search | string | — | Префикс по табельному + подстрока по ФИО |
sortBy | enum | — | personnelNumber|fullName|position|subdivision|workedHours|earnedAmount (default fullName) |
sortOrder | asc|desc | — | default 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. Чтобы решить, показывать время или буквенный код, фронт держит кэш типов явки:
- На загрузке экрана один раз дёрнуть
GET /staffing/attendance-types→AttendanceTypeResponseDto[](там естьearningRule). - По
attendance.attendanceType.idнайти соответствующий элемент кэша. - Если
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 ?? registeredArrivalTime | confirmedArrivalTime |
| Уход | confirmedDepartureTime ?? registeredDepartureTime ?? «Не закрыта» | confirmedDepartureTime |
| Длительность | Разница Уход − Приход в формате HH:MM | — (фронт пересчитывает в confirmedDepartureTime) |
| Тип явки | attendanceType.name | attendanceTypeId (выбор из селектора) |
| Подразделение | subdivision.name | subdivisionId |
| Должность | position.name | positionId |
| Комментарий (общий, внизу) | comment | comment |
Что показывать справа (Зарегистрированная явка) — read-only
| Поле UI | Откуда |
|---|---|
| Приход | registeredArrivalTime |
| Уход | registeredDepartureTime ?? «Не закрыта» |
| Длительность | Разница (registeredDeparture ?? now) − registeredArrival, формат HH:MM |
| Заработок за явку, ₽ | earnedAmount |
Сценарий 3. Сохранить правки
Запрос
PATCH /staffing/attendances/:idIf-Match: 3Content-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.
| Статус явки | Поле в PATCH | APPROVE → результат | REJECT → результат |
|---|---|---|---|
PENDING_OFF_SCHEDULE_APPROVAL | offScheduleApproval | OPEN или CLOSED (зависит от того, было ли время ухода) | Soft-delete, earnedAmount = 0 |
PENDING_OVERTIME_APPROVAL | overtimeApproval | CLOSED, фактическое время ухода и заработок сохраняются | CLOSED, время ухода обрезается до допустимого порога, заработок пересчитан |
PENDING_PROXY_CARD_APPROVAL | proxyCardApproval | CLOSED или OPEN, флаг прокатки сохраняется в истории | Soft-delete, earnedAmount = 0 |
PATCH /staffing/attendances/:idIf-Match: 3Content-Type: application/json
{ "overtimeApproval": "APPROVE" }Ответ — обновлённая явка с новым status и version.
Сценарий 5. Скачать Excel
Запрос
POST /staffing/attendances/journal/exportContent-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.sheetContent-Disposition: attachment; filename="attendance-journal.xlsx"- Бинарный xlsx.
Скачивание стандартным паттерном: fetch → blob() → 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 + CLOSED | HH:MM – HH:MM (или HH:MM в режиме «Длительность») |
WORK + OPEN | HH:MM – не закрыта |
WORK + OVERDUE | HH: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/positions | id, name, workScheduleType, hourlyRate/salary, allowedOvertimeMinutes, … | name |
| Подразделения | GET /organization/subdivisions | id, name, type, legalEntityId, parentId, … | name |
| Типы явки | GET /staffing/attendance-types | id, name, shortName, earningRule, rateCoefficient | name. Кэшировать целиком — фронт использует earningRule для определения «работа/неявка» (см. выше) |
Пагинации у текущих списков нет — всё возвращается одним массивом. Если когда-то будет, эндпоинт будет принимать ?page / ?limit.
Пресеты периода
Dropdown «За период» — чистый фронт. Бэк знает только dateFrom и dateTo.
| Пресет | dateFrom | dateTo |
|---|---|---|
| Открытый период (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 показывает пометку «перекрыла авто-явку отсутствия».