# Паттерны администрирования ДП

Каталог паттернов администрирования дополнительных параметров (ДП) в категориях задач. Охватывает JSON-форматы значений для ДП «Выбор пользователя» и «Таблица», операции с мультилукапами, настройку связей между ДП, управление доступами через SQL-функции и базовые конфигурационные настройки ДП в категории. Каждый паттерн содержит условие применения и готовый код или описание настройки. Формат записи: паттерн, когда применять, код — без tutorial-нарратива. Паттерны сгруппированы по типу ДП и области применения.

## ДП «Выбор пользователя» — JSON-формат и SQL-паттерны

Значение ДП «Выбор пользователя» передается строкой JSON. Три ветви: пользователи, группы, орг. единицы. Каждая содержит массивы `Added` и `Deleted` с ID.

```
'{"Users":{"Deleted":[XX,...,XX],"Added":[XX,...,XX]},"Groups":{"Deleted":[XX,...,XX],"Added":[XX,...,XX]},"OrgUnits":{"Deleted":[XX,...,XX],"Added":[XX,...,XX]}}'
```

Для добавления пользователей при постановке или переходе задачи указывается список ID в массиве `Added`:

```sql
'{"Users":{"Deleted":[],"Added":[2110,2122]},"Groups":{"Deleted":[],"Added":[]},"OrgUnits":{"Deleted":[],"Added":[]}}'
```

Для назначения орг. единиц используются строковые ID. Напрямую по ID:
```sql
'{"Users":{"Deleted":[],"Added":[]},"Groups":{"Deleted":[],"Added":[]},"OrgUnits":{"Deleted":[],"Added":["1234","5678"]}}'
```

Через текстовый ДП, содержащий ID:
```sql
'{"Users":{"Deleted":[],"Added":[]},"Groups":{"Deleted":[],"Added":[]},"OrgUnits":{"Deleted":[],"Added":["' + ДП_ID_орг.единицы + '"]}}'
```

Для удаления пользователей из ДП при переходе (когда нужно оставить только определённых пользователей, например только ответственного) используется смарт-выражение: функция «Сконкатенировать в строку» для формирования списка ID удаляемых пользователей, помещаемого в `"Deleted"`.

### SQL-паттерн: STRING_AGG для динамического заполнения

Когда список пользователей собирается SQL-запросом по бизнес-логике (например менеджеры проекта по округу), применяется `STRING_AGG`:

```sql
DECLARE @managers VARCHAR(50) = ''
SET @managers =
    (SELECT STRING_AGG(UO.UserID, ',')
    FROM TasksInSubcat53Denormalized OPVZ WITH (NOLOCK)
        JOIN TasksInSubcat52Denormalized PPVZ WITH (NOLOCK)
            ON OPVZ.extparam174nativevalue = PPVZ.taskid
        JOIN TasksInSubcat47Denormalized OKR WITH (NOLOCK)
            ON OKR.taskid = PPVZ.extparam143nativevalue
        JOIN ExtParamSelectUsersValuesOrgUnits RO WITH (NOLOCK)
            ON OKR.taskid = RO.taskid AND RO.extparamid = 150
        JOIN OrgStructureUnit OU WITH (NOLOCK)
            ON OU.id = RO.OrgStructureUnitID
        JOIN OrgStructureUnit OU2 WITH (NOLOCK)
            ON OU.ParentId = OU2.Id
        JOIN OrgStructureUnit OU3 WITH (NOLOCK)
            ON OU3.ParentId = OU2.Id AND OU3.IsDirector <> 1
        JOIN UserOrgStructureUnit UO WITH (NOLOCK)
            ON UO.OrgStructureUnitId = OU3.id
    WHERE OPVZ.taskid = @contextid)

SELECT
'{"Users":{"Deleted":[],"Added":['+@managers+']},"Groups":{"Deleted":[],"Added":[]},
"OrgUnits":{"Deleted":[],"Added":[]}}'
```

Ключевой прием: `STRING_AGG(UserID, ',')` формирует строку `'2110,2122,2131'`, которая подставляется в JSON.

## ДП «Таблица» — JSON-наполнение и CRUD-операции

Добавление строки в таблицу (например наполнение проектной команды): указываются только заполняемые колонки, пустые можно пропустить, массив заключается в `[ ]`.

```
'+[{ID_колонки:{"First":"значение"}, ID_колонки2:{"First":"значение2"}}]'
```

Пример — добавить номер задачи в колонку 3:
```sql
'+[{3:{"First":"' + CAST(TaskID AS varchar(10)) + '"}}]'
```

Для записи списка в ячейку (например несколько пользователей в колонку «Ответственный») используется `STRING_AGG`:

```sql
SELECT DISTINCT
    '+[{5:{"First":"' + CAST(t1.TaskID AS varchar(10)) + '"},'
    + '6:{"First":"'
    + (SELECT STRING_AGG(CAST(t2.UserID AS varchar(10)), ',')
       FROM CTE t2
       WHERE t1.TaskID = t2.TaskID)
    + '"}}]'
FROM CTE t1;
```

Обновление существующей строки: изменяются только указанные колонки, остальные сохраняются.

```
'={"First":"ID_строки","Second":{ID_колонки:{"First":"новое_значение"}}}'
```

Пример — обновить колонку 6 (ответственный) в существующей строке:
```sql
SELECT '={"First":"' + CAST(t1.RowID AS varchar(4)) + '",'
    + '"Second":{6:{"First":"'
    + STUFF((SELECT ',' + ISNULL(CAST(t2.UserID AS varchar(10)), '')
             FROM CTE2 t2
             WHERE t1.TaskID = t2.TaskID
             FOR XML PATH('')), 1, 1, '')
    + '"}}}'
FROM CTE2 t1
```

Важно: `ID_строки` берется в кавычки (это строковое значение в JSON), `ID_колонки` — нет (это ключ JSON-объекта, числовой).

Удаление строки из таблицы требует только ID строки:

```
'-ID_строки'
```

ID строки НЕ берется в кавычки (здесь это не JSON-значение, а строковый литерал с префиксом `-`).

Полная перезапись таблицы выполняется операторами `#` и `|`, когда нужно обновить все строки одной операцией:

| Оператор | Поведение |
|----------|-----------|
| `\|` (pipe) | Удаляет ВСЕ строки, затем создает из переданных данных |
| `#` | Удаляет отсутствующие, добавляет новые, обновляет совпадающие (diff) |

Формат одинаковый:
```
'#{"ID_строки":{ID_колонки:{"First":XX},...},...}'
'|{"ID_строки":{ID_колонки:{"First":XX},...},...}'
```

ID строки берется в кавычки, ID колонки — нет.

## Мультилукапы — добавление, удаление и автозаполнение

Добавление одной записи в мультилукап при смене ДП или статуса выполняется конвертацией ID задачи в строку:

```
ВСтроку(ID_задачи)
```

Удаление одной записи из мультилукапа при переходе в неактивный статус использует префикс `-` перед ID:

```
'-' + ВСтроку(ID_задачи)
```

Префикс `-` перед ID означает удаление из мультилукапа.

Автозаполнение мультилукапа по условию (например все контактные лица контрагента после постановки задачи) настраивается через смарт-правило «После постановки» / «После смены ДП». Действие: «Изменить значение ДП». Значение: выбрать все задачи из категории-источника, где ДП = значению ДП текущей задачи.

## Связи ДП — жесткая и слабая фильтрация

Жесткая связь (parent-child) делает подчиненный ДП доступным для выбора ТОЛЬКО если заполнен родительский (например Округ → Регион → Город). Настройка: «Доп. параметры» >> «Связи доп. параметров» >> Создать.

| Параметр | Значение для Lookup -> Lookup |
|----------|-------------------------------|
| Источник данных | `ExtParamValue` |
| Колонка для отбора | `SelectedTaskID` |
| Значение подчиненного параметра | `TaskID` |
| Текст подчиненного параметра | `TaskID` |
| Жесткая связь | `True` |

Поведение: при пустом родительском ДП подчиненный пуст. При заполнении — только связанные значения.

Слабая (нежесткая) связь фильтрует подчиненный ДП по родительскому, но при пустом родительском показывает ВСЕ значения (например Контрагент → Контактное лицо). Настройка аналогична жесткой, но `Жесткая связь = False`.

## Доступы к ДП — уровни и SQL-функции

Уровни доступа к ДП определяются числовыми значениями:

| Значение | Право |
|----------|-------|
| 0 | Нет доступа |
| 1 | Чтение |
| 2 | Редактирование |

Скалярная SQL-функция доступа применяется, когда доступ к ДП зависит от роли пользователя в задаче (ответственный, руководитель) или статуса задачи. Принцип: начать с `@access = 0`, затем повышать от меньшего к большему. Входные параметры: `@TaskID`, `@ExtParamID`, `@UserID`, `@StateID`, `@IsResponsiblePerformer` и др. Одна функция может обслуживать несколько ДП категории по `@ExtParamID`.

### Пакетная SQL-функция доступа и альтернативы

Пакетная SQL-функция (batch) оптимизирует вызовы скалярной функции при отображении ДП в гриде, где скалярная функция вызывается N раз (на каждую задачу). Именование: `{имя_скалярной}_batch`.

Шаблон:
```sql
CREATE OR ALTER FUNCTION dbo.smartAccessBatch
(
  @taskIds dbo.id_tbl_type READONLY,
  @extParamId int,
  @userId int
)
RETURNS TABLE
AS
RETURN
   SELECT tid.Id TaskId, 2 Access
   FROM @taskIds tid
;
```

Вход: массив TaskID (`dbo.id_tbl_type`). Выход: таблица `(TaskId, Access)`.

Логика аналогична скалярной, но `IF` заменяется на `CASE`:

```sql
SELECT tid.Id AS TaskId,
    CASE
        WHEN @ExtParamId IN (166,177)
             AND ts.ResponsiblePerformerId = @userId
             AND StateId NOT IN (2, 46, 47)
        THEN 2
        WHEN @extParamId IN (167,166,177)
             AND (ts.ResponsiblePerformerId = @userId OR ...)
        THEN 1
        ELSE 0
    END AS Access
FROM @taskIds tid
LEFT JOIN TaskStates ts ON ts.taskid = tid.Id
```

Альтернатива пакетной функции — настройка «Просмотр не ограничивается». Применяется, когда скалярная функция возвращает только 1 или 2 (не возвращает 0). В гриде можно не вызывать функцию — все ДП видны, а при открытии задачи доступы определяются по скалярной функции.

## Базовые настройки ДП в категории

Типы доступа к ДП определяют механизм управления правами:

| Способ | Когда применять |
|--------|----------------|
| По задаче | Нет специфических ограничений по группам/ролям |
| Только по группам | Доступ зависит только от группы пользователя |
| По матрице доступа | Доступ зависит от комбинации группа + роль + статус |
| По SQL-функции | Доступ зависит от данных в задаче (роль в задаче, значения ДП) |

Доступ по статусу ограничивает редактирование ДП после определённого статуса. Правило: на статусы ДО и включая текущий — чтение + редактирование. На ВСЕ последующие — только чтение.

### Свойства ДП, неизменяемость типа и синхронизация между задачами

Настройки ДП существуют на двух уровнях:

| Уровень | Что настраивается | Область действия |
|---------|-------------------|-----------------|
| Свойства ДП (отдельная страница) | Формат, источник данных, разрешения | Все категории, где используется |
| Настройки в категории (правая панель) | Видимость, обязательность, блок, доступ | Только эта категория |

Изменение свойств ДП применяется ко ВСЕМ категориям, где он используется.

Тип ДП нельзя изменить после создания. Если тип выбран неверно — удалить и создать заново. Удаление невозможно, если ДП используется в задачах.

Синхронизация ДП между задачами настраивается по направлению:

| Настройка | Направление | Когда срабатывает |
|-----------|------------|-------------------|
| Синхронизировать в родительскую | Дочерняя -> Родительская | При заполнении ДП в дочерней |
| Синхронизировать в дочернюю | Родительская -> Дочерняя | При изменении ДП в родительской (не при создании дочерней) |

Условие: одинаковый ID ДП в обеих категориях.
