diff --git a/projects/social_platform/src/app/auth/services/auth.service.ts b/projects/social_platform/src/app/auth/services/auth.service.ts index 9c544e02..e93a13b9 100644 --- a/projects/social_platform/src/app/auth/services/auth.service.ts +++ b/projects/social_platform/src/app/auth/services/auth.service.ts @@ -11,6 +11,8 @@ import { RegisterResponse, } from "../models/http.model"; import { User, UserRole } from "../models/user.model"; +import { Project } from "@office/models/project.model"; +import { ApiPagination } from "@office/models/api-pagination.model"; /** * Сервис аутентификации и управления пользователями @@ -123,6 +125,14 @@ export class AuthService { ); } + /** + * Получить проекты где пользователь leader + * @returns Observable проектов внутри профиля + */ + getLeaderProjects(): Observable> { + return this.apiService.get(`${this.AUTH_USERS_URL}/projects/leader/`); + } + /** * Получить роли, которые может изменить текущий пользователь * @returns Observable с массивом изменяемых ролей diff --git a/projects/social_platform/src/app/office/features/detail/detail.component.html b/projects/social_platform/src/app/office/features/detail/detail.component.html index 1d17051f..d5f138e3 100644 --- a/projects/social_platform/src/app/office/features/detail/detail.component.html +++ b/projects/social_platform/src/app/office/features/detail/detail.component.html @@ -195,103 +195,6 @@ } } - - - - > } @else { пригласить } @if (+profile.id !== +info().id) { @@ -607,6 +509,169 @@

подтвердить владение н } + + @if (!showNoProjectsModal && showSendInviteModal) { + +
+ @if (profileProjects().length) { +
    +
    +

    выберите проект и роль для приглашения.

    + +
    + + @for (project of profileProjects(); track project.id) { +
  • +
    + +

    + {{ project.name ?? "Проект без названия" | truncate: 20 }} +

    +
    + +
  • + } @if (inviteForm.get("role"); as role) { +
    + + @if ((role | controlError: "required")) { +
    + {{ errorMessage.VALIDATION_REQUIRED }} +
    + } +
    + } + + + отправить приглашение + + +
+ } +
+
+ } + + +
+
+

вы не являетесь лидером ни в одном проекте

+
+ + + + перейти в проекты +
+
+ + +
+
+

+ У данного участника уже есть активное приглашение в проект +

+
+ + + + перейти в проекты +
+
+ + +
+
+

+ Пользователь не зарегистрирован в программе, поэтому нельзя отправить приглашение + в данный проект +

+
+ + + + перейти в проекты +
+
+ + +
+ + +

+ приглашение отправлено +

+ + перейти в проекты +
+
+ } @if (profile) { @if (profile.id === info().id) {
@@ -655,25 +720,6 @@

подтвердить владение н

- - diff --git a/projects/social_platform/src/app/office/features/detail/detail.component.scss b/projects/social_platform/src/app/office/features/detail/detail.component.scss index 19a1f339..46710f0f 100644 --- a/projects/social_platform/src/app/office/features/detail/detail.component.scss +++ b/projects/social_platform/src/app/office/features/detail/detail.component.scss @@ -161,23 +161,24 @@ $detail-bar-mb: 12px; display: flex; flex-direction: column; gap: 10px; - align-items: center; max-height: none; &--scrollable { - max-height: 180px; + max-height: 250px; overflow-y: auto; } } + &__invites { + max-height: 250px; + } + &__item { display: flex; flex-grow: 1; align-items: center; justify-content: space-between; - width: 300px; - margin-bottom: 12px; - margin-left: 40px; + margin-top: 8px; &--info { display: flex; @@ -243,6 +244,11 @@ $detail-bar-mb: 12px; z-index: 2; cursor: pointer; } + + &__success { + margin-top: 10px; + color: var(--green); + } } .support { diff --git a/projects/social_platform/src/app/office/features/detail/detail.component.ts b/projects/social_platform/src/app/office/features/detail/detail.component.ts index 48788f03..9b2173ab 100644 --- a/projects/social_platform/src/app/office/features/detail/detail.component.ts +++ b/projects/social_platform/src/app/office/features/detail/detail.component.ts @@ -2,14 +2,14 @@ import { CommonModule, Location } from "@angular/common"; import { ChangeDetectorRef, Component, inject, OnDestroy, OnInit, signal } from "@angular/core"; -import { ButtonComponent } from "@ui/components"; +import { ButtonComponent, InputComponent } from "@ui/components"; import { IconComponent } from "@uilib"; import { ModalComponent } from "@ui/components/modal/modal.component"; import { ActivatedRoute, Router, RouterModule } from "@angular/router"; import { AuthService } from "@auth/services"; import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { TooltipComponent } from "@ui/components/tooltip/tooltip.component"; -import { concatMap, filter, map, of, Subscription, tap } from "rxjs"; +import { concatMap, EMPTY, filter, map, Observable, of, Subscription, tap } from "rxjs"; import { User } from "@auth/models/user.model"; import { Collaborator } from "@office/models/collaborator.model"; import { ProjectService } from "@office/services/project.service"; @@ -30,6 +30,12 @@ import { projectNewAdditionalProgramVields, } from "@office/models/partner-program-fields.model"; import { saveFile } from "@utils/helpers/export-file"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe"; +import { ControlErrorPipe, ValidationService } from "@corelib"; +import { ErrorMessage } from "@error/models/error-message"; +import { InviteService } from "@office/services/invite.service"; +import { ApiPagination } from "@office/models/api-pagination.model"; @Component({ selector: "app-detail", @@ -38,17 +44,22 @@ import { saveFile } from "@utils/helpers/export-file"; imports: [ CommonModule, RouterModule, + ReactiveFormsModule, IconComponent, ButtonComponent, ModalComponent, AvatarComponent, TooltipComponent, ApproveSkillComponent, + InputComponent, + TruncatePipe, + ControlErrorPipe, ], standalone: true, }) export class DeatilComponent implements OnInit, OnDestroy { private readonly authService = inject(AuthService); + private readonly fb = inject(FormBuilder); private readonly route = inject(ActivatedRoute); private readonly projectService = inject(ProjectService); private readonly programDataService = inject(ProgramDataService); @@ -62,11 +73,14 @@ export class DeatilComponent implements OnInit, OnDestroy { public readonly chatService = inject(ChatService); private readonly cdRef = inject(ChangeDetectorRef); private readonly programService = inject(ProgramService); + private readonly inviteService = inject(InviteService); + private readonly validationService = inject(ValidationService); private readonly projectFormService = inject(ProjectFormService); // Основные данные(типы данных, данные) info = signal(undefined); profile?: User; + profileProjects = signal([]); listType: "project" | "program" | "profile" = "project"; // Переменная для подсказок @@ -94,8 +108,7 @@ export class DeatilComponent implements OnInit, OnDestroy { isProfileFill = false; // Переменные для работы с модалкой подачи проекта - // selectedProjectId: number | null = null; - // dubplicatedProjectId = 0; + selectedProjectId: number | null = null; memberProjects: Project[] = []; userType = signal(undefined); @@ -120,6 +133,11 @@ export class DeatilComponent implements OnInit, OnDestroy { // Переменные для работы с подтверждением навыков showApproveSkillModal = false; + showSendInviteModal = false; + showNoProjectsModal = false; + showActiveInviteModal = false; + showNoInProgramModal = false; + showSuccessInviteModal = false; readAllModal = false; // Сигналы для работы с модальными окнами с текстом @@ -131,6 +149,12 @@ export class DeatilComponent implements OnInit, OnDestroy { return this.projectFormService.formModel; } + readonly inviteForm = this.fb.group({ + role: ["", Validators.required], + }); + + protected readonly errorMessage = ErrorMessage; + ngOnInit(): void { const listTypeSub$ = this.route.data.subscribe(data => { this.listType = data["listType"]; @@ -207,19 +231,19 @@ export class DeatilComponent implements OnInit, OnDestroy { /** * Обработчик изменения радио-кнопки для выбора проекта */ - // onProjectRadioChange(event: Event): void { - // const target = event.target as HTMLInputElement; - // this.selectedProjectId = +target.value; + onProjectRadioChange(event: Event): void { + const target = event.target as HTMLInputElement; + this.selectedProjectId = +target.value; - // if (this.selectedProjectId) { - // this.memberProjects.find(project => project.id === this.selectedProjectId); - // } - // } + if (this.selectedProjectId) { + this.memberProjects.find(project => project.id === this.selectedProjectId); + } + } /** * Добавление проекта на программу */ - // addProjectModal(): void { + // selectProject(): void { // if (this.selectedProjectId === null) { // return; // } @@ -228,12 +252,7 @@ export class DeatilComponent implements OnInit, OnDestroy { // project => project.id === this.selectedProjectId // ); - // if (!selectedProject) { - // this.snackbarService.error("Проект не найден. Попробуйте выбрать другой проект."); - // return; - // } - - // this.assignProjectToProgram(selectedProject!); + // console.log(selectedProject); // } // get isProjectSelected(): boolean { @@ -271,47 +290,6 @@ export class DeatilComponent implements OnInit, OnDestroy { }); } - /** Эмитим логику для привязки проекта к программе */ - /** - * Привязка проекта к программе выбранной - * Перенаправление её на редактирование "нового" проекта - */ - // assignProjectToProgram(project: Project): void { - // if (!project || !project.id) { - // this.snackbarService.error("Ошибка при выборе проекта"); - // return; - // } - - // if (this.info().id) { - // this.projectService - // .assignProjectToProgram(project.id, Number(this.route.snapshot.params["programId"])) - // .subscribe({ - // next: r => { - // this.dubplicatedProjectId = r.newProjectId; - // this.assignProjectToProgramModalMessage.set(r); - // this.isAssignProjectToProgramModalOpen.set(true); - // this.toggleSubmitProjectModal(); - // this.selectedProjectId = null; - // }, - // - // error: err => { - // if (err instanceof HttpErrorResponse) { - // if (err.status === 400) { - // this.setAssignProjectToProgramError(err.error); - // } - // } - // }, - // }); - // } - // } - - // closeAssignProjectToProgramModal(): void { - // this.isAssignProjectToProgramModalOpen.set(false); - // this.router.navigateByUrl( - // `/office/projects/${this.dubplicatedProjectId}/edit?editingStep=main` - // ); - // } - /** * Закрытие модального окна выхода из проекта */ @@ -398,6 +376,47 @@ export class DeatilComponent implements OnInit, OnDestroy { }); } + /** + * Открывает модалку для отправки приглашения пользователю + * Проверяет какие отрендерить проекты где profile.id === leader + */ + inviteUser(): void { + if (!this.profileProjects().length) { + this.showNoProjectsModal = true; + } else { + this.showSendInviteModal = true; + } + } + + sendInvite(): void { + const role = this.inviteForm.get("role")?.value; + const userId = this.route.snapshot.params["id"]; + + if ( + !this.validationService.getFormValidation(this.inviteForm) || + this.selectedProjectId === null + ) { + return; + } + + this.inviteService.sendForUser(userId, this.selectedProjectId, role!).subscribe({ + next: () => { + this.showSendInviteModal = false; + this.showSuccessInviteModal = true; + + this.inviteForm.reset(); + this.selectedProjectId = null; + }, + error: err => { + if (err.error.user[0].includes("проект относится к программе")) { + this.showNoInProgramModal = true; + } else if (err.error.user[0].includes("активное приглашение")) { + this.showActiveInviteModal = true; + } + }, + }); + } + /** * Перенаправляет на страницу с информацией в завивисимости от listType */ @@ -417,6 +436,10 @@ export class DeatilComponent implements OnInit, OnDestroy { } } + routingToMyProjects(): void { + this.router.navigateByUrl(`/office/projects/my`); + } + /** * Проверка завершения программы перед регистрацией */ @@ -525,7 +548,13 @@ export class DeatilComponent implements OnInit, OnDestroy { this.isInProfileInfo(); - this.subscriptions.push(profileDataSub$); + const profileLeaderProjectsSub$ = this.authService.getLeaderProjects().subscribe({ + next: (projects: ApiPagination) => { + this.profileProjects.set(projects.results); + }, + }); + + this.subscriptions.push(profileDataSub$, profileLeaderProjectsSub$); } } @@ -534,9 +563,9 @@ export class DeatilComponent implements OnInit, OnDestroy { next: profile => { this.profile = profile; - if (this.info()) { + if (this.info() && this.listType === "project") { this.isInProject = this.info() - ?.collaborators.map((person: Collaborator) => person.userId) + ?.collaborators?.map((person: Collaborator) => person.userId) .includes(profile.id); } }, diff --git a/projects/social_platform/src/app/office/models/invite.model.ts b/projects/social_platform/src/app/office/models/invite.model.ts index aaba627e..ddcd998c 100644 --- a/projects/social_platform/src/app/office/models/invite.model.ts +++ b/projects/social_platform/src/app/office/models/invite.model.ts @@ -11,7 +11,7 @@ import { Project } from "./project.model"; * - Информацию о проекте и роли * - Статус принятия приглашения * - Мотивационное письмо и специализацию - * - Данные пользователя-отправителя + * - Данные пользователя-отправителя и получателя */ export class Invite { id!: number; @@ -24,4 +24,6 @@ export class Invite { specialization?: string; user!: User; + + sender!: User; } diff --git a/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html b/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html index 71d8c348..ec54b368 100644 --- a/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html +++ b/projects/ui/src/lib/components/layout/invite-manage-card/invite-manage-card.component.html @@ -4,12 +4,12 @@
- +
- {{ invite.user.firstName }} {{ invite.user.lastName }}{{ invite.sender.firstName }} {{ invite.sender.lastName }}
приглашает вас в проект
"{{ invite.project.name | truncate: 20 }}"