meraproject/docs/user-reader-api.md
keboss-m 9fa9d5e3ed Add project_id to read API responses and remove duplicate id fields.
Expose project_id across labor and calendar endpoints for unambiguous project matching. Use only project_id in /api/projects and /api/project-report; update docs, UI tables, and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 19:01:33 +03:00

962 lines
33 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# User Reader — API и доступ
Справочник по HTTP API микросервиса **user-reader**: адреса, аутентификация, эндпоинты, параметры, ответы и типовые сценарии вызова.
Общая документация по развёртыванию и модели данных: [DEVELOPERS.md](../DEVELOPERS.md).
Интерактивная схема: **`/docs`** (Swagger), **`/redoc`**.
---
## Содержание
1. [Базовый адрес](#базовый-адрес)
2. [Доступ и заголовки](#доступ-и-заголовки)
3. [Матрица доступа к эндпоинтам](#матрица-доступа-к-эндпоинтам)
4. [Общие правила работы с API](#общие-правила-работы-с-api)
5. [Служебные эндпоинты](#1-служебные-эндпоинты)
6. [Сотрудники](#2-сотрудники)
7. [Трудозатраты (чтение)](#3-трудозатраты-чтение)
8. [Табель (чтение)](#4-табель-чтение)
9. [Табель (запись)](#5-табель-запись)
10. [Ошибки и коды ответа](#ошибки-и-коды-ответа)
11. [Типовые сценарии](#типовые-сценарии)
12. [Примеры запросов](#примеры-запросов)
---
## Базовый адрес
| Окружение | Base URL |
|-----------|----------|
| Docker Compose (хост) | `http://localhost:8090` |
| Docker Compose (другой контейнер) | `http://user-reader:8090` |
| Прод / свой сервер | `http://<хост>:<порт>` |
Все пути API начинаются с **`/api/`**.
Кодировка: **UTF-8**, формат тела: **JSON** (кроме GET без тела).
---
## Доступ и заголовки
### Уровень 1 — service-to-service (API-ключ)
На сервере задаётся переменная окружения **`USER_READER_API_KEY`**.
| Состояние ключа | Поведение |
|-----------------|-----------|
| Непустая строка | Все «защищённые» эндпоинты требуют ключ |
| Пустая строка | Проверка отключена (только для локальной отладки) |
Передать ключ одним из способов:
```http
X-Api-Key: <ваш-ключ>
```
или
```http
Authorization: Bearer <ваш-ключ>
```
При отсутствии или неверном ключе: **`401 Unauthorized`**.
```json
{
"detail": "Требуется ключ: заголовок X-Api-Key или Authorization: Bearer <ключ>"
}
```
Локальный ключ в `docker-compose.yml` (сменить на проде): **`local-dev-key-change-in-prod`**.
### Уровень 2 — идентичность сотрудника (табель)
Для эндпоинтов табеля (чтение календаря, запись часов и отсутствий) **дополнительно** нужен заголовок:
```http
X-Acting-Emp-Id: <целое число > 0>
```
Это id сотрудника в `tMerakomisEmp`, от имени которого выполняется операция (аналог cookie `fg_emp_id` в PHP).
Опционально в query или JSON-теле:
| Поле | Смысл |
|------|--------|
| `emp_id` | За кого операция; если не передан — используется `X-Acting-Emp-Id` |
Права (админ, РП, делегат…) проверяются на сервере. Перед записью можно вызвать **`GET /api/labor/permissions`**.
### Сводка заголовков
| Заголовок | Когда обязателен |
|-----------|------------------|
| `X-Api-Key` или `Authorization: Bearer` | Все `/api/*`, кроме `/api/health`, если ключ задан на сервере |
| `X-Acting-Emp-Id` | Эндпоинты табеля с пометкой **Acting** |
| `Content-Type: application/json` | `PUT` с телом |
### CORS
Сервис отдаёт `Access-Control-Allow-Origin: *`. Браузерные клиенты с другого домена могут вызывать API при наличии ключа.
---
## Матрица доступа к эндпоинтам
| Метод | Путь | API-ключ | X-Acting-Emp-Id |
|-------|------|:--------:|:---------------:|
| GET | `/api/health` | — | — |
| GET | `/api/meta` | ✓ | — |
| GET | `/api/employees` | ✓ | — |
| GET | `/api/employees/delta` | ✓ | — |
| GET | `/api/labor/meta` | ✓ | — |
| GET | `/api/projects` | ✓ | — |
| GET | `/api/sections` | ✓ | — |
| GET | `/api/project-members` | ✓ | — |
| GET | `/api/project-sections` | ✓ | — |
| GET | `/api/member-roles` | ✓ | — |
| GET | `/api/time-entries` | ✓ | — |
| GET | `/api/labor-summary` | ✓ | — |
| GET | `/api/work-report` | ✓ | — |
| GET | `/api/project-report` | ✓ | — |
| GET | `/api/calendar-days` | ✓ | — |
| GET | `/api/absence-types` | ✓ | — |
| GET | `/api/labor/permissions` | ✓ | ✓ |
| GET | `/api/time-summary` | ✓ | ✓ |
| GET | `/api/time-calendar` | ✓ | ✓ |
| PUT | `/api/time-entries` | ✓ | ✓ |
| PUT | `/api/project-members` | ✓ | ✓ |
| PUT | `/api/absences` | ✓ | ✓ |
| PUT | `/api/absences/range` | ✓ | ✓ |
✓ — обязателен, если на сервере включён соответствующий режим (ключ / табель).
### Пакетные запросы и `fetch_all`
| Метод | Путь | API-ключ | Описание |
|-------|------|:--------:|----------|
| POST | `/api/batch` | ✓ | До 25 подзапросов GET в одном HTTP-вызове |
Параметр **`fetch_all=true`** (query) на списках и отчётах возвращает **все** строки за один запрос (лимит 50000). Поддерживается на:
`/api/employees`, `/api/project-members`, `/api/time-entries`, `/api/work-report`, `/api/labor-summary`, `/api/project-report`.
---
## Общие правила работы с API
### Даты
- В query и JSON: **`YYYY-MM-DD`** (например `2026-06-10`).
- В ответах поля `date`, `date_from`, `date_to` — в том же формате.
- Поля `datetime` из БД — ISO-строка с пробелом: `2026-06-10 14:30:00`.
### Пагинация
Большинство списков поддерживают `limit` и `offset`.
**`fetch_all=true`** — вернуть все строки одним ответом (без цикла `offset`). Максимум **50000** строк; при превышении — `400 too_many_rows`. Рекомендуется для `/api/work-report` за месяц/квартал.
```text
GET /api/work-report?date_from=2026-01-01&date_to=2026-03-31&fetch_all=true
```
Классическая пагинация:
```text
offset = 0
loop:
GET …?limit=L&offset=offset
обработать items
offset += count
пока offset < total
```
В ответе обычно:
| Поле | Смысл |
|------|--------|
| `total` | Всего записей (до пагинации) |
| `limit`, `offset` | Эхо запроса |
| `count` | Размер массива `items` в текущем ответе (не у всех эндпоинтов) |
| `items` | Массив строк |
### Часы и переработки
В агрегатах (`work-report`, `labor-summary`, `project-report`):
| Поле | Смысл |
|------|--------|
| `hours` | Рабочие часы (`is_over = 0`) |
| `over1` | Переработка в рабочий день |
| `over2` | Переработка в выходной/праздник или день отсутствия |
| `over` | `over1 + over2` |
| `total` | `hours + over` |
В сырых записях `/api/time-entries`: поле `is_over``0` или `1`.
### Стадия проекта
| Поле | Где | Смысл |
|------|-----|--------|
| `step` | `/api/projects` | Числовой id стадии |
| `step_name` | Эндпоинты с `project_name` | Краткое название: ПД, РД, К… |
| `status` | `/api/projects`, эндпоинты с `project_name` | Числовой код статуса (0…3) |
| `status_name` | Эндпоинты с `project_name` | Подпись: «В работе», «Завершён»… |
| `archive` | `/api/projects`, эндпоинты с `project_name` | `0` — активный, `1` — в архиве |
| `archive_date` | `/api/projects`, эндпоинты с `project_name` | Дата архивации (`YYYY-MM-DD`) или `null` |
### Id проекта (`project_id`)
Во всех ответах, где есть **`project_code`**, **`project_name`** или поле **`project`** (сырой табель), дополнительно отдаётся **`project_id`** — числовой id из `tMerakomisProject`. Используйте его для однозначной идентификации проекта (код может повторяться в истории).
| Эндпоинт | Поле с id проекта |
|----------|-------------------|
| `GET /api/projects` | **`project_id`** |
| `GET /api/project-members`, `GET /api/labor-summary` | **`project_id`** |
| `GET /api/work-report` | **`project_id`** (`id` — это `emp_id`) |
| `GET /api/project-report` | **`project_id`** |
| `GET /api/time-entries` | **`project_id`** и `project` (оба — id проекта) |
| `GET /api/time-calendar` | **`project_id`** в корне ответа (если фильтр по проекту) |
| `GET /api/time-summary` | **`project_id`** в элементах массива `project[]` |
Коды **`status`** (порт `eStatus.php`):
| `status` | `status_name` |
|----------|---------------|
| `0` | Не определён |
| `1` | В работе |
| `2` | Завершён |
| `3` | Пауза |
### Секретные данные
Пароли и поля с `password` / `secret` / суффиксом `_pass` **не возвращаются** в API сотрудников.
---
## 1. Служебные эндпоинты
### `GET /api/health`
Проверка живости и подключения к MySQL. **Ключ не нужен.**
**Ответ `200`:**
```json
{
"ok": true,
"db": true,
"database": "j7508239_tracker"
}
```
При ошибке БД: `ok: false`, `db: false`, поле `error` со строкой.
**Использование:** health-check в оркестраторе, перед массовой синхронизацией.
---
### `GET /api/meta`
Метаданные таблицы сотрудников для клиента.
**Query:** нет.
**Ответ `200` (основные поля):**
| Поле | Описание |
|------|----------|
| `schema_mode` | `merakomis_emp` или `generic_user_table` |
| `physical_table` | Имя таблицы в MySQL |
| `delta_field` | Имя поля в `items` для инкремента (`updated` и т.п.) |
| `selected_fields` | Список полей в `/api/employees` |
| `where_removed` | Колонка фильтра удалённых (если есть) |
**Использование:** один раз при старте интеграции — понять схему и поле для delta.
---
### `GET /api/labor/meta`
Имена физических таблиц Merakomis (проекты, время, отсутствия).
**Ответ `200`:**
```json
{
"database": "…",
"tables": {
"project": "tmerakomisproject",
"section": "…",
"team_member": "…",
"time": "…",
"day": "…",
"time_absence": "…"
},
"overtime": { "is_over_field": "is_over", "over1": "…", "over2": "…" },
"section_source": "tMerakomisTeamMember.section через project.team"
}
```
---
## 2. Сотрудники
### `GET /api/employees`
Постраничный список сотрудников.
| Query | По умолчанию | Ограничения |
|-------|--------------|-------------|
| `limit` | `100` | 1…500 |
| `offset` | `0` | ≥ 0 |
**Ответ `200`:**
| Поле | Описание |
|------|----------|
| `total` | Всего записей (с учётом фильтра `removed = 0` в Merakomis) |
| `items[]` | Объекты сотрудника |
Дополнительные поля в `items` (Merakomis):
| Поле | Описание |
|------|----------|
| `id`, `name`, `login`, `email`, … | См. `/api/meta``selected_fields` |
| `department` | Коды отдела через запятую: `ОВ`, `АР`… |
| `department_codes` | Массив кодов |
| `departments` | `{ department_id, name, short, code }` |
| `staffing` | Сырое значение из БД: JSON-массив id должностей |
| `staffing_ids` | Массив id из `tMerakomisDStaffing` |
| `staffing_names` | Названия должностей: Архитектор, ГИП, ГАП… |
| `staffing_title` | Должности одной строкой через запятую |
| `staffings` | `{ id, name, text }``text` часто аббревиатура (ГИП, ГАП) |
Поле **`roles`** в Merakomis — это не должность, а служебный фильтр «подразделение» в UI портала; оргструктура в API — через `departments` / `department`.
---
### `GET /api/employees/delta`
Инкрементальная выгрузка: только изменённые записи.
| Query | По умолчанию | Описание |
|-------|--------------|----------|
| `since_updated` | `0` | Unix timestamp (секунды); выбираются строки с `updated > since_updated` |
| `limit` | `500` | 1…500 |
**Ответ `200`:**
| Поле | Описание |
|------|----------|
| `since_updated` | Эхо запроса |
| `max_updated` | Максимум `updated` среди возвращённых строк — передать в следующий запрос |
| `delta_column` | Имя поля времени в `items` |
| `count` | Число элементов |
| `items` | Те же поля, что в `/api/employees` |
**Алгоритм синхронизации:**
```text
since = сохранённый_у_себя_маркер # 0 при первом запуске
повторять:
r = GET /api/employees/delta?since_updated={since}&limit=500
upsert каждого r.items по id
since = r.max_updated
пока r.count == 500
сохранить since
```
---
## 3. Трудозатраты (чтение)
### `GET /api/projects`
Справочник проектов.
| Query | По умолчанию | Описание |
|-------|--------------|----------|
| `limit` | `100` | 1…500 |
| `offset` | `0` | |
| `include_removed` | `false` | Включать удалённые |
| `include_archive` | `true` | Не скрывать архивные |
**Поля `items`:** **`project_id`**, `code`, `name`, `director`, `step`, `status`, `status_name`, `team`, `archive`, `archive_date`, `removed`, `date`, `date_end`.
---
### `GET /api/sections`
Справочник разделов проекта.
| Query | Описание |
|-------|----------|
| `limit` | 1…1000 (по умолчанию 500) |
| `offset` | Пагинация |
| `step` | Фильтр по полю `step` раздела |
**Поля `items`:** `id`, `name`, `parent`, `step`.
---
### `GET /api/project-members`
Участники команд: сотрудник ↔ проект ↔ раздел.
| Query | По умолчанию | Описание |
|-------|--------------|----------|
| `project_id` | — | Фильтр |
| `emp_id` | — | Фильтр |
| `active_only` | `true` | Только активные в команде |
| `limit` | `100` | 1…500 |
| `offset` | `0` | |
**Поля `items`:**
| Поле | Описание |
|------|----------|
| `member_id` | id TeamMember |
| `emp_id`, `emp_name` | Сотрудник |
| `project_id`, `project_code`, `project_name` | Проект |
| `step_name` | Стадия (ПД, РД…) |
| `status`, `status_name` | Статус проекта |
| `archive`, `archive_date` | Архив и дата архивации |
| `section_id`, `section_name` | Раздел в команде |
| `role`, `active` | Роль и активность |
**Использование:** список проектов сотрудника для UI, проверка членства перед записью часов.
---
### `GET /api/project-sections`
Разделы, настроенные для проекта (`tMerakomisProjectSection`).
| Query | Обязательно |
|-------|:-----------:|
| `project_id` | да |
**Ответ `200`:** `project_id`, `total`, `items[]``section_id`, `section_name`.
**Ошибки:** `404` — проект не найден.
---
### `GET /api/member-roles`
Справочник ролей в команде (ГИП, ГАП, Сотрудник…).
| Query | Описание |
|-------|----------|
| `role` | Включить архивную роль (для формы редактирования) |
**Ответ:** `total`, `items[]``id`, `title`.
---
### `GET /api/time-entries`
Сырые строки табеля без агрегации.
| Query | По умолчанию | Описание |
|-------|--------------|----------|
| `date_from`, `date_to` | — | Период `YYYY-MM-DD` |
| `emp_id`, `project_id` | — | Фильтры |
| `is_over` | — | `0` / `1` |
| `limit` | `500` | 1…2000 |
| `offset` | `0` | |
**Поля `items`:** `id`, `emp`, **`project_id`**, `project`, `date`, `duration`, `is_over`.
Ошибка `400`, если `date_from > date_to`.
---
### `GET /api/work-report` ⭐ рекомендуемый для интеграции
Агрегат за период: **сотрудник × проект** + отдел + раздел + стадия + часы.
| Query | Обязательно | По умолчанию | Описание |
|-------|:-----------:|--------------|----------|
| `date_from` | да | — | Начало периода |
| `date_to` | да | — | Конец периода (включительно) |
| `emp_id` | | — | Фильтр |
| `project_id` | | — | Фильтр |
| `limit` | | `500` | 1…2000 |
| `offset` | | `0` | Пагинация |
**Поля `items`:**
| Поле | Тип | Описание |
|------|-----|----------|
| `id` | int | `emp_id` |
| `employee` | string | ФИО |
| `department` | string \| null | Орготдел |
| `staffing_title` | string \| null | Должность (Архитектор, ГИП…) |
| `section` | string \| null | Раздел в проекте |
| **`project_id`** | int | Id проекта Merakomis |
| `project_code` | string | |
| `step_name` | string | Стадия |
| `status` | int \| null | Код статуса проекта |
| `status_name` | string \| null | Подпись статуса |
| `archive` | int | `0` / `1` |
| `archive_date` | string \| null | Дата архивации |
| `project_name` | string | |
| `hours`, `over`, `over1`, `over2`, `total` | number | Часы |
**Особенности:**
- Нет delta — каждый запрос пересчитывает весь период.
- Раздел берётся из членства в команде, не из строки времени.
- При большом объёме обходите `offset` до `total`.
---
### `GET /api/project-report`
Агрегат: **проект × отдел × раздел** (сумма по всем сотрудникам).
| Query | Обязательно | Описание |
|-------|:-----------:|----------|
| `date_from`, `date_to` | да | Период |
| `project_id` | | Фильтр |
| `limit`, `offset` | | 1…2000 |
**Поля `items`:** **`project_id`**, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `project_name`, `department`, `section`, `hours`, `over`, `over1`, `over2`, `total`.
---
### `GET /api/labor-summary`
Тот же расчёт, что `work-report`, но с внутренними id Merakomis. Для внешних систем предпочтительнее **`/api/work-report`**.
| Query | Обязательно |
|-------|:-----------:|
| `date_from`, `date_to` | да |
| `emp_id`, `project_id`, `limit`, `offset` | опционально |
**Поля `items`:** `emp_id`, `emp_name`, `department`, `staffing_title`, `project_id`, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `project_name`, `section_id`, `section_name`, `role`, `hours`, `over`, `over1`, `over2`, `total`.
---
## 4. Табель (чтение)
Все эндпоинты ниже требуют **`X-Acting-Emp-Id`** и API-ключ.
### `GET /api/calendar-days`
Производственный календарь (рабочие/выходные дни).
| Query | Обязательно |
|-------|:-----------:|
| `date_from`, `date_to` | да |
**Ответ:** `date_from`, `date_to`, `items[]` — дни с типом и подписью.
---
### `GET /api/absence-types`
Справочник типов отсутствий.
**Ответ:** `items[]` — всегда есть элемент `{ "id": 0, "title": "—", "code": "" }` для снятия отсутствия.
---
### `GET /api/labor/permissions`
Проверка прав до записи.
| Query | Обязательно | Описание |
|-------|:-----------:|----------|
| `target_emp_id` | да | За кого проверяем |
| `project_id` | | Для `can_write_time` |
**Ответ `200`:**
| Поле | Описание |
|------|----------|
| `can_read` | Чтение табеля |
| `can_write_time` | Запись часов в `project_id` (false, если `project_id` не передан) |
| `can_write_absence` | Запись отсутствий |
| `can_write_member` | Запись состава проекта (`PUT /api/project-members`) |
| `is_admin` | Администратор |
| `is_delegate_writer` | Делегат на запись чужого табеля |
Дополнительные query: `member_id` (опционально) — для уточнения `can_write_member` при редактировании.
---
### `GET /api/time-calendar`
Данные календаря табеля (формат, близкий к PHP `getTimeTable`).
| Query | По умолчанию | Описание |
|-------|--------------|----------|
| `emp_id` | acting | Чей табель |
| `project_id` | `0` | `0` — сводный; иначе — по проекту |
**Ответ (ключевые поля):** `can_edit`, `dates`, `days`, `absence`, `absence_options`, `project`, `total`, `acting_emp_id`, `target_emp_id`.
Ошибки: `403` нет прав, `404` проект не найден.
---
### `GET /api/time-summary`
Сводка часов за **текущий месяц** и **текущий год** для сотрудника.
| Query | По умолчанию |
|-------|--------------|
| `emp_id` | acting |
**Ответ:** объекты `month`, `year` с блоками `project`, `total`, `absence`.
---
## 5. Табель (запись)
Требуются **API-ключ** и **`X-Acting-Emp-Id`**.
Тело запросов: **`Content-Type: application/json`**.
Детали бизнес-логики и портирования PHP: [change-proposal-labor-api-write.md](change-proposal-labor-api-write.md).
### `PUT /api/time-entries`
Запись или обновление часов за один день по одному проекту.
**Тело:**
```json
{
"emp_id": 46,
"project_id": 86,
"date": "2026-06-10",
"time": 8,
"over": 0
}
```
| Поле | Обязательно | Описание |
|------|:-----------:|----------|
| `project_id` | да | id проекта |
| `date` | да | `YYYY-MM-DD` |
| `time` | да | Часы, 0…24 |
| `over` | | `0` — рабочие (по умолчанию), `1` — переработка |
| `emp_id` | | Целевой сотрудник; иначе = acting |
`time: 0`**удаляет** запись за этот день и тип (`over`).
**Ответ `200`:**
```json
{
"ok": true,
"acting_emp_id": 46,
"target_emp_id": 46,
"id": 12345,
"duration": 8,
"is_over": 0,
"info": { "hours": 8, "over": 0 },
"limits": { },
"cache_rows_deleted": 0
}
```
Ошибки: `403` нет прав, `404` сотрудник не найден, `400` невалидная дата.
---
### `PUT /api/project-members`
Добавление или обновление участника команды проекта (upsert по паре `project_id` + `emp_id`).
**Тело:**
```json
{
"project_id": 86,
"emp_id": 46,
"section_id": 12,
"role": 22,
"active": true,
"text": ""
}
```
| Поле | Обязательно | Описание |
|------|:-----------:|----------|
| `project_id` | да | id проекта |
| `emp_id` | да | id сотрудника |
| `section_id` | да | id из `GET /api/project-sections` |
| `role` | да | id из `GET /api/member-roles` |
| `active` | | По умолчанию `true`; `false` — деактивация в команде |
| `text` | | Комментарий |
**Ответ `200`:** `ok: true` и поля как у элемента `GET /api/project-members` (`member_id`, `emp_name`, `project_code`, `step_name`, `status`, `status_name`, `archive`, `archive_date`, `section_name`, `role`, `active`).
Ошибки: `403` нет прав (`Rules::isRwMember`), `404` проект/сотрудник, `400` невалидный раздел, роль или архивный сотрудник.
**Превью UI:** `GET /project-member`
---
### `PUT /api/absences`
Установка или снятие отсутствия по списку дат.
**Тело:**
```json
{
"emp_id": 46,
"type": 3,
"dates": ["2026-06-10", "2026-06-11"]
}
```
| Поле | Описание |
|------|----------|
| `type` | id из `/api/absence-types`; `0` — снять отсутствие |
| `dates` | Массив дат, минимум одна |
При `type != 0` рабочие часы (`is_over = 0`) за эти дни **удаляются**.
**Ответ `200`:** `ok`, `results[]` с `date`, `absence_type_id`, `cache_rows_deleted`.
---
### `PUT /api/absences/range`
Массовая операция за диапазон дат.
**Тело:**
```json
{
"emp_id": 46,
"absence_id": 3,
"begin": "2026-06-10",
"end": "2026-06-20"
}
```
| Поле | Описание |
|------|----------|
| `absence_id` | `0` — очистить диапазон (удалить отсутствия и рабочие часы) |
| `begin`, `end` | Границы включительно; `end >= begin` |
**Ответ `200`:** `ok`, `dates_count`, `m`: `"Успешно сохранено"`.
---
## 6. Пакетные запросы (`POST /api/batch`)
Один HTTP-вызов — несколько **GET** к `/api/*`. Снижает накладные расходы при выгрузке нескольких периодов или эндпоинтов.
**Заголовки:** `X-Api-Key` (обязателен, если настроен); `X-Acting-Emp-Id` пробрасывается в подзапросы.
**Тело:**
```json
{
"requests": [
{
"id": "q1",
"method": "GET",
"path": "/api/work-report?date_from=2026-01-01&date_to=2026-01-31&fetch_all=true"
},
{
"id": "q2",
"method": "GET",
"path": "/api/work-report?date_from=2026-02-01&date_to=2026-02-28&fetch_all=true"
}
]
}
```
| Поле | Описание |
|------|----------|
| `requests` | 1…25 подзапросов |
| `id` | Произвольный идентификатор для сопоставления ответов |
| `method` | Только `GET` |
| `path` | Относительный путь `/api/...` с query |
**Ответ `200`:**
```json
{
"ok": true,
"count": 2,
"results": [
{ "id": "q1", "status": 200, "ok": true, "path": "…", "body": { } },
{ "id": "q2", "status": 200, "ok": true, "path": "…", "body": { } }
]
}
```
`ok` на верхнем уровне — `true`, только если **все** подзапросы успешны.
Ограничения: рекурсивный `/api/batch` запрещён; абсолютные URL запрещены.
---
## Ошибки и коды ответа
| HTTP | Когда | Формат `detail` |
|------|--------|-----------------|
| `400` | Невалидные параметры, даты, нет `X-Acting-Emp-Id`, delta без колонки времени | Строка или `{ "code", "message" }` |
| `401` | Нет/неверный API-ключ | Строка |
| `403` | Нет прав на табель | `{ "code": "forbidden", "message": "…" }` |
| `404` | Сотрудник/проект не найден | `{ "code": "…_not_found", "message": "…" }` |
| `500` | Ошибка БД, отсутствует таблица Merakomis | Строка или `{ "code": "db_error", "message" }` |
**Рекомендации клиенту:**
- На `401` — проверить ключ и переменную `USER_READER_API_KEY` на сервере.
- На `403` — вызвать `/api/labor/permissions` или проверить `X-Acting-Emp-Id`.
- На `500` — повтор с backoff; при `DEBUG=1` на сервере в теле может быть traceback (не для прода).
---
## Типовые сценарии
### Проверка перед интеграцией
```text
1. GET /api/health
2. GET /api/meta (с ключом)
3. GET /api/labor/meta (с ключом)
```
### Ежедневная синхронизация сотрудников
```text
GET /api/employees/delta?since_updated={stored}&limit=500
→ upsert по id
→ stored = max_updated
```
### Отчёт за месяц в BI/другой сервис
```text
GET /api/work-report?date_from=2026-05-01&date_to=2026-05-31&fetch_all=true
```
Или несколько месяцев одним HTTP:
```text
POST /api/batch
→ work-report за январь, февраль, … с fetch_all=true
```
### Запись часов из внешнего UI
```text
1. GET /api/project-members?emp_id={acting}&active_only=true
2. GET /api/labor/permissions?target_emp_id={target}&project_id={pid}
→ если can_write_time
3. PUT /api/time-entries { project_id, date, time, over }
```
### Сводка по проектам для руководства
```text
GET /api/project-report?date_from=…&date_to=…&limit=2000
```
### Управление составом проекта
```text
1. GET /api/project-sections?project_id={pid}
2. GET /api/member-roles
3. GET /api/labor/permissions?target_emp_id={acting}&project_id={pid}
→ если can_write_member
4. PUT /api/project-members { project_id, emp_id, section_id, role, active }
```
---
## Примеры запросов
Замените `HOST` и ключ.
```bash
# Без ключа
curl -s "http://HOST:8090/api/health"
# С ключом
export KEY="local-dev-key-change-in-prod"
export H="http://HOST:8090"
curl -s -H "X-Api-Key: $KEY" "$H/api/meta"
curl -s -H "X-Api-Key: $KEY" \
"$H/api/work-report?date_from=2026-05-01&date_to=2026-05-31&limit=100"
curl -s -H "X-Api-Key: $KEY" \
"$H/api/project-members?emp_id=46&active_only=true"
# Табель: чтение
curl -s -H "X-Api-Key: $KEY" -H "X-Acting-Emp-Id: 46" \
"$H/api/labor/permissions?target_emp_id=46&project_id=86"
# Табель: запись
curl -s -X PUT "$H/api/time-entries" \
-H "X-Api-Key: $KEY" \
-H "X-Acting-Emp-Id: 46" \
-H "Content-Type: application/json" \
-d '{"project_id":86,"date":"2026-06-10","time":8,"over":0}'
# Состав проекта: справочники и запись
curl -s -H "X-Api-Key: $KEY" "$H/api/project-sections?project_id=86"
curl -s -H "X-Api-Key: $KEY" "$H/api/member-roles"
curl -s -X PUT "$H/api/project-members" \
-H "X-Api-Key: $KEY" \
-H "X-Acting-Emp-Id: 1" \
-H "Content-Type: application/json" \
-d '{"project_id":86,"emp_id":46,"section_id":12,"role":22,"active":true}'
```
### Пагинация `work-report` (Python)
```python
import httpx
def fetch_all_work_report(base: str, key: str, date_from: str, date_to: str) -> list:
headers = {"X-Api-Key": key}
items, offset, limit = [], 0, 2000
while True:
r = httpx.get(
f"{base}/api/work-report",
headers=headers,
params={
"date_from": date_from,
"date_to": date_to,
"limit": limit,
"offset": offset,
},
timeout=120.0,
)
r.raise_for_status()
data = r.json()
items.extend(data["items"])
offset += data["count"]
if offset >= data["total"]:
break
return items
```
---
## Связанные документы
| Документ | Содержание |
|----------|------------|
| [DEVELOPERS.md](../DEVELOPERS.md) | Развёртывание, модель данных, сборка Docker |
| [change-proposal-labor-api-write.md](change-proposal-labor-api-write.md) | Детали write API и PHP-аналоги |
| [services/user-reader/README.md](../services/user-reader/README.md) | Быстрый старт контейнера |