@hubex/mcp 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/guides.js +1 -0
- package/dist/tools/request.js +23 -8
- package/docs/guides/plannedtasks.md +165 -0
- package/docs/guides/roles.md +164 -8
- package/generated/index.dev.json +614 -89
- package/package.json +1 -1
- package/swagger/dev/WORK.json +18309 -17872
- package/swagger/dev/WSP.json +563 -180
package/dist/tools/guides.js
CHANGED
|
@@ -20,6 +20,7 @@ const TOPIC_GROUPS = [
|
|
|
20
20
|
taskcreate: "создание заявки: метаданные формы, обязательные поля, справочники, порядок вызовов",
|
|
21
21
|
taskedit: "редактирование заявки: правка полей, стадии и статусы, работы и материалы, удаление",
|
|
22
22
|
taskchecklists: "чек-листы в заявке: заполнение пунктов, вложения, сохранение и переход стадии",
|
|
23
|
+
plannedtasks: "плановые заявки: шаблон заявки, расписание PMP, активация, точки срабатывания",
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
{
|
package/dist/tools/request.js
CHANGED
|
@@ -37,23 +37,38 @@ export function applyDefaults(body, defaults) {
|
|
|
37
37
|
}
|
|
38
38
|
return { body: out, applied };
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Значение параметра без учёта регистра имени: в swagger HubEx объявленное имя
|
|
42
|
+
* и плейсхолдер в пути расходятся регистром (`taskTemplateID` против
|
|
43
|
+
* `{taskTemplateId}`), и модель может передать любой из вариантов.
|
|
44
|
+
*/
|
|
45
|
+
function lookupParam(params, name) {
|
|
46
|
+
if (Object.hasOwn(params, name))
|
|
47
|
+
return params[name];
|
|
48
|
+
const lower = name.toLowerCase();
|
|
49
|
+
const key = Object.keys(params).find((k) => k.toLowerCase() === lower);
|
|
50
|
+
return key === undefined ? undefined : params[key];
|
|
51
|
+
}
|
|
52
|
+
export function resolvePath(entry, pathParams) {
|
|
41
53
|
const missing = entry.pathParams.filter((name) => {
|
|
42
|
-
const v = pathParams
|
|
54
|
+
const v = lookupParam(pathParams, name);
|
|
43
55
|
return v === undefined || v === null || v === "";
|
|
44
56
|
});
|
|
45
57
|
if (missing.length > 0) {
|
|
46
58
|
throw new Error(`Не хватает обязательных path-параметров для ${entry.id}: ${missing.join(", ")}. ` +
|
|
47
59
|
`Передай их в объекте pathParams.`);
|
|
48
60
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
const declared = new Map(entry.pathParams.map((name) => [name.toLowerCase(), name]));
|
|
62
|
+
const path = entry.path.replace(/\{([^}]+)\}/g, (placeholder, name) => {
|
|
63
|
+
const declaredName = declared.get(name.toLowerCase());
|
|
64
|
+
if (declaredName === undefined)
|
|
65
|
+
return placeholder;
|
|
66
|
+
return encodeURIComponent(String(lookupParam(pathParams, declaredName)));
|
|
67
|
+
});
|
|
53
68
|
const unresolved = path.match(/\{[^}]+\}/g);
|
|
54
69
|
if (unresolved) {
|
|
55
70
|
throw new Error(`Не удалось подставить параметры пути для ${entry.id}: остались незаменённые ${unresolved.join(", ")}. ` +
|
|
56
|
-
`Объявленные параметры: ${entry.pathParams.join(", ")}.
|
|
71
|
+
`Объявленные параметры: ${entry.pathParams.join(", ")}. В swagger этого эндпоинта плейсхолдер не объявлен — сверь схему через hubex_describe_endpoint.`);
|
|
57
72
|
}
|
|
58
73
|
return path;
|
|
59
74
|
}
|
|
@@ -83,7 +98,7 @@ function requestTool(group, deps) {
|
|
|
83
98
|
},
|
|
84
99
|
pathParams: {
|
|
85
100
|
type: "object",
|
|
86
|
-
description: "Значения параметров пути, например { \"taskID\": 42 }.",
|
|
101
|
+
description: "Значения параметров пути, например { \"taskID\": 42 }. Регистр имён не важен.",
|
|
87
102
|
},
|
|
88
103
|
query: { type: "object", description: "Query-параметры запроса." },
|
|
89
104
|
headers: {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
topic: plannedtasks
|
|
3
|
+
sources:
|
|
4
|
+
- page: mainapp/src/modules/plannedTaskCreateForms.js
|
|
5
|
+
content_hash: bd3012b5664edf6a
|
|
6
|
+
- page: mainapp/src/modules/plannedTaskEditForms.js
|
|
7
|
+
content_hash: a760154e74211900
|
|
8
|
+
- page: mainapp/src/pages/plannedTaskEditForm/TopBar.jsx
|
|
9
|
+
content_hash: 5786556ba8917e33
|
|
10
|
+
- page: mainapp/src/utils/date.js
|
|
11
|
+
content_hash: 1fe8704c1a74013e
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Плановые заявки: шаблон и расписание
|
|
15
|
+
|
|
16
|
+
Плановая заявка — это **шаблон заявки** (WORK), к которому привязано
|
|
17
|
+
**расписание** (PMP). Пока расписание активно, HubEx сам создаёт заявки по
|
|
18
|
+
шаблону в точках срабатывания. Расписание хранится в PMP, а привязка к
|
|
19
|
+
шаблону и включение/выключение — в WORK.
|
|
20
|
+
|
|
21
|
+
Веб-клиент (MainApp) создаёт плановую заявку в таком порядке:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
POST WORK:POST:/TaskTemplates → [taskTemplateID]
|
|
25
|
+
POST WORK:POST:/TaskTemplateExcludedAssets → исключённые объекты (если есть)
|
|
26
|
+
POST WORK:POST:/TaskTemplates/{id}/assignment → исполнители (если не автоназначение)
|
|
27
|
+
POST PMP:POST:/Schedules → [scheduleID]
|
|
28
|
+
POST WORK:POST:/TaskTemplates/{id}/schedules → привязка расписания к шаблону
|
|
29
|
+
PUT WORK:PUT:/TaskTemplates/{taskTemplateId}/schedules/{scheduleId}/activate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Последний шаг веб-клиент выполняет только по кнопке «Запланировать»; кнопка
|
|
33
|
+
«Сохранить» оставляет расписание **неактивным** — заявки не создаются.
|
|
34
|
+
|
|
35
|
+
## 1. Шаблон заявки
|
|
36
|
+
|
|
37
|
+
`WORK:POST:/TaskTemplates` принимает массив, ответ — массив идентификаторов.
|
|
38
|
+
Поля, которые заполняет веб-клиент:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
[
|
|
42
|
+
{
|
|
43
|
+
"name": "Сменный отчёт — день",
|
|
44
|
+
"taskTypeID": 3,
|
|
45
|
+
"workTypeID": 12,
|
|
46
|
+
"criticalityID": 2,
|
|
47
|
+
"code": "SHIFT-DAY",
|
|
48
|
+
"notes": "Описание, попадёт в заявку",
|
|
49
|
+
"estimatedTimeConsumptionMinutes": 60,
|
|
50
|
+
"estimatedCost": 0,
|
|
51
|
+
"requestMethodID": 1,
|
|
52
|
+
"assetFilter": { "assets": [101, 102] }
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `assetFilter` — по каким объектам создавать заявки: `assets`,
|
|
58
|
+
`assetTypes`, `assetClasses`, `companies`, `districts`, `responsiblePersons`,
|
|
59
|
+
`workTypes`, `taskTypes` (массивы id), `warrantyPeriod` (`from`/`till`).
|
|
60
|
+
- Исключить объекты из фильтра — `WORK:POST:/TaskTemplateExcludedAssets`,
|
|
61
|
+
тело `[{ "taskTemplateID": "<id>", "data": [assetID, ...] }]`.
|
|
62
|
+
- Исполнители — `WORK:POST:/TaskTemplates/{id}/assignment`, тело — массив
|
|
63
|
+
`userID`. Пустой массив `[]` снимает назначенных ранее исполнителей; так
|
|
64
|
+
веб-клиент включает автоназначение.
|
|
65
|
+
- Изменение — `WORK:PUT:/TaskTemplates` (массив с `id`), чтение —
|
|
66
|
+
`WORK:GET:/TaskTemplates/{id}`.
|
|
67
|
+
|
|
68
|
+
## 2. Расписание
|
|
69
|
+
|
|
70
|
+
`PMP:POST:/Schedules` создаёт **или обновляет** расписания: элемент без `id`
|
|
71
|
+
создаёт новое, с `id` — перезаписывает существующее. Ответ — массив
|
|
72
|
+
идентификаторов.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
[
|
|
76
|
+
{
|
|
77
|
+
"frequencyTypeID": 2,
|
|
78
|
+
"options": {
|
|
79
|
+
"Name": "Daily",
|
|
80
|
+
"DateFrom": "2026-10-20T00:00:00",
|
|
81
|
+
"DateTill": "2027-10-20T00:00:00",
|
|
82
|
+
"Frequency": { "AppointmentTimes": ["05:00"], "EachDayNumber": [1] }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Формат `options` бери отсюда, а не из swagger.** В swagger поле объявлено
|
|
89
|
+
строкой с примером `{"interval":1,"timeOfDay":"10:00:00"}`, но веб-клиент
|
|
90
|
+
такой формат не пишет и не читает. Он передаёт объект с ключами в PascalCase:
|
|
91
|
+
|
|
92
|
+
- `Name` — код частоты: `DuringTheDay`, `Daily`, `Weekly`, `Monthly`
|
|
93
|
+
(`MonthlyLastDay`, если выбран последний день месяца), `Yearly`.
|
|
94
|
+
- `DateFrom`, `DateTill` — период действия, `YYYY-MM-DDTHH:mm:ss`. Без
|
|
95
|
+
периода веб-клиент подставляет «сегодня + 1 год».
|
|
96
|
+
- `Frequency.AppointmentTimes` — время срабатывания `HH:mm` **в UTC**
|
|
97
|
+
(см. «Подводные камни»).
|
|
98
|
+
- `TaskCreationOffset` — необязательно: за сколько до срабатывания создавать
|
|
99
|
+
заявку, `часы:мм:сс` (`2:00:00`, за двое суток — `48:00:00`).
|
|
100
|
+
- `AppointmentsLimit` — необязательно: сколько раз сработать всего.
|
|
101
|
+
|
|
102
|
+
Остальные поля `Frequency` зависят от типа частоты
|
|
103
|
+
(`PMP:GET:/FrequencyTypes`):
|
|
104
|
+
|
|
105
|
+
| `frequencyTypeID` | `Name` | Поля `Frequency` кроме `AppointmentTimes` |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| 1 | `DuringTheDay` | — (`DateFrom` и `DateTill` — один день, несколько времён) |
|
|
108
|
+
| 2 | `Daily` | `EachDayNumber: [N]` — каждые N дней |
|
|
109
|
+
| 3 | `Weekly` | `EachDayNumber` — дни недели (1 — пн … 7 — вс), `EachWeekNumber: [N]` — каждые N недель |
|
|
110
|
+
| 4 | `Monthly` | `EachDayNumber` — числа месяца (0 — последний день, тогда ещё `EachLastDayNumber: [0]`), `EachMonthNumber: [N]` — каждые N месяцев |
|
|
111
|
+
| 5 | `Yearly` | `EachDayNumber` — числа, `EachMonthNumber` — месяцы (1–12), `EachYearNumber: [N]` — каждые N лет; последний день — как у `Monthly` |
|
|
112
|
+
|
|
113
|
+
Чтение — `PMP:GET:/Schedules/{id}`: `options` возвращается тем же объектом,
|
|
114
|
+
тип частоты — в поле `frequencyType` объектом `{ id, name }`.
|
|
115
|
+
|
|
116
|
+
## 3. Привязка, активация, проверка
|
|
117
|
+
|
|
118
|
+
- `WORK:POST:/TaskTemplates/{id}/schedules` — тело — массив `scheduleID`
|
|
119
|
+
(ответ `PMP:POST:/Schedules` передаётся как есть), ответ — 202 без тела.
|
|
120
|
+
При изменении расписания через `PMP:POST:/Schedules` с `id` привязку
|
|
121
|
+
повторять не нужно.
|
|
122
|
+
- `WORK:GET:/TaskTemplates/{id}/schedules` — привязанные расписания:
|
|
123
|
+
`state.isActive`, а у активного ещё `appointment.total`/`remaining` и
|
|
124
|
+
`appointment.next` — ближайшее срабатывание (UTC).
|
|
125
|
+
- Включить — `WORK:PUT:/TaskTemplates/{taskTemplateId}/schedules/{scheduleId}/activate`,
|
|
126
|
+
выключить — `WORK:PUT:/TaskTemplates/{taskTemplateId}/schedules/{scheduleId}/deactivate`.
|
|
127
|
+
Тело пустое. `activate` отвечает `{ "isActive": true, "nextAppointment": "..." }`
|
|
128
|
+
(время в UTC), `deactivate` — 202 без тела. Веб-клиент выключает и включает
|
|
129
|
+
расписание той же кнопкой.
|
|
130
|
+
- Точки срабатывания — `PMP:GET:/Schedules/{scheduleID}/appointments` с
|
|
131
|
+
`dateFrom`/`dateTill`, ответ — массив `{ id, appointment }` (UTC); 204 —
|
|
132
|
+
событий в периоде нет. События появляются только после активации: у
|
|
133
|
+
неактивного расписания `appointment.total` равно 0.
|
|
134
|
+
- `WORK:POST:/TaskTemplates/{taskTemplateId}/schedules/{scheduleId}/appointments`
|
|
135
|
+
перегенерирует события. Веб-клиент его не вызывает: активации достаточно.
|
|
136
|
+
|
|
137
|
+
## Последовательность MCP-вызовов
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
hubex_request_read(endpointId: `PMP:GET:/FrequencyTypes`) → frequencyTypeID
|
|
141
|
+
hubex_request_write(endpointId: `WORK:POST:/TaskTemplates`, body: [{ name, taskTypeID, workTypeID, criticalityID, requestMethodID: 1, assetFilter }])
|
|
142
|
+
→ [taskTemplateID]
|
|
143
|
+
hubex_request_write(endpointId: `PMP:POST:/Schedules`, body: [{ frequencyTypeID, options: { Name, DateFrom, DateTill, Frequency } }])
|
|
144
|
+
→ [scheduleID]
|
|
145
|
+
hubex_request_write(endpointId: `WORK:POST:/TaskTemplates/{id}/schedules`, pathParams: { id: taskTemplateID }, body: [scheduleID])
|
|
146
|
+
hubex_request_write(endpointId: `WORK:PUT:/TaskTemplates/{taskTemplateId}/schedules/{scheduleId}/activate`, pathParams: { taskTemplateId, scheduleId })
|
|
147
|
+
hubex_request_read(endpointId: `PMP:GET:/Schedules/{scheduleID}/appointments`, pathParams: { scheduleID }, query: { dateFrom, dateTill })
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Подводные камни
|
|
151
|
+
|
|
152
|
+
- **`AppointmentTimes` — в UTC.** Веб-клиент переводит введённое локальное
|
|
153
|
+
время в UTC перед сохранением и обратно при показе. Заявка в 08:00 по
|
|
154
|
+
Москве (UTC+3) — это `"05:00"`; записанное буквально `"08:00"` сработает в
|
|
155
|
+
11:00 по Москве.
|
|
156
|
+
- Созданное, но не активированное расписание заявок не создаёт. После
|
|
157
|
+
сохранения проверь `state.isActive` и точки срабатывания.
|
|
158
|
+
- Не бери `options` из swagger-примера (`interval`, `timeOfDay`,
|
|
159
|
+
`daysOfWeek`): веб-клиент не покажет и не отредактирует такое расписание.
|
|
160
|
+
Используй ключи из раздела 2 и сверь результат по точкам срабатывания.
|
|
161
|
+
- `PMP:POST:/Schedules` с чужим `id` перезапишет чужое расписание: при
|
|
162
|
+
изменении сначала прочитай его через `PMP:GET:/Schedules/{id}`.
|
|
163
|
+
- Имена параметров пути у `activate`/`deactivate` в swagger расходятся
|
|
164
|
+
регистром с шаблоном пути (`taskTemplateID` против `{taskTemplateId}`).
|
|
165
|
+
Сервер MCP принимает любой из вариантов.
|
package/docs/guides/roles.md
CHANGED
|
@@ -3,6 +3,12 @@ topic: roles
|
|
|
3
3
|
sources:
|
|
4
4
|
- page: adminapp/src/modules/RolesPage/saga.js
|
|
5
5
|
content_hash: dd2943597b30a21c
|
|
6
|
+
- page: adminapp/src/modules/RolesPage/helpers.js
|
|
7
|
+
content_hash: 5d278fab266691e4
|
|
8
|
+
- page: adminapp/src/modules/UIPermissionModal/containers/UIPermissionModalContainer.js
|
|
9
|
+
content_hash: ae0bbd7e29704a68
|
|
10
|
+
- page: adminapp/src/modules/InterfaceElements/actions.js
|
|
11
|
+
content_hash: 07877111bb1d1ceb
|
|
6
12
|
- page: adminapp/src/services/requests.js
|
|
7
13
|
content_hash: 54c3d95d85bb0ce0
|
|
8
14
|
- page: adminapp/src/locales/ru/app.json
|
|
@@ -46,11 +52,33 @@ POST ADM:POST:/RoleApplications → доступные приложения
|
|
|
46
52
|
ответ `201` — массив идентификаторов. Изменение названия и описания —
|
|
47
53
|
`ADM:PUT:/Roles` (массив с `id`), удаление — `ADM:DELETE:/Roles/{id}`.
|
|
48
54
|
|
|
49
|
-
Тела привязок — массивы объектов
|
|
50
|
-
|
|
55
|
+
Тела привязок — массивы объектов «роль + список ID» в поле `data`, а не по
|
|
56
|
+
объекту на каждое полномочие:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
// API, расширенные полномочия, приложения
|
|
60
|
+
[{ "roleID": 7, "data": [10, 11] }]
|
|
61
|
+
// UI-полномочия — с уровнем доступа, по объекту на каждый capabilityID
|
|
62
|
+
[{ "roleID": 7, "capabilityID": 1, "data": [10] },
|
|
63
|
+
{ "roleID": 7, "capabilityID": 2, "data": [11, 12] }]
|
|
64
|
+
```
|
|
65
|
+
|
|
51
66
|
Снятие полномочия — соответствующий `ADM:DELETE:/RolePermissionsApi`,
|
|
52
67
|
`ADM:DELETE:/RolePermissionsUi`, `ADM:DELETE:/RolePermissionsExt`,
|
|
53
|
-
`ADM:DELETE:/RoleApplications` с
|
|
68
|
+
`ADM:DELETE:/RoleApplications` с телом того же вида: `[{ roleID, data: [ids] }]`.
|
|
69
|
+
|
|
70
|
+
### Изменение полномочий существующей роли
|
|
71
|
+
|
|
72
|
+
Веб-клиент не перезаписывает набор целиком, а отправляет разницу:
|
|
73
|
+
|
|
74
|
+
1. Прочитать текущее состояние: `ADM:GET:/Roles/{roleID}/permissionsUi` с
|
|
75
|
+
`isCheckedPermission=false` (и `isSystemPermission=true`, как в веб-клиенте).
|
|
76
|
+
Выбранные полномочия — с `isChecked: true`, уровень — в `capabilityID`.
|
|
77
|
+
2. Включённые полномочия — `POST` с `data` только из новых ID.
|
|
78
|
+
3. Выключенные — `DELETE` с `data` только из снимаемых ID.
|
|
79
|
+
4. Смена уровня UI-полномочия (например, RO → RW) — повторный
|
|
80
|
+
`ADM:POST:/RolePermissionsUi` с новым `capabilityID`; веб-клиент перед этим
|
|
81
|
+
ничего не удаляет.
|
|
54
82
|
|
|
55
83
|
## 2. Роль по шаблону и копирование
|
|
56
84
|
|
|
@@ -83,14 +111,27 @@ PUT ADM:PUT:/PermissionsUi
|
|
|
83
111
|
DELETE ADM:DELETE:/PermissionsUi
|
|
84
112
|
```
|
|
85
113
|
|
|
114
|
+
`ADM:PUT:/PermissionsUi` принимает массив **полных** записей:
|
|
115
|
+
`[{ "id": 12, "code": "MyPerm", "description": "...", "mustBeAssignedToRole": false, "allowReadonlyOnly": false }]`.
|
|
116
|
+
Поля `id`, `code`, `mustBeAssignedToRole` и `allowReadonlyOnly` обязательны.
|
|
117
|
+
В примере из описания эндпоинта их меньше: тело, собранное по этому примеру,
|
|
118
|
+
сервер отклонит с 400. Удаление — `ADM:DELETE:/PermissionsUi` с массивом ID:
|
|
119
|
+
`[12, 13]`.
|
|
120
|
+
|
|
86
121
|
## 5. Доступ роли к полям заявки
|
|
87
122
|
|
|
88
|
-
Видимость полей формы заявки настраивается по паре «роль +
|
|
123
|
+
Видимость полей формы заявки настраивается по паре «роль + стадия». Доступ
|
|
124
|
+
задаётся **роли**, не пользователю: чтобы поменять поле «для пользователя N»,
|
|
125
|
+
сначала узнай его роли через `ADM:GET:/Users/{id}/roles`. Если ролей несколько,
|
|
126
|
+
действует правило максимума (см. «Подводные камни»).
|
|
89
127
|
|
|
90
|
-
- `
|
|
91
|
-
`
|
|
128
|
+
- `UI:GET:/Components` — справочник основных полей формы (`id`, `code`,
|
|
129
|
+
`description`), например `56` — `companyID`, «Компания-заказчик».
|
|
130
|
+
- `TSTG:GET:/TaskStageComponents/availability` с query `roleID`, `taskStageID`
|
|
131
|
+
(оба можно повторять) — текущая доступность: массивы `components` и
|
|
132
|
+
`attributes`, у каждого элемента `availability.capabilityID`.
|
|
92
133
|
- `TSTG:POST:/TaskStageComponents` — запись доступности:
|
|
93
|
-
`[{ "taskStageID":
|
|
134
|
+
`[{ "taskStageID": 27, "components": [{ "id": 56, "roleID": 3, "capabilityID": 2 }, ...], "attributes": [...] }]`.
|
|
94
135
|
- Справочник уровней доступа — `ADM:GET:/Capabilities`. Отдаёт пять записей с
|
|
95
136
|
полем `weightCoefficient`: `RO`=1 (только чтение), `RW`=2 (чтение и запись),
|
|
96
137
|
`RWM`=3 (обязательная запись), `RWE`=4 (эксклюзивная запись), `RWME`=5
|
|
@@ -101,6 +142,102 @@ DELETE ADM:DELETE:/PermissionsUi
|
|
|
101
142
|
`ADM:POST:/RoleTaskPropertiesAccess/attributes`,
|
|
102
143
|
`ADM:PUT:/RoleTaskPropertiesAccess/attributes`.
|
|
103
144
|
|
|
145
|
+
### Как устроена запись
|
|
146
|
+
|
|
147
|
+
Под капотом каждое поле стадии — это UI-полномочие с автоматическим кодом, а
|
|
148
|
+
доступ роли к полю — привязка роли к этому полномочию с уровнем `capabilityID`:
|
|
149
|
+
|
|
150
|
+
- основное поле: `taskForm$<стадия, 5 цифр>@<code компонента>`, например
|
|
151
|
+
`taskForm$00027@companyID`;
|
|
152
|
+
- дополнительное поле: `taskForm$<стадия>@<attributeID, 5 цифр>`, например
|
|
153
|
+
`taskForm$00027@00011`;
|
|
154
|
+
- правило для одного типа заявки: `taskForm$00027-007@companyID` (тип 7).
|
|
155
|
+
|
|
156
|
+
Эти полномочия видны в `ADM:GET:/Roles/{roleID}/permissionsUi` (с
|
|
157
|
+
`isCheckedPermission`) и в `ADM:GET:/Users/this/permissions/ui`.
|
|
158
|
+
|
|
159
|
+
**POST заменяет список, а не сливает.** Перед записью бэкенд дополняет тело
|
|
160
|
+
всеми компонентами, которые сейчас есть у роли на этой стадии, но не переданы,
|
|
161
|
+
с `capabilityID: null`, а `capabilityID: null` означает «снять доступ у этой
|
|
162
|
+
роли». Итог: переданный массив `components` становится полным списком
|
|
163
|
+
компонентов роли на стадии, остальные поля пропадают из заявки. С `attributes`
|
|
164
|
+
то же самое. Если ключа `attributes` в теле нет вовсе, дополнительные поля не
|
|
165
|
+
трогаются. Другие роли и стадии не затрагиваются.
|
|
166
|
+
|
|
167
|
+
**`permissionUiID` не передавай.** Бэкенд подбирает полномочие сам по коду
|
|
168
|
+
`taskForm$…` (и создаёт его, если такого нет). Переданный ID лишь проверяется
|
|
169
|
+
на существование — несуществующий даёт ошибку, — после чего заменяется
|
|
170
|
+
найденным по коду; для записей с `taskTypeID` поле отбрасывается сразу.
|
|
171
|
+
Пример в описании эндпоинта с `"permissionUiID": 10` вводит в заблуждение.
|
|
172
|
+
|
|
173
|
+
**`taskTypeID` не передавай без явной задачи.** Без него правило действует для
|
|
174
|
+
всех типов заявок на стадии — так пишет и веб-клиент («Элементы интерфейса»).
|
|
175
|
+
С `taskTypeID`:
|
|
176
|
+
|
|
177
|
+
- бэкенд заводит отдельное полномочие `taskForm$<стадия>-<тип>@<code>` и
|
|
178
|
+
отдельную строку поля для этого типа;
|
|
179
|
+
- для заявок этого типа мета формы берёт **только** эту строку, а общее
|
|
180
|
+
правило игнорирует — для **всех ролей**. Роль, которой не выдали новое
|
|
181
|
+
полномочие, теряет поле на заявках этого типа, даже если в общем правиле
|
|
182
|
+
у неё был доступ;
|
|
183
|
+
- `taskTypeID` вместе с `capabilityID: null` создаёт строку-«заглушку»:
|
|
184
|
+
поле для типа есть, а полномочие не выдано никому, и поле скрыто у всех на
|
|
185
|
+
заявках этого типа;
|
|
186
|
+
- повторный POST с тем же `taskTypeID` правило для типа не убирает.
|
|
187
|
+
|
|
188
|
+
Чтобы снять правило для типа, отправь общий POST **без** `taskTypeID` с полным
|
|
189
|
+
списком компонентов роли: бэкенд снимет привязки роли к полномочиям
|
|
190
|
+
`taskForm$<стадия>-<тип>@…` по тем же полям и удалит строку для типа, если её
|
|
191
|
+
полномочие больше ни у одной роли не осталось. Само полномочие остаётся в
|
|
192
|
+
справочнике `ADM:GET:/PermissionsUi`, но ни на что не влияет.
|
|
193
|
+
|
|
194
|
+
### Изменение одного поля
|
|
195
|
+
|
|
196
|
+
1. Снимок до записи: `availability` для нужных `roleID` и `taskStageID` и
|
|
197
|
+
`ADM:GET:/Roles/{roleID}/permissionsUi` с `isCheckedPermission=true` —
|
|
198
|
+
строки `taskForm$<стадия>…` с `capabilityID`. Без снимка не пиши: откатить
|
|
199
|
+
будет не к чему.
|
|
200
|
+
2. Взять **все** компоненты роли из `availability`, поменять `capabilityID` у
|
|
201
|
+
нужного и отправить полный массив без `taskTypeID` и `permissionUiID`. Если
|
|
202
|
+
меняются и дополнительные поля, так же отправить полный массив `attributes`.
|
|
203
|
+
3. Проверить результат (см. ниже). Откат — тот же POST с полным списком и
|
|
204
|
+
`capabilityID` из снимка.
|
|
205
|
+
|
|
206
|
+
### Проверка результата
|
|
207
|
+
|
|
208
|
+
`availability` показывает только то, что записано, но не то, что увидит
|
|
209
|
+
пользователь. Проверяй два места:
|
|
210
|
+
|
|
211
|
+
- `ADM:GET:/Roles/{roleID}/permissionsUi` — у полномочия
|
|
212
|
+
`taskForm$<стадия>@<code>` стоит `isChecked: true` и нужный `capabilityID`, и
|
|
213
|
+
нет посторонних `taskForm$<стадия>-<тип>@…`. Это работает для любой роли.
|
|
214
|
+
- `WORK:GET:/Tasks/{taskID}/meta` — в `components` есть нужный `code` с
|
|
215
|
+
`permission` и `capability`. Мета строится для текущего пользователя, а MCP
|
|
216
|
+
работает под одной учёткой, поэтому мету для пользователя с другой ролью
|
|
217
|
+
через MCP не проверить.
|
|
218
|
+
|
|
219
|
+
Клиенты собирают права поля из двух ответов: мета заявки даёт код полномочия
|
|
220
|
+
компонента, `ADM:GET:/Users/this/permissions/ui` — уровень по этому коду. Если
|
|
221
|
+
компонента нет в мете, поле скрыто. Мобильное приложение перезапрашивает
|
|
222
|
+
UI-права при входе и обновлении токена, поэтому после правки может
|
|
223
|
+
понадобиться перелогин.
|
|
224
|
+
|
|
225
|
+
### Уровни доступа в мете формы
|
|
226
|
+
|
|
227
|
+
Мета формы (`WORK:GET:/Tasks/{taskID}/meta`) отдаёт компонент, только если у
|
|
228
|
+
пользователя есть его полномочие хотя бы через одну роль, а уровень берёт
|
|
229
|
+
максимальный по весу. `RWE`/`RWME` бэкенд пересчитывает сам:
|
|
230
|
+
|
|
231
|
+
- исполнителю заявки (или пользователю с расширенным полномочием
|
|
232
|
+
«игнорировать эксклюзивные права») — `RW`/`RWM`;
|
|
233
|
+
- остальным, если исполнитель назначен, — `RO`;
|
|
234
|
+
- у заявки без исполнителя код уходит как есть — `RWE`/`RWME`.
|
|
235
|
+
|
|
236
|
+
Веб-клиент показывает поле только при `RO`, `RW`, `ROM` или `RWM`, поэтому на
|
|
237
|
+
заявке без исполнителя поле с `RWE`/`RWME` скрыто. Чтобы поле было видно и
|
|
238
|
+
редактировалось у всех, ставь `2` (`RW`), чтобы было обязательным — `3`
|
|
239
|
+
(`RWM`).
|
|
240
|
+
|
|
104
241
|
## Последовательность MCP-вызовов
|
|
105
242
|
|
|
106
243
|
```text
|
|
@@ -108,12 +245,31 @@ hubex_request_read(endpointId: `ADM:GET:/Roles`)
|
|
|
108
245
|
hubex_request_write(endpointId: `ADM:POST:/Roles`, body: [{ name, description }])
|
|
109
246
|
→ response[0] = roleID
|
|
110
247
|
hubex_request_read(endpointId: `ADM:GET:/PermissionsUi`)
|
|
111
|
-
hubex_request_write(endpointId: `ADM:POST:/RolePermissionsUi`, body: [{ roleID, ... }])
|
|
248
|
+
hubex_request_write(endpointId: `ADM:POST:/RolePermissionsUi`, body: [{ roleID, capabilityID: 2, data: [permissionUiID, ...] }])
|
|
112
249
|
hubex_request_read(endpointId: `ADM:GET:/Roles/{roleID}/permissionsUi`, pathParams: { roleID }, query: { isCheckedPermission: false })
|
|
113
250
|
```
|
|
114
251
|
|
|
252
|
+
Доступ к полю заявки для пользователя:
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
hubex_request_read(endpointId: `ADM:GET:/Users/{id}/roles`, pathParams: { id: userID })
|
|
256
|
+
hubex_request_read(endpointId: `UI:GET:/Components`, query: { fetch: 500, offset: 0 })
|
|
257
|
+
hubex_request_read(endpointId: `TSTG:GET:/TaskStageComponents/availability`, query: { roleID, taskStageID })
|
|
258
|
+
hubex_request_read(endpointId: `ADM:GET:/Roles/{roleID}/permissionsUi`, pathParams: { roleID }, query: { isCheckedPermission: true })
|
|
259
|
+
hubex_request_write(endpointId: `TSTG:POST:/TaskStageComponents`, body: [{ taskStageID, components: [<ВСЕ компоненты роли из ответа, одно изменено>] }])
|
|
260
|
+
hubex_request_read(endpointId: `ADM:GET:/Roles/{roleID}/permissionsUi`, pathParams: { roleID }, query: { isCheckedPermission: true })
|
|
261
|
+
hubex_request_read(endpointId: `WORK:GET:/Tasks/{taskID}/meta`, pathParams: { taskID })
|
|
262
|
+
```
|
|
263
|
+
|
|
115
264
|
## Подводные камни
|
|
116
265
|
|
|
266
|
+
- `TSTG:POST:/TaskStageComponents` с одним компонентом в `components` стирает
|
|
267
|
+
у роли на этой стадии все остальные компоненты: из заявки пропадают все поля,
|
|
268
|
+
кроме переданного. Всегда отправляй полный список, прочитанный из
|
|
269
|
+
`availability`.
|
|
270
|
+
- `taskTypeID` в `TSTG:POST:/TaskStageComponents` перекрывает общее правило
|
|
271
|
+
поля для всех ролей на заявках этого типа: у ролей без нового полномочия поле
|
|
272
|
+
пропадает. С `capabilityID: null` поле скрывается у всех.
|
|
117
273
|
- Создание роли не даёт ей ни одного полномочия: без последующих
|
|
118
274
|
`RolePermissions*` пользователь с этой ролью не увидит ничего.
|
|
119
275
|
- Четыре набора полномочий независимы. Выдать API-полномочие и забыть про
|