Приглашения пользователей в проекты. Лидер проекта (или участник с правами) приглашает пользователя на роль; приглашённый принимает или отклоняет; отправитель может отозвать приглашение или изменить роль.
- Отправка приглашения пользователю на конкретную роль/специализацию в проекте.
- Принятие / отклонение приглашения пользователем-получателем.
- Отзыв уже отправленного приглашения отправителем.
- Изменение роли / специализации существующего приглашения.
- Список моих приглашений — отображается в шапке через
app-profile-control-panel(см.docs/uilib.md) с бейджем количества. - Список приглашений проекта — для отображения в
pages/projects/edit/components/project-team-stepи вpages/projects/list?type=invites.
Связан с project (приглашение принадлежит проекту, accept меняет состав команды) и auth (User и sender).
export class Invite {
id: number;
datetimeCreated: string;
datetimeUpdated: string;
isAccepted?: boolean; // undefined = ещё не решили
motivationalLetter?: string;
project: Project; // полная модель проекта
role: string; // должность в проекте
specialization?: string;
user: User; // получатель
sender: User; // отправитель
}interface SendForUserCommand {
userId;
projectId;
role: string;
specialization?: string;
}
interface UpdateInviteCommand {
inviteId;
role: string;
specialization?: string;
}| Event | Payload | Где обрабатывается |
|---|---|---|
AcceptInvite |
{ inviteId, projectId, userId, role } |
InviteRepository: myInvitesCount$ -= 1. |
RejectInvite |
{ inviteId, projectId, userId } |
InviteRepository: myInvitesCount$ -= 1. |
RevokeInvite |
{ inviteId, projectId, userId } |
(никто не подписан в текущем коде) |
AcceptInviteтакже косвенно влияет наProjectRepository(новый коллаборатор), но прямой подписки на это событие вProjectRepositoryнет — вместо этого инвалидация идёт черезRemoveProjectCollaboratorevent (зеркальная операция). Это асимметрия: при invite-accept надо бы тоже инвалидировать проект.
abstract class InviteRepositoryPort {
sendForUser(userId, projectId, role, specialization?): Observable<Invite>;
revokeInvite(invitationId): Observable<Invite>;
acceptInvite(inviteId): Observable<Invite>;
rejectInvite(inviteId): Observable<Invite>;
updateInvite(inviteId, role, specialization?): Observable<Invite>;
getMy(): Observable<Invite[]>;
getByProject(projectId): Observable<Invite[]>;
}DI-биндинг (infrastructure/di/invite.providers.ts):
{ provide: InviteRepositoryPort, useExisting: InviteRepository }| Use-case | Параметры | Возвращает | Эмитит событие |
|---|---|---|---|
SendForUserUseCase |
command: SendForUserCommand |
Result<Invite, error> |
— |
RevokeInviteUseCase |
inviteId, projectId, userId |
Result<Invite, error> |
RevokeInvite |
AcceptInviteUseCase |
inviteId, projectId, userId, role |
Result<Invite, error> |
AcceptInvite |
RejectInviteUseCase |
inviteId, projectId, userId |
Result<Invite, error> |
RejectInvite |
UpdateInviteUseCase |
command: UpdateInviteCommand |
Result<Invite, error> |
— |
GetMyInvitesUseCase |
— | Result<Invite[], error> |
— |
GetProjectInvitesUseCase |
projectId |
Result<Invite[], error> |
— |
Все ошибки коллапсятся в { kind: "...error", cause? } (стандартный стиль).
InviteRepository implements InviteRepositoryPort. Pass-through к адаптеру с plainToInstance(Invite, ...).
Особенность: хранит myInvitesCount$: BehaviorSubject<number>(0) для бейджа в шапке. Подписан на EventBus:
this.eventBus.on<AcceptInvite>("AcceptInvite").subscribe(() => {
this.myInvitesCount$.next(Math.max(0, currentCount - 1));
});
this.eventBus.on<RejectInvite>("RejectInvite").subscribe(() => {
this.myInvitesCount$.next(Math.max(0, currentCount - 1));
});Счётчик уменьшается на accept/reject, но не инкрементируется при создании нового приглашения через push-уведомления / WebSocket — это пробел. Текущая логика: при первой загрузке
getMy()UI вычисляетlength, обновления только локальные.
Префикс /invites.
| Метод | HTTP | URL | Тело / параметры | Ответ |
|---|---|---|---|---|
sendForUser(userId, projectId, role, specialization?) |
POST | /invites/ |
{ user, project, role, specialization } |
Invite |
revokeInvite(invitationId) |
DELETE | /invites/<invitationId> |
— | void |
acceptInvite(inviteId) |
POST | /invites/<inviteId>/accept/ |
{} |
Invite |
rejectInvite(inviteId) |
POST | /invites/<inviteId>/decline/ |
{} |
Invite |
updateInvite(inviteId, role, specialization?) |
PATCH | /invites/<inviteId> |
{ role, specialization } |
Invite |
getMy() |
GET | /invites/ |
— | Invite[] |
getByProject(projectId) |
GET | /invites/ |
?project=<id>&user=any |
Invite[] |
decline/acceptимеют trailing slash,revoke(DELETE /invites/<id>) иupdateInvite(PATCH /invites/<id>) — без. Несогласованно (особенность бэка).
Параметр
?user=anyвgetByProject— вернуть приглашения проекта от ВСЕХ пользователей (без фильтра по получателю).
Отдельных ui/routes/invite/ или ui/pages/invite/ нет — приглашения отображаются:
- В
pages/projects/list/list.component.tsпри?type=invites— список моих приглашений. - В
pages/projects/edit/components/project-team-step/...— приглашения этого проекта (отправленные). - В
widgets/header(app-header) иapp-profile-control-panel(@uilib) — бейдж количества + dropdown с принять/отклонить.
Resolver ProjectsInvitesResolver в docs/modules/project.md дёргает GetMyInvitesUseCase.
| Widget | Где |
|---|---|
<app-invite-manage-card> |
@uilib (docs/uilib.md) — карточка одного invite с accept/reject. |
<app-info-card type="invite"> |
widgets/info-card — представление как часть списка приглашений в виде карточки. |
| Где | Как использует |
|---|---|
widgets/header, widgets/profile-control-panel (@uilib) |
Отображение количества + меню. |
pages/projects/edit/components/project-team-step |
SendForUserUseCase (отправка), UpdateInviteUseCase (изменение роли), RevokeInviteUseCase (отзыв), GetProjectInvitesUseCase (список приглашений проекта). |
pages/projects/list (тип invites) |
GetMyInvitesUseCase (список моих приглашений). |
domain/auth/user.model.ts |
Invite.user/sender: User. |
pages/profile/detail/main |
Может показывать приглашения профиля. |
utils/inviteToProjectMapper.ts |
Утилита маппинга Invite[] → any[] для отображения в проектных списках (см. docs/social-platform/shared.md). |