933 lines
36 KiB
Markdown
933 lines
36 KiB
Markdown
|
|
# Change Proposal: Labor API Write — полная замена UI табеля MeraProject
|
|||
|
|
|
|||
|
|
**Статус:** proposal (на согласование)
|
|||
|
|
**Дата:** 2026-06-10
|
|||
|
|
**Сервис:** `services/user-reader` (расширение; далее — **Labor API**)
|
|||
|
|
**Цель:** перенести заполнение рабочих часов сотрудниками во внешний сервис с сохранением поведения PHP/React UI 1:1.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Контекст
|
|||
|
|
|
|||
|
|
### 1.1 Что заменяем
|
|||
|
|
|
|||
|
|
| PHP API (`/api/{module}/{action}/`) | Назначение в UI |
|
|||
|
|
|-------------------------------------|-----------------|
|
|||
|
|
| `themes.merakomis.time/getTimeTable` | Календарь табеля (сводный + по проекту) |
|
|||
|
|
| `themes.merakomis.time/addFromCalendar` | Запись/изменение часов в ячейке |
|
|||
|
|
| `themes.merakomis.time/setAbsence` | Отметка отсутствия по выбранным датам |
|
|||
|
|
| `themes.merakomis.time/getSummary` | Виджет «месяц/год» на дашборде табеля |
|
|||
|
|
| `themes.merakomis.time.absence/setDays` | Массовая установка отсутствия (диапазон дат) |
|
|||
|
|
| `themes.merakomis.day/getByRange` | Производственный календарь (внутри `getTimeTable`) |
|
|||
|
|
|
|||
|
|
### 1.2 Что уже есть в Labor API (read)
|
|||
|
|
|
|||
|
|
| Эндпоинт | Покрытие |
|
|||
|
|
|----------|----------|
|
|||
|
|
| `GET /api/employees` | Справочник сотрудников |
|
|||
|
|
| `GET /api/projects` | Справочник проектов |
|
|||
|
|
| `GET /api/project-members` | Состав команд |
|
|||
|
|
| `GET /api/time-entries` | Сырые записи времени |
|
|||
|
|
| `GET /api/work-report`, `/api/labor-summary` | Отчёты (не UI табеля) |
|
|||
|
|
|
|||
|
|
### 1.3 Вне scope этого proposal
|
|||
|
|
|
|||
|
|
| PHP API | Причина |
|
|||
|
|
|---------|---------|
|
|||
|
|
| `themes.merakomis.time/getStat` | Аналитика/отчёты, не ввод часов |
|
|||
|
|
| `themes.merakomis.time/getTableData` | Админ-таблица CMS; при необходимости — отдельный proposal |
|
|||
|
|
| CRUD через `merakomisControllerTable` | Не используется мобильным табелём |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Таблицы MySQL
|
|||
|
|
|
|||
|
|
| Логическое имя | PHP `model.php` | Уникальный ключ |
|
|||
|
|
|----------------|-----------------|-----------------|
|
|||
|
|
| Время | `tMerakomisTime` | `(project, emp, is_over, date)` |
|
|||
|
|
| Отсутствия | `tMerakomisTimeAbsence` | `(emp, date)` |
|
|||
|
|
| Кэш отчётов | `tMerakomisTimeCache` | `key` (md5) |
|
|||
|
|
| Производственный календарь | `tMerakomisDay` | по `date` |
|
|||
|
|
| Справочник отсутствий | `tMerakomisDAbsence` | `id` |
|
|||
|
|
| Участники команд | `tMerakomisTeamMember` | — |
|
|||
|
|
| Проекты | `tMerakomisProject` | `id` |
|
|||
|
|
| Сотрудники | `tMerakomisEmp` | `id` |
|
|||
|
|
|
|||
|
|
### 2.1 Поля `tMerakomisTime`
|
|||
|
|
|
|||
|
|
| Поле | Тип | Описание |
|
|||
|
|
|------|-----|----------|
|
|||
|
|
| `id` | int AI | PK |
|
|||
|
|
| `emp` | int | ID сотрудника |
|
|||
|
|
| `project` | int | ID проекта |
|
|||
|
|
| `date` | date | День |
|
|||
|
|
| `duration` | double(10,2) | Часы (0 = удалить запись) |
|
|||
|
|
| `is_over` | tinyint | `0` рабочие, `1` переработка |
|
|||
|
|
| `portal` | int | Служебное (default `0` при записи через API) |
|
|||
|
|
| `account` | int | Служебное (default `0` при записи через API) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Аутентификация и авторизация
|
|||
|
|
|
|||
|
|
### 3.1 Два уровня (как сейчас + acting user)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Внешний сервис ──X-Api-Key──▶ Labor API ──▶ MySQL
|
|||
|
|
X-Acting-Emp-Id (новое)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Заголовок | Обязательность | Описание |
|
|||
|
|
|-----------|----------------|----------|
|
|||
|
|
| `X-Api-Key` или `Authorization: Bearer` | Если задан `USER_READER_API_KEY` | Service-to-service |
|
|||
|
|
| `X-Acting-Emp-Id` | **Обязателен** для write и permission-sensitive read | ID сотрудника, от имени которого выполняется действие (аналог `Emp::$AUTH_ID`) |
|
|||
|
|
|
|||
|
|
### 3.1.1 Модель идентичности (Identity model)
|
|||
|
|
|
|||
|
|
Labor API **не** принимает логин/пароль Merakomis. Идентификация разделена на два слоя: **кто вызывает API** (доверенный сервис) и **от чьего имени выполняется действие** (сотрудник в Merakomis).
|
|||
|
|
|
|||
|
|
#### Термины
|
|||
|
|
|
|||
|
|
| Термин | Источник в запросе | Поле в БД | Описание |
|
|||
|
|
|--------|-------------------|-----------|----------|
|
|||
|
|
| **acting** (`acting_emp_id`) | Заголовок `X-Acting-Emp-Id` | — | Кто сейчас «залогинен» во внешнем UI. Используется только для **проверки прав**. В `tMerakomisTime` не пишется. |
|
|||
|
|
| **target** (`target_emp_id`) | `emp_id` в body или query | `tMerakomisTime.emp` | **За кого** создаётся/меняется запись времени или отсутствия. |
|
|||
|
|
|
|||
|
|
**Правило по умолчанию:** если `emp_id` в body/query **не передан**, то `target_emp_id = acting_emp_id` (сотрудник работает со своим табелем).
|
|||
|
|
|
|||
|
|
#### Аналогия с PHP UI
|
|||
|
|
|
|||
|
|
| PHP (сессия) | Labor API |
|
|||
|
|
|--------------|-----------|
|
|||
|
|
| `Emp::$AUTH_ID` после `login` | `X-Acting-Emp-Id` |
|
|||
|
|
| `$_POST['emp']` или default `Emp::$AUTH_ID` | `emp_id` в body/query |
|
|||
|
|
| Cookie сессии | Нет; вместо неё пара API-key + acting |
|
|||
|
|
| `intval($_POST['emp']) ?: Emp::$AUTH_ID` в `addFromCalendar` | `target = body.emp_id ?? header.acting` |
|
|||
|
|
|
|||
|
|
#### Поток во внешнем сервисе
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Пользователь логинится во внешнем сервисе (SSO, email+password, и т.д.)
|
|||
|
|
2. Внешний сервис резолвит Merakomis emp_id:
|
|||
|
|
GET /api/employees → сопоставление по login / email / tg_acc
|
|||
|
|
3. Внешний сервис хранит emp_id в своей сессии/JWT
|
|||
|
|
4. При вызове Labor API бэкенд добавляет:
|
|||
|
|
X-Api-Key: <секрет сервиса>
|
|||
|
|
X-Acting-Emp-Id: <emp_id из шага 2>
|
|||
|
|
5. При записи за другого сотрудника (менеджер) бэкенд дополнительно передаёт emp_id в body
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌──────────────┐ login ┌──────────────────┐
|
|||
|
|
│ Браузер │───────────────▶│ Ваш бэкенд │
|
|||
|
|
│ (без ключа) │◀───────────────│ знает emp_id │
|
|||
|
|
└──────────────┘ JWT/session └────────┬─────────┘
|
|||
|
|
│ X-Api-Key
|
|||
|
|
│ X-Acting-Emp-Id
|
|||
|
|
│ body.emp_id (опц.)
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ Labor API :8090 │
|
|||
|
|
└────────┬─────────┘
|
|||
|
|
│ emp = target_emp_id
|
|||
|
|
▼
|
|||
|
|
┌──────────────────┐
|
|||
|
|
│ tMerakomisTime │
|
|||
|
|
└──────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Безопасность
|
|||
|
|
|
|||
|
|
| Правило | Обоснование |
|
|||
|
|
|---------|-------------|
|
|||
|
|
| `USER_READER_API_KEY` **только на бэкенде** внешнего сервиса | С ключом можно писать за любого `emp_id` |
|
|||
|
|
| Браузер **никогда** не шлёт `X-Api-Key` напрямую в Labor API | Клиент ходит только в ваш API |
|
|||
|
|
| `X-Acting-Emp-Id` выставляет **только ваш бэкенд** после своей аутентификации | Нельзя доверять `emp_id` из тела запроса браузера как «кто я» |
|
|||
|
|
| `emp_id` в body от браузера — только если ваш бэкенд **перепроверяет** право менеджера писать за подчинённого | Иначе подмена target |
|
|||
|
|
|
|||
|
|
Рекомендация: для менеджерского UI ваш бэкенд принимает от фронта `target_emp_id`, сверяет с собственной моделью прав (или вызывает `GET /api/labor/permissions`), и только потом проксирует в Labor API.
|
|||
|
|
|
|||
|
|
#### Где задаётся target (`emp_id`) по эндпоинтам
|
|||
|
|
|
|||
|
|
| Эндпоинт | acting | target (`emp_id`) | Default target |
|
|||
|
|
|----------|--------|-------------------|----------------|
|
|||
|
|
| `PUT /api/time-entries` | заголовок | body `emp_id` | `= acting` |
|
|||
|
|
| `PUT /api/absences` | заголовок | body `emp_id` | `= acting` |
|
|||
|
|
| `PUT /api/absences/range` | заголовок | body `emp_id` | **обязателен** (как PHP `setDays`) |
|
|||
|
|
| `GET /api/time-calendar` | заголовок | query `emp_id` | `= acting` |
|
|||
|
|
| `GET /api/time-summary` | заголовок | query `emp_id` | `= acting` |
|
|||
|
|
| `GET /api/time-entries` | заголовок¹ | query `emp_id` | нет default² |
|
|||
|
|
| `GET /api/labor/permissions` | заголовок | query `target_emp_id` | **обязателен** |
|
|||
|
|
|
|||
|
|
¹ Для read-only справочников (`/api/employees`, `/api/projects`) `X-Acting-Emp-Id` **не обязателен** — достаточно API-key.
|
|||
|
|
|
|||
|
|
² Существующий `GET /api/time-entries` без `emp_id` возвращает всех; для UI табеля всегда передавайте `emp_id` явно.
|
|||
|
|
|
|||
|
|
#### Примеры запросов
|
|||
|
|
|
|||
|
|
**Сотрудник пишет 8 часов за себя** (acting = target):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -s -X PUT "http://HOST:8090/api/time-entries" \
|
|||
|
|
-H "X-Api-Key: local-dev-key-change-in-prod" \
|
|||
|
|
-H "X-Acting-Emp-Id: 46" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{
|
|||
|
|
"project_id": 86,
|
|||
|
|
"date": "2026-06-10",
|
|||
|
|
"time": 8,
|
|||
|
|
"over": 0
|
|||
|
|
}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`emp_id` опущен → запись в `tMerakomisTime` с `emp = 46`.
|
|||
|
|
|
|||
|
|
**Тот же случай с явным `emp_id`:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{ "emp_id": 46, "project_id": 86, "date": "2026-06-10", "time": 8, "over": 0 }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Менеджер (emp 10) вносит часы за подчинённого (emp 46):**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -s -X PUT "http://HOST:8090/api/time-entries" \
|
|||
|
|
-H "X-Api-Key: local-dev-key-change-in-prod" \
|
|||
|
|
-H "X-Acting-Emp-Id: 10" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{
|
|||
|
|
"emp_id": 46,
|
|||
|
|
"project_id": 86,
|
|||
|
|
"date": "2026-06-10",
|
|||
|
|
"time": 8,
|
|||
|
|
"over": 0
|
|||
|
|
}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Проверка: `can_write_time_entry(acting=10, target=46, project_id=86)` — true, если `46` в `write_other_table_write_ids(10)` или `10` — admin.
|
|||
|
|
|
|||
|
|
**Чтение календаря подчинённого:**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -s "http://HOST:8090/api/time-calendar?emp_id=46&project_id=86" \
|
|||
|
|
-H "X-Api-Key: local-dev-key-change-in-prod" \
|
|||
|
|
-H "X-Acting-Emp-Id: 10"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Проверка прав до отрисовки UI:**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -s "http://HOST:8090/api/labor/permissions?target_emp_id=46&project_id=86" \
|
|||
|
|
-H "X-Api-Key: local-dev-key-change-in-prod" \
|
|||
|
|
-H "X-Acting-Emp-Id: 10"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Ответ write-эндпоинтов: эхо идентичности
|
|||
|
|
|
|||
|
|
Во всех ответах на запись **обязательно** возвращать (для отладки и аудита):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"acting_emp_id": 10,
|
|||
|
|
"target_emp_id": 46,
|
|||
|
|
"ok": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Ошибки, связанные с идентичностью
|
|||
|
|
|
|||
|
|
| HTTP | `detail.code` | Когда |
|
|||
|
|
|------|---------------|-------|
|
|||
|
|
| `400` | `missing_acting_emp` | Нет заголовка `X-Acting-Emp-Id` на write / permission-sensitive read |
|
|||
|
|
| `400` | `invalid_acting_emp` | `X-Acting-Emp-Id` не целое число или ≤ 0 |
|
|||
|
|
| `404` | `acting_emp_not_found` | acting нет в `tMerakomisEmp` или `removed = 1` |
|
|||
|
|
| `404` | `target_emp_not_found` | target нет в `tMerakomisEmp` или `removed = 1` |
|
|||
|
|
| `403` | `forbidden` | acting не может выполнить операцию для target |
|
|||
|
|
|
|||
|
|
#### Резолв `login → emp_id` (вне Labor API)
|
|||
|
|
|
|||
|
|
Labor API не содержит эндпоинта login. Внешний сервис использует существующий справочник:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Пример: найти id по login (на стороне клиента)
|
|||
|
|
curl -s -H "X-Api-Key: ..." "http://HOST:8090/api/employees?limit=500" \
|
|||
|
|
| jq '.items[] | select(.login=="ivanov") | .id'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Поля для сопоставления (режим `merakomis_emp`): `id`, `login`, `email`, `name`. Кэшировать каталог на стороне внешнего сервиса; для дельты — `GET /api/employees/delta`.
|
|||
|
|
|
|||
|
|
### 3.2 Правила доступа (порт `themes/merakomis/time/model.php` + `emp/Rules.php`)
|
|||
|
|
|
|||
|
|
#### `can_read_time_calendar(acting, target, project_id)`
|
|||
|
|
|
|||
|
|
- `acting == target` → разрешено
|
|||
|
|
- `is_admin(acting)` → разрешено (`tMerakomisEmp.type = 1`)
|
|||
|
|
- `target in write_other_table_write_ids(acting)` → разрешено (см. §3.3)
|
|||
|
|
- иначе → `403`
|
|||
|
|
|
|||
|
|
#### `can_write_time_entry(acting, target, project_id)`
|
|||
|
|
|
|||
|
|
`project_id` **обязателен** и `> 0` (сводный табель `project_id=0` — только чтение, как в PHP `can_edit=false`).
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
can_write =
|
|||
|
|
is_admin(acting)
|
|||
|
|
OR (
|
|||
|
|
target in write_other_table_write_ids(acting)
|
|||
|
|
)
|
|||
|
|
OR (
|
|||
|
|
acting == target
|
|||
|
|
AND member.active = 1
|
|||
|
|
AND project.archive = 0
|
|||
|
|
AND member exists for (target, project)
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Источник PHP (`getTimeTable`, строки 314–318):
|
|||
|
|
|
|||
|
|
```php
|
|||
|
|
$isCanEdit =
|
|||
|
|
(boolval($member[Member::$ACTIVE]) and $member[Member::$EMP] == Emp::$AUTH_ID and !$project[Project::$ARCHIVE])
|
|||
|
|
|| Rules::isWriteOtherTableWrite($emp_id);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### `can_write_absence(acting, target)`
|
|||
|
|
|
|||
|
|
Повторяет PHP `setAbsence` / `setDays`: в PHP проверяется только `Emp::$IS_AUTH`.
|
|||
|
|
**Решение proposal:** разрешать, если `can_read_time_calendar(acting, target, *)` (т.е. за себя или подчинённых/админ). Ужесточение возможно отдельным решением.
|
|||
|
|
|
|||
|
|
### 3.3 `write_other_table_write_ids(acting)` — полный порт
|
|||
|
|
|
|||
|
|
Объединение (уникальные int):
|
|||
|
|
|
|||
|
|
1. `Emp::getSubEmps(acting)` — подчинённые по оргструктуре (директора отделов)
|
|||
|
|
2. `Rules::getMyCitySubEmps()` — сотрудники городов из групп `acting`
|
|||
|
|
|
|||
|
|
`getSubEmps` требует портирования `Department::getStructure()` + `Emp::getRoles()` — **отдельный модуль** `app/labor_permissions.py`. До полного порта допустим fallback: вызов SQL-эвристики + интеграционные тесты против PHP на фикстурах.
|
|||
|
|
|
|||
|
|
### 3.4 Коды ошибок авторизации
|
|||
|
|
|
|||
|
|
| HTTP | `detail.code` | Когда |
|
|||
|
|
|------|---------------|-------|
|
|||
|
|
| `401` | `unauthorized` | Нет/неверный API-ключ |
|
|||
|
|
| `400` | `missing_acting_emp` | Нет `X-Acting-Emp-Id` (см. §3.1.1) |
|
|||
|
|
| `400` | `invalid_acting_emp` | Некорректный `X-Acting-Emp-Id` |
|
|||
|
|
| `404` | `acting_emp_not_found` | acting не найден в `tMerakomisEmp` |
|
|||
|
|
| `404` | `target_emp_not_found` | target (`emp_id`) не найден в `tMerakomisEmp` |
|
|||
|
|
| `403` | `forbidden` | Нет прав на операцию |
|
|||
|
|
| `403` | `project_readonly` | `project_id=0` или архивный проект для self-write |
|
|||
|
|
| `403` | `not_team_member` | Self-write, но нет записи в `tMerakomisTeamMember` |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Общие соглашения API
|
|||
|
|
|
|||
|
|
- Базовый URL: `http://HOST:8090` (docker-compose)
|
|||
|
|
- `Content-Type: application/json` для write
|
|||
|
|
- Даты: `YYYY-MM-DD` (ISO)
|
|||
|
|
- Часы: `number`, шаг после нормализации: `0`, `0.5`, `1`
|
|||
|
|
- Поля ответа: snake_case
|
|||
|
|
- Ошибки FastAPI: `{ "detail": "..." }` или `{ "detail": { "code": "...", "message": "..." } }` для бизнес-ошибок
|
|||
|
|
|
|||
|
|
### 4.1 Общие коды ошибок
|
|||
|
|
|
|||
|
|
| HTTP | `code` | Когда |
|
|||
|
|
|------|--------|-------|
|
|||
|
|
| `400` | `invalid_date` | Невалидная дата |
|
|||
|
|
| `400` | `invalid_duration` | duration < 0 или > 24 до нормализации |
|
|||
|
|
| `400` | `date_range_invalid` | `begin > end` |
|
|||
|
|
| `400` | `missing_field` | Обязательное поле |
|
|||
|
|
| `404` | `project_not_found` | |
|
|||
|
|
| `404` | `absence_type_not_found` | |
|
|||
|
|
| `422` | — | Ошибка валидации Pydantic |
|
|||
|
|
| `500` | `db_error` | MySQL |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Эндпоинты (новые и расширенные)
|
|||
|
|
|
|||
|
|
### 5.1 `GET /api/time-calendar` — замена `getTimeTable`
|
|||
|
|
|
|||
|
|
**PHP:** `POST themes.merakomis.time/getTimeTable`
|
|||
|
|
**Параметры:**
|
|||
|
|
|
|||
|
|
| Query | Тип | Обяз. | Default | Описание |
|
|||
|
|
|-------|-----|-------|---------|----------|
|
|||
|
|
| `emp_id` | int | нет | `X-Acting-Emp-Id` | Чей табель |
|
|||
|
|
| `project_id` | int | нет | `0` | `0` = сводный табель |
|
|||
|
|
| `year` | int | нет | текущий | |
|
|||
|
|
| `month` | int | нет | текущий | 1–12 |
|
|||
|
|
|
|||
|
|
**Заголовки:** `X-Api-Key`, `X-Acting-Emp-Id`
|
|||
|
|
|
|||
|
|
**Ответ `200`:** структура совместима с PHP `getTimeTable` (ключи сохраняются):
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"can_edit": true,
|
|||
|
|
"cant_edit": false,
|
|||
|
|
"dates": { "2026-06-10": { "hours": 8, "over": 0 } },
|
|||
|
|
"days": { "2026-06-01": { "date": "2026-06-01", "type": 1, "text": "" } },
|
|||
|
|
"day_begin": "2024-01-15",
|
|||
|
|
"day_end": "2026-06-10",
|
|||
|
|
"name": "Иванов Иван",
|
|||
|
|
"project": "2025-0016",
|
|||
|
|
"text": "...",
|
|||
|
|
"archive": false,
|
|||
|
|
"month": { "2026": { "6": { "hours": 80, "over": 4, "total": 84 } } },
|
|||
|
|
"absence": { "2026-06-05": { "id": 1, "title": "Отпуск", "code": "ОТ", "absence_id": 3 } },
|
|||
|
|
"absence_stat": { "2026": { "6": 2 } },
|
|||
|
|
"absence_options": [{ "id": 0, "title": "—" }, { "id": 3, "title": "Отпуск" }],
|
|||
|
|
"project_date": "2024-01-15",
|
|||
|
|
"events": { "2026-06-12": ["Корпоратив"] },
|
|||
|
|
"base": { "2026-06-10": { "hours": 8, "over": 0 } },
|
|||
|
|
"graph1": {},
|
|||
|
|
"graph2": {},
|
|||
|
|
"totals": { "2026-06-10": { "hours": "2025-0016: 8", "over": "" } },
|
|||
|
|
"total": { "hours": "80 час.", "over": "4 час.", "total": "84 час." }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**SQL (основные блоки):**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- Записи времени сотрудника (опционально по проекту)
|
|||
|
|
SELECT t.date, t.is_over, t.duration, p.id, p.name, p.code, p.step
|
|||
|
|
FROM tMerakomisTime t
|
|||
|
|
LEFT JOIN tMerakomisProject p ON p.id = t.project
|
|||
|
|
WHERE t.duration > 0
|
|||
|
|
AND t.emp = :emp_id
|
|||
|
|
AND (:project_id = 0 OR t.project = :project_id);
|
|||
|
|
|
|||
|
|
-- Сводка по дням (base)
|
|||
|
|
SELECT date, is_over, SUM(duration) AS cc
|
|||
|
|
FROM tMerakomisTime
|
|||
|
|
WHERE emp = :emp_id
|
|||
|
|
GROUP BY date, is_over;
|
|||
|
|
|
|||
|
|
-- Отсутствия
|
|||
|
|
SELECT a.id, a.date, a.absence, d.name, d.code
|
|||
|
|
FROM tMerakomisTimeAbsence a
|
|||
|
|
LEFT JOIN tMerakomisDAbsence d ON d.id = a.absence
|
|||
|
|
WHERE a.emp = :emp_id AND a.absence <> 0;
|
|||
|
|
|
|||
|
|
-- Производственный календарь
|
|||
|
|
SELECT date, type, text
|
|||
|
|
FROM tMerakomisDay
|
|||
|
|
WHERE date BETWEEN :day_begin AND :day_end;
|
|||
|
|
|
|||
|
|
-- Участник команды + проект
|
|||
|
|
SELECT m.*, p.archive, p.date AS project_date, p.code, p.team
|
|||
|
|
FROM tMerakomisProject p
|
|||
|
|
LEFT JOIN tMerakomisTeamMember m
|
|||
|
|
ON m.team = p.team AND m.emp = :emp_id
|
|||
|
|
WHERE p.id = :project_id
|
|||
|
|
LIMIT 1;
|
|||
|
|
|
|||
|
|
-- Справочник отсутствий (vis=1 + zero option)
|
|||
|
|
SELECT id, name, code FROM tMerakomisDAbsence WHERE vis = 1;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Логика `day_begin`:** `project.date` или `'2000-01-01'`; `day_end` = `CURRENT_DATE`.
|
|||
|
|
|
|||
|
|
**Логика `events`:** все записи `tMerakomisDay.text` по диапазону (как PHP).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.2 `PUT /api/time-entries` — замена `addFromCalendar`
|
|||
|
|
|
|||
|
|
**PHP:** `POST themes.merakomis.time/addFromCalendar`
|
|||
|
|
|
|||
|
|
**Тело запроса:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"emp_id": 46,
|
|||
|
|
"project_id": 86,
|
|||
|
|
"date": "2026-06-10",
|
|||
|
|
"time": 7.3,
|
|||
|
|
"over": 0
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Поле | Тип | Обяз. | PHP-аналог |
|
|||
|
|
|------|-----|-------|------------|
|
|||
|
|
| `emp_id` | int | нет | `$_POST['emp']` (default: acting) |
|
|||
|
|
| `project_id` | int | да | `$_POST['project']` |
|
|||
|
|
| `date` | string | да | `$_POST['date']` |
|
|||
|
|
| `time` | number | да | `$_POST['time']` — **до** нормализации |
|
|||
|
|
| `over` | int 0\|1 | нет | `$_POST['over']`, default `0` |
|
|||
|
|
|
|||
|
|
#### Алгоритм (1:1 с `controller.php` + `model.php _insert`)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Проверить can_write_time_entry(acting, emp_id, project_id)
|
|||
|
|
2. time = float(time)
|
|||
|
|
3. Нормализация дробной части:
|
|||
|
|
x = (time - floor(time)) * 100
|
|||
|
|
x <= 25 → 0
|
|||
|
|
x >= 75 → 100 (т.е. +1 час)
|
|||
|
|
иначе → 50 (т.е. +0.5)
|
|||
|
|
time = floor(time) + x/100
|
|||
|
|
4. time = clamp(time, 0, 24)
|
|||
|
|
5. Загрузить суммы за день по emp_id и date:
|
|||
|
|
total_work = SUM(duration) WHERE is_over=0
|
|||
|
|
total_over = SUM(duration) WHERE is_over=1
|
|||
|
|
6. max_work = get_work_hours_by_date(date) -- §6.1
|
|||
|
|
max_over = 24 - max_work
|
|||
|
|
7. info = часы за (emp, project, date) — отдельно work и over
|
|||
|
|
8. total_work -= info.hours; total_over -= info.over
|
|||
|
|
9. new_work = total_work + time (если over=0)
|
|||
|
|
new_over = total_over + time (если over=1)
|
|||
|
|
10. Если over=1 и new_over > max_over: time = max_over - total_over (= leftOverHours)
|
|||
|
|
Если over=0 и new_work > max_work: time = max_work - total_work (= leftWorkHours)
|
|||
|
|
11. UPSERT в tMerakomisTime (§6.2)
|
|||
|
|
12. Если time == 0 после шага 10: DELETE строки (как PHP _insert)
|
|||
|
|
13. invalidate_time_cache(date, emp_id, project_id) (§6.3)
|
|||
|
|
14. Вернуть ответ с диагностикой (как PHP)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Ответ `200`:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"ok": true,
|
|||
|
|
"acting_emp_id": 46,
|
|||
|
|
"target_emp_id": 46,
|
|||
|
|
"id": 12345,
|
|||
|
|
"emp_id": 46,
|
|||
|
|
"project_id": 86,
|
|||
|
|
"date": "2026-06-10",
|
|||
|
|
"duration": 7.5,
|
|||
|
|
"is_over": 0,
|
|||
|
|
"info": { "hours": 7.5, "over": 0, "over1": 0, "over2": 0, "total": 7.5 },
|
|||
|
|
"limits": {
|
|||
|
|
"max_work_hours": 8,
|
|||
|
|
"max_over_hours": 16,
|
|||
|
|
"left_work_hours": 0.5,
|
|||
|
|
"left_over_hours": 16,
|
|||
|
|
"new_work_hours": 8,
|
|||
|
|
"new_over_hours": 0,
|
|||
|
|
"clamped": true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
PHP возвращал `$newWorkHours`, `$leftWorkHours` и т.д. с префиксом `$` — в API переименовать в `limits.*` (документировать breaking change для клиента).
|
|||
|
|
|
|||
|
|
**Ошибки:**
|
|||
|
|
|
|||
|
|
| HTTP | code | Когда |
|
|||
|
|
|------|------|-------|
|
|||
|
|
| `403` | `forbidden` | Нет `can_write` |
|
|||
|
|
| `400` | `invalid_date` | |
|
|||
|
|
| `404` | `project_not_found` | |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.3 `PUT /api/absences` — замена `setAbsence`
|
|||
|
|
|
|||
|
|
**PHP:** `POST themes.merakomis.time/setAbsence`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"emp_id": 46,
|
|||
|
|
"project_id": 86,
|
|||
|
|
"type": 3,
|
|||
|
|
"dates": ["2026-06-10", "2026-06-11"]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Поле | Описание |
|
|||
|
|
|------|----------|
|
|||
|
|
| `type` | `absence_id`; `0` = снять отсутствие |
|
|||
|
|
| `dates` | Массив дат (не пустой) |
|
|||
|
|
| `project_id` | В PHP передаётся, но **не используется** в логике — опционально для совместимости |
|
|||
|
|
|
|||
|
|
**Алгоритм (порт `Absence::uadd` + `afterUadd`):**
|
|||
|
|
|
|||
|
|
Для каждой `date` в `dates`:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. can_write_absence(acting, emp_id)
|
|||
|
|
2. UPSERT tMerakomisTimeAbsence (emp, date) ON DUPLICATE UPDATE absence=type
|
|||
|
|
3. Если type == 0:
|
|||
|
|
- DELETE FROM tMerakomisTimeAbsence WHERE emp AND date
|
|||
|
|
4. Если type != 0 (после upsert, как afterUadd):
|
|||
|
|
- DELETE FROM tMerakomisTime
|
|||
|
|
WHERE emp=:emp AND date=:date AND is_over=0
|
|||
|
|
5. invalidate_time_cache(date, emp_id, project_id=NULL)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**SQL:**
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO tMerakomisTimeAbsence (emp, date, absence, portal, account)
|
|||
|
|
VALUES (:emp, :date, :type, 0, 0)
|
|||
|
|
ON DUPLICATE KEY UPDATE absence = VALUES(absence);
|
|||
|
|
|
|||
|
|
-- type = 0:
|
|||
|
|
DELETE FROM tMerakomisTimeAbsence WHERE emp = :emp AND date = :date;
|
|||
|
|
|
|||
|
|
-- type != 0: удалить рабочие часы за день
|
|||
|
|
DELETE FROM tMerakomisTime
|
|||
|
|
WHERE emp = :emp AND date = :date AND is_over = 0;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Ответ `200`:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"ok": true,
|
|||
|
|
"acting_emp_id": 10,
|
|||
|
|
"target_emp_id": 46,
|
|||
|
|
"emp_id": 46,
|
|||
|
|
"results": [
|
|||
|
|
{ "date": "2026-06-10", "absence_type_id": 3, "cache_rows_deleted": 2 },
|
|||
|
|
{ "date": "2026-06-11", "absence_type_id": 3, "cache_rows_deleted": 1 }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.4 `PUT /api/absences/range` — замена `setDays`
|
|||
|
|
|
|||
|
|
**PHP:** `POST themes.merakomis.time.absence/setDays`
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"emp_id": 46,
|
|||
|
|
"absence_id": 3,
|
|||
|
|
"begin": "2026-06-10",
|
|||
|
|
"end": "2026-06-14"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Валидация (как `Absence::setDays`):**
|
|||
|
|
|
|||
|
|
| Условие | code | message |
|
|||
|
|
|---------|------|---------|
|
|||
|
|
| `!begin` | `missing_begin` | Укажите дату начала |
|
|||
|
|
| `!end` | `missing_end` | Укажите дату окончания |
|
|||
|
|
| `end < begin` | `date_range_invalid` | Дата окончания должна быть больше даты начала |
|
|||
|
|
| `!emp_id` | `missing_emp` | Укажите сотрудника |
|
|||
|
|
|
|||
|
|
**Алгоритм:**
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. Сгенерировать список дат [begin..end] включительно
|
|||
|
|
2. DELETE FROM tMerakomisTimeAbsence WHERE emp AND date IN (dates)
|
|||
|
|
3. DELETE FROM tMerakomisTime WHERE emp AND date IN (dates) AND is_over=0
|
|||
|
|
4. Если absence_id > 0: INSERT для каждой даты
|
|||
|
|
5. Для каждой даты: invalidate_time_cache(date, emp_id, NULL)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Ответ `200`:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"ok": true,
|
|||
|
|
"acting_emp_id": 10,
|
|||
|
|
"target_emp_id": 46,
|
|||
|
|
"e": 0,
|
|||
|
|
"m": "Успешно сохранено",
|
|||
|
|
"dates_count": 5
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(`e`/`m` — совместимость с PHP notify.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.5 `GET /api/time-summary` — замена `getSummary`
|
|||
|
|
|
|||
|
|
**PHP:** `POST themes.merakomis.time/getSummary`
|
|||
|
|
|
|||
|
|
| Query | Default |
|
|||
|
|
|-------|---------|
|
|||
|
|
| `emp_id` | `X-Acting-Emp-Id` |
|
|||
|
|
|
|||
|
|
**Ответ:** структура `Time::getMySummary` — блоки `month` и `year` с `project[]`, `total[]`, `absence[]`, `title`, `text`.
|
|||
|
|
|
|||
|
|
**SQL:** агрегация `SUM(duration)` по проектам + `COUNT` отсутствий по типам за:
|
|||
|
|
- месяц: `[YYYY-MM-01, TODAY]`
|
|||
|
|
- год: `[YYYY-01-01, TODAY]`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.6 `GET /api/calendar-days` — замена `day/getByRange`
|
|||
|
|
|
|||
|
|
| Query | Обяз. |
|
|||
|
|
|-------|-------|
|
|||
|
|
| `date_from` | да |
|
|||
|
|
| `date_to` | да |
|
|||
|
|
|
|||
|
|
**Ответ:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"items": {
|
|||
|
|
"2026-06-10": { "date": "2026-06-10", "type": 1, "text": "" }
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`type`: `1` WORK, `2` NO_WORK, `3` SHORT (`EDayType`).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.7 `GET /api/absence-types` — справочник
|
|||
|
|
|
|||
|
|
**Ответ:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"items": [
|
|||
|
|
{ "id": 0, "title": "—", "code": "" },
|
|||
|
|
{ "id": 3, "title": "Отпуск", "code": "ОТ", "vis": 1 }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Фильтр: `vis = 1` (+ zero option), как `DAbsence::getNameList`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5.8 `GET /api/labor/permissions` — вспомогательный (новый)
|
|||
|
|
|
|||
|
|
Для внешнего UI — проверка до отрисовки кнопок.
|
|||
|
|
|
|||
|
|
| Query | Описание |
|
|||
|
|
|-------|----------|
|
|||
|
|
| `target_emp_id` | |
|
|||
|
|
| `project_id` | опционально |
|
|||
|
|
|
|||
|
|
**Ответ:**
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"acting_emp_id": 10,
|
|||
|
|
"target_emp_id": 46,
|
|||
|
|
"project_id": 86,
|
|||
|
|
"can_read": true,
|
|||
|
|
"can_write_time": false,
|
|||
|
|
"can_write_absence": true,
|
|||
|
|
"is_admin": false,
|
|||
|
|
"is_delegate_writer": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. SQL-операции (ядро)
|
|||
|
|
|
|||
|
|
### 6.1 `get_work_hours_by_date(date)` — порт `Day::getWorkHoursByDate`
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. SELECT * FROM tMerakomisDay WHERE date = :date LIMIT 1
|
|||
|
|
2. Если строка есть: return day.hours
|
|||
|
|
3. Иначе: если weekday in (Mon..Fri): return 8
|
|||
|
|
иначе: return 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Константа `8` = `Day::getPerHour()`.
|
|||
|
|
|
|||
|
|
### 6.2 Upsert времени
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
INSERT INTO tMerakomisTime (emp, project, date, duration, is_over, portal, account)
|
|||
|
|
VALUES (:emp, :project, :date, :duration, :is_over, 0, 0)
|
|||
|
|
ON DUPLICATE KEY UPDATE duration = VALUES(duration);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
После insert:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- Если duration = 0:
|
|||
|
|
DELETE FROM tMerakomisTime
|
|||
|
|
WHERE emp = :emp AND project = :project AND date = :date AND is_over = :is_over;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 `invalidate_time_cache(date, emp, project)`
|
|||
|
|
|
|||
|
|
Порт `Cache::removeRows`:
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
DELETE FROM tMerakomisTimeCache
|
|||
|
|
WHERE begin <= :date
|
|||
|
|
AND end >= :date
|
|||
|
|
AND (:emp IS NULL OR emp = :emp)
|
|||
|
|
AND (
|
|||
|
|
:project IS NULL
|
|||
|
|
OR project = :project
|
|||
|
|
OR project = 0
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.4 Суммы за день (для лимитов)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SELECT is_over, SUM(duration) AS cc
|
|||
|
|
FROM tMerakomisTime
|
|||
|
|
WHERE emp = :emp AND date = :date
|
|||
|
|
GROUP BY is_over;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.5 Часы по проекту за день
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
SELECT is_over, duration
|
|||
|
|
FROM tMerakomisTime
|
|||
|
|
WHERE emp = :emp AND project = :project AND date = :date;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Побочные эффекты (чеклист паритета)
|
|||
|
|
|
|||
|
|
| # | Поведение PHP | Обязательно в API |
|
|||
|
|
|---|---------------|-------------------|
|
|||
|
|
| 1 | Upsert по `(project, emp, is_over, date)` | да |
|
|||
|
|
| 2 | `duration=0` → DELETE | да |
|
|||
|
|
| 3 | Округление 0/0.5/1 | да |
|
|||
|
|
| 4 | Clamp 0..24 | да |
|
|||
|
|
| 5 | Лимит рабочих/переработки за день | да |
|
|||
|
|
| 6 | Инвалидация `tMerakomisTimeCache` | да |
|
|||
|
|
| 7 | Отсутствие удаляет `is_over=0` за день | да |
|
|||
|
|
| 8 | `setDays` удаляет старые absence+work перед insert | да |
|
|||
|
|
| 9 | Сводный табель `project_id=0` read-only | да |
|
|||
|
|
| 10 | Архивный проект — read-only для self | да |
|
|||
|
|
| 11 | `portal`/`account` = 0 | да |
|
|||
|
|
| 12 | Пересчёт `over1`/`over2` в отчётах | автоматически при чтении (уже в `labor.py`) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Структура кода (реализация)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
services/user-reader/app/
|
|||
|
|
labor.py # существующие GET
|
|||
|
|
labor_write.py # PUT time-entries, absences
|
|||
|
|
labor_calendar.py # GET time-calendar, calendar-days, time-summary
|
|||
|
|
labor_permissions.py # can_write*, write_other_table_write_ids
|
|||
|
|
labor_cache.py # invalidate_time_cache
|
|||
|
|
labor_day.py # get_work_hours_by_date, is_no_work_day
|
|||
|
|
merakomis_schema.py # + TIME_CACHE_TABLE, ABSENCE_DICT_TABLE
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Обновить `DEVELOPERS.md` — раздел Write API.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Тест-план (паритет с PHP)
|
|||
|
|
|
|||
|
|
| # | Сценарий | Ожидание |
|
|||
|
|
|---|----------|----------|
|
|||
|
|
| 1 | 8ч рабочие в будний день | OK |
|
|||
|
|
| 2 | 9ч рабочие при max=8 | clamp до 8 |
|
|||
|
|
| 3 | 7.2ч ввод | → 7.5 |
|
|||
|
|
| 4 | 7.8ч ввод | → 8.0 |
|
|||
|
|
| 5 | Два проекта по 4ч | OK, сумма 8 |
|
|||
|
|
| 6 | Замена 4ч→2ч на проекте A | лимит пересчитывается с вычитанием старых часов проекта |
|
|||
|
|
| 7 | `duration=0` | строка удалена |
|
|||
|
|
| 8 | Отсутствие type=3 | рабочие часы за день удалены |
|
|||
|
|
| 9 | `setDays` на диапазон | старые записи очищены |
|
|||
|
|
| 10 | Запись в архивный проект self | 403 |
|
|||
|
|
| 11 | Менеджер пишет за подчинённого | OK |
|
|||
|
|
| 12 | После записи `work-report` | актуальные часы (кэш сброшен) |
|
|||
|
|
| 13 | Сводный `project_id=0` write | 403 |
|
|||
|
|
| 14 | Суббота без записи в Day | max_work=0 |
|
|||
|
|
|
|||
|
|
**Метод:** параллельные запросы к PHP API (с тестовой сессией) и Labor API на копии БД; diff JSON.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. Миграция потребителей
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Этап 1: Реализовать API + тесты паритета
|
|||
|
|
Этап 2: Внешний сервис читает time-calendar, пишет time-entries
|
|||
|
|
Этап 3: Пилот на группе сотрудников
|
|||
|
|
Этап 4: Скрыть маршрут табеля в MeraProject React (feature flag)
|
|||
|
|
Этап 5: Мониторинг расхождений work-report
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
PHP UI **не удалять** до стабилизации; писать в ту же БД.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. Риски
|
|||
|
|
|
|||
|
|
| Риск | Митигация |
|
|||
|
|
|------|-----------|
|
|||
|
|
| Неполный порт `writeOtherTableWrite` | Модуль permissions + тесты против PHP |
|
|||
|
|
| Расхождение имён колонок (`tMerakomisTime_emp`) | `_prefixed_col` как в `labor.py` |
|
|||
|
|
| Двойной ввод (старый + новый UI) | Коммуникация + скрытие UI |
|
|||
|
|
| `Cache::removeRows` echo в PHP | Не влияет на API; SQL идентичен |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. Задачи реализации
|
|||
|
|
|
|||
|
|
- [ ] `labor_day.py` — календарь и норма часов
|
|||
|
|
- [ ] `labor_cache.py` — инвалидация кэша
|
|||
|
|
- [ ] `labor_permissions.py` — полный порт Rules
|
|||
|
|
- [ ] `PUT /api/time-entries` — алгоритм addFromCalendar + _insert
|
|||
|
|
- [ ] `PUT /api/absences` — setAbsence + afterUadd
|
|||
|
|
- [ ] `PUT /api/absences/range` — setDays
|
|||
|
|
- [ ] `GET /api/time-calendar` — getTimeTable
|
|||
|
|
- [ ] `GET /api/time-summary` — getMySummary
|
|||
|
|
- [ ] `GET /api/calendar-days`, `/api/absence-types`
|
|||
|
|
- [ ] `GET /api/labor/permissions`
|
|||
|
|
- [ ] Интеграционные тесты паритета
|
|||
|
|
- [ ] `DEVELOPERS.md` — документация write API
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Приложение A: маппинг полей запроса PHP → REST
|
|||
|
|
|
|||
|
|
| PHP | REST |
|
|||
|
|
|-----|------|
|
|||
|
|
| `Emp::$AUTH_ID` (сессия) | заголовок `X-Acting-Emp-Id` |
|
|||
|
|
| `$_POST['emp']` | `emp_id` в body/query (`target`); если пусто → `= acting` |
|
|||
|
|
|
|||
|
|
| PHP `$_POST` | REST |
|
|||
|
|
|--------------|------|
|
|||
|
|
| `emp` | `emp_id` |
|
|||
|
|
| `project` | `project_id` |
|
|||
|
|
| `date` | `date` |
|
|||
|
|
| `time` | `time` |
|
|||
|
|
| `over` | `over` / `is_over` в ответе |
|
|||
|
|
| `dates[]` | `dates` |
|
|||
|
|
| `type` | `type` / `absence_type_id` |
|
|||
|
|
| `begin` / `end` | `begin` / `end` |
|
|||
|
|
| `absence_id` | `absence_id` |
|
|||
|
|
|
|||
|
|
## Приложение B: ссылки на PHP
|
|||
|
|
|
|||
|
|
| Логика | Файл |
|
|||
|
|
|--------|------|
|
|||
|
|
| addFromCalendar | `themes/merakomis/time/controller.php:43-128` |
|
|||
|
|
| Time _insert | `themes/merakomis/time/model.php:174-186` |
|
|||
|
|
| getTimeTable | `themes/merakomis/time/model.php:189-418` |
|
|||
|
|
| getMySummary | `themes/merakomis/time/model.php:75-172` |
|
|||
|
|
| setAbsence | `themes/merakomis/time/controller.php:130-145` |
|
|||
|
|
| Absence afterUadd | `themes/merakomis/time/absence/model.php:111-129` |
|
|||
|
|
| setDays | `themes/merakomis/time/absence/model.php:46-99` |
|
|||
|
|
| Cache::removeRows | `themes/merakomis/time/cache/model.php:75-87` |
|
|||
|
|
| Day::getWorkHoursByDate | `themes/merakomis/day/model.php:127-179` |
|
|||
|
|
| can_edit | `themes/merakomis/time/model.php:314-318` |
|
|||
|
|
| writeOtherTableWrite | `themes/merakomis/emp/Rules.php:626-647` |
|
|||
|
|
|
|||
|
|
## Приложение C: шпаргалка Identity model
|
|||
|
|
|
|||
|
|
| Вопрос | Ответ |
|
|||
|
|
|--------|-------|
|
|||
|
|
| Кто вносит запись? | `X-Acting-Emp-Id` (заголовок) |
|
|||
|
|
| За кого вносится запись в БД? | `emp_id` в body/query → `tMerakomisTime.emp` |
|
|||
|
|
| Не указал `emp_id` | Пишем за себя (`target = acting`) |
|
|||
|
|
| Менеджер за подчинённого | `X-Acting-Emp-Id: <менеджер>`, `emp_id: <подчинённый>` |
|
|||
|
|
| Где login/password? | Только во внешнем сервисе; Labor API их не принимает |
|
|||
|
|
| Как получить `emp_id` по логину? | `GET /api/employees` на стороне внешнего сервиса |
|