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>
962 lines
33 KiB
Markdown
962 lines
33 KiB
Markdown
# 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) на списках и отчётах возвращает **все** строки за один запрос (лимит 50 000). Поддерживается на:
|
||
|
||
`/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`). Максимум **50 000** строк; при превышении — `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) | Быстрый старт контейнера |
|