From afaba37df031676ecf9e15486e544adf5a0e69da Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 11 Nov 2025 18:03:34 +0300 Subject: [PATCH 01/22] add kanban-board page & columns & task components --- .../features/detail/detail.component.html | 37 ++++- .../features/detail/detail.component.ts | 10 +- .../office/projects/detail/detail.routes.ts | 11 ++ .../kanban-board/kanban-board.component.html | 76 ++++++++++ .../kanban-board/kanban-board.component.scss | 131 ++++++++++++++++++ .../kanban-board/kanban-board.component.ts | 92 ++++++++++++ .../detail/kanban-board/kanban-board.guard.ts | 36 +++++ .../kanban-board/kanban-board.resolver.ts | 26 ++++ .../kanban-board/kanban-board.service.ts | 24 ++++ .../kanban-board-actions.component.html | 17 +++ .../kanban-board-actions.component.scss | 45 ++++++ .../actions/kanban-board-actions.component.ts | 14 ++ .../kanban-board-sidebar.component.html | 11 ++ .../kanban-board-sidebar.component.scss | 15 ++ .../sidebar/kanban-board-sidebar.component.ts | 18 +++ .../work-section/work-section.component.html | 8 +- .../work-section/work-section.component.ts | 9 +- .../src/assets/icons/svg/arrow-wide.svg | 3 + .../src/assets/icons/svg/calendar.svg | 3 + .../assets/icons/symbol/svg/sprite.css.svg | 2 +- .../social_platform/src/styles/_colors.scss | 7 +- 21 files changed, 580 insertions(+), 15 deletions(-) create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.guard.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.resolver.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts create mode 100644 projects/social_platform/src/assets/icons/svg/arrow-wide.svg create mode 100644 projects/social_platform/src/assets/icons/svg/calendar.svg 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 d354d0ab..d032d733 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 @@ -308,10 +308,18 @@ - @if (isInProject) { + @if (isKanbanBoardPage && isInProject) { + вернуться + } @else if (isInProject && !isKanbanBoardPage) { @@ -334,6 +342,15 @@ > написать + } @else if (isKanbanBoardPage) { + + диаграмма ганта + } @else { + @if (isKanbanBoardPage) { + + контрольные точки + + } @else { команда - - @if (isInProject) { @if (profile) { @if (profile.id === info().leader) { + } @if (isInProject) { @if (isKanbanBoardPage) { + + статистика + + } @else { @if (profile) { @if (profile.id === info().leader) { выйти из проекта - } } + } } }
+ +
+
+ @if (projectBoardInfo(); as projectBoardInfo) { + + } @for (boardColumn of boardColumns(); track $index) { +
+
+

{{ boardColumn.tasks.length }}

+
+ @if (boardColumn.locked) { + + } +

{{ boardColumn.name }}

+
+ +
+ +
+ @for (task of boardColumn.tasks; track task.id) { +
+
+ {{ task.title }} +

{{ task.description ?? "" }}

+
+
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ +
+
+ +

+ 04.04.2026 +

+
+ + #аналитика + выход на рынок для чего +
+
+ +
+
+ +
+
+ } +
+ + +
+ } +
+
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss new file mode 100644 index 00000000..fd75a40d --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -0,0 +1,131 @@ +.kanban { + &__wrapper { + display: grid; + grid-template-columns: 1fr 3fr 3fr 3fr; + grid-gap: 20px; + } + + &__column { + i, p { + color: var(--accent); + cursor: pointer; + } + + &--locked { + display: flex; + align-items: center; + gap: 5px; + } + + &--header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 12px; + border-radius: var(--rounded-xl); + border: 0.5px solid var(--accent); + } + + &--add-task { + display: flex; + justify-content: center; + align-items: center; + align-self: center; + margin-top: 20px; + } + } + + &__tasks { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: 20px; + } + + &__task { + display: flex; + gap: 10px; + padding: 12px; + width: 100%; + max-width: 245px; + border-radius: var(--rounded-lg); + border: 0.5px solid var(--medium-grey-for-outline); + + &--left { + display: flex; + flex-direction: column; + gap: 5px; + align-items: flex-start; + } + + &--right { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + } + + &--mid { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + } + + &--priority { + width: 6px; + height: 6px; + border-radius: var(--rounded-xxl); + } + + &--deadline, &--bottom { + display: flex; + align-items: center; + gap: 2px; + + p { + color: var(--accent) !important; + } + + ::ng-deep { + app-tag { + .tag { + padding: 2px 10px; + } + } + } + } + + &--people { + display: flex; + align-items: center; + gap: 3px; + align-items: flex-start; + } + + &--responsible, &--performers, &--deadline { + display: flex; + align-items: center; + gap: 2px; + + i { + min-width: 6px; + } + } + + &--icons { + display: flex; + align-items: center; + gap: 5px; + } + + span { + color: var(--black); + } + + p { + color: var(--grey-for-text); + } + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts new file mode 100644 index 00000000..58af6772 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts @@ -0,0 +1,92 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component, inject, OnDestroy, OnInit, signal } from "@angular/core"; +import { Subscription } from "rxjs"; +import { ProjectDataService } from "../services/project-data.service"; +import { Project } from "@office/models/project.model"; +import { KanbanBoardSidebarComponent } from "./shared/sidebar/kanban-board-sidebar.component"; +import { ActivatedRoute } from "@angular/router"; +import { IconComponent, ButtonComponent } from "@ui/components"; +import { AvatarComponent } from "@ui/components/avatar/avatar.component"; +import { TagComponent } from "@ui/components/tag/tag.component"; +import { EditorSubmitButtonDirective } from "@ui/directives/editor-submit-button.directive"; + +@Component({ + selector: "app-kanban-board", + templateUrl: "./kanban-board.component.html", + styleUrl: "./kanban-board.component.scss", + imports: [ + CommonModule, + KanbanBoardSidebarComponent, + IconComponent, + ButtonComponent, + AvatarComponent, + TagComponent, + EditorSubmitButtonDirective, + ], + standalone: true, +}) +export class KanbanBoardComponent implements OnInit, OnDestroy { + private readonly projectDataService = inject(ProjectDataService); + private readonly route = inject(ActivatedRoute); + private subscriptions: Subscription[] = []; + + projectBoardInfo = signal(null); + boardColumns = signal([]); + + ngOnInit(): void { + const projectInfo$ = this.projectDataService.project$.subscribe({ + next: project => { + if (project) { + this.projectBoardInfo.set(project); + } + }, + }); + + this.subscriptions.push(projectInfo$); + + const mockColumns = [ + { + id: 0, + name: "бэклог", + locked: true, + tasks: [ + { + id: 1, + title: "собрать требования", + description: + "Сейчас, чтобы создался аккаунт внтури скиллз, пользователю обязательно надо войти внутрь вкладки траектории и еще раз залогиниться...", + }, + { id: 2, title: "создать дизайн макеты" }, + ], + }, + { + id: 1, + locked: false, + name: "в работе", + tasks: [ + { id: 3, title: "настроить API" }, + { id: 4, title: "подключить WebSocket" }, + ], + }, + { + id: 2, + locked: false, + name: "Готово", + tasks: [ + { id: 5, title: "верстка страницы входа" }, + { id: 6, title: "настроить Dockerfile" }, + ], + }, + ]; + + this.boardColumns.set(mockColumns); + } + + ngOnDestroy(): void { + this.subscriptions.forEach($ => $.unsubscribe()); + } + + private buildColumns(): void {} +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.guard.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.guard.ts new file mode 100644 index 00000000..a2c87c6e --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.guard.ts @@ -0,0 +1,36 @@ +/** @format */ + +import { inject } from "@angular/core"; +import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from "@angular/router"; +import { User } from "@auth/models/user.model"; +import { AuthService } from "@auth/services"; +import { Collaborator } from "@office/models/collaborator.model"; +import { ProjectService } from "@office/services/project.service"; +import { catchError, map, Observable, of, switchMap } from "rxjs"; + +export const KanbanBoardGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot +): Observable => { + const router = inject(Router); + const projectService = inject(ProjectService); + const authService = inject(AuthService); + + const projectId = Number(route.parent?.params["projectId"]); + + if (!projectId) return of(router.createUrlTree([`/office/projects/${projectId}/`])); + + return authService.profile.pipe( + switchMap((user: User) => + projectService.getOne(projectId).pipe( + map(project => { + const isInProject = project.collaborators.some( + (collaborator: Collaborator) => collaborator.userId === user.id + ); + + return isInProject ? true : router.createUrlTree([`/office/projects/${projectId}`]); + }), + catchError(() => of(router.createUrlTree(["/office/projects"]))) + ) + ) + ); +}; diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.resolver.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.resolver.ts new file mode 100644 index 00000000..71b0691b --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.resolver.ts @@ -0,0 +1,26 @@ +/** @format */ + +import { inject } from "@angular/core"; +import { KanbanBoardService } from "./kanban-board.service"; +import { ActivatedRouteSnapshot, Router } from "@angular/router"; +import { catchError, map, of } from "rxjs"; + +export const KanbanBoardResolver = (route: ActivatedRouteSnapshot): any => { + const kanbanBoardService = inject(KanbanBoardService); + const router = inject(Router); + + const projectId = Number(route.parent?.params["projectId"]); + if (!projectId) { + return of(router.createUrlTree(["/projects"])); + } + + // return kanbanBoardService.getBoardByProjectId(projectId).pipe( + // map((board: any) => ({ + // columns: board.columns, + // })), + // catchError((error) => { + // console.error('Error resolving Kanban board:', error); + // return of(router.createUrlTree(['/projects'])); + // }) + // ); +}; diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts new file mode 100644 index 00000000..c67156a0 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts @@ -0,0 +1,24 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { ApiService } from "@corelib"; + +@Injectable({ + providedIn: "root", +}) +export class KanbanBoardService { + private readonly apiService = inject(ApiService); + private readonly KANBAN_BOARD_URL = ""; + + getBoardByProjectId(projectId: number) { + return this.apiService.get(`${this.KANBAN_BOARD_URL}/`); + } + + getTasksByColumnId(columnId: number) { + return this.apiService.get(`${this.KANBAN_BOARD_URL}/`); + } + + getTaskById(taskId: number) { + return this.apiService.get(`${this.KANBAN_BOARD_URL}/`); + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html new file mode 100644 index 00000000..b821dd72 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html @@ -0,0 +1,17 @@ + + + +
+
+ +
+ +
+ +
+
+ +
+ +
+
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss new file mode 100644 index 00000000..77fb1356 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss @@ -0,0 +1,45 @@ +.kanban { + &__sidebar { + &--item { + border-radius: var(--rounded-xxl); + padding: 12px; + display: flex; + align-self: center; + justify-content: center; + align-items: center; + cursor: pointer; + } + + &--add-project { + background-color: var(--light-white); + border: 0.5px solid var(--accent); + } + } + + &__actions { + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 15px; + } +} + +.actions { + &__person { + background-color: var(--accent-light); + border: 0.5px solid var(--accent); + + i { + color: var(--accent); + } + } + + &__priority { + background-color: var(--green-light); + border: 0.5px solid var(--green); + + i { + color: var(--green); + } + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.ts new file mode 100644 index 00000000..5cd91570 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.ts @@ -0,0 +1,14 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { IconComponent } from "@uilib"; + +@Component({ + selector: "app-kanban-board-actions", + templateUrl: "./kanban-board-actions.component.html", + styleUrl: "./kanban-board-actions.component.scss", + imports: [CommonModule, IconComponent], + standalone: true, +}) +export class KanbanBoardActionsComponent {} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html new file mode 100644 index 00000000..22056b21 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html @@ -0,0 +1,11 @@ + + +
+ @if (projectBoardInfo) { +
+ +
+ } + + +
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss new file mode 100644 index 00000000..96616bbb --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss @@ -0,0 +1,15 @@ +.kanban { + &__sidebar { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + height: 60vh; + max-height: 373px; + gap: 15px; + padding: 15px; + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + background-color: var(--light-white); + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts new file mode 100644 index 00000000..39730f99 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts @@ -0,0 +1,18 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { Project } from "@office/models/project.model"; +import { AvatarComponent } from "@ui/components/avatar/avatar.component"; +import { KanbanBoardActionsComponent } from "../actions/kanban-board-actions.component"; + +@Component({ + selector: "app-kanban-board-sidebar", + templateUrl: "./kanban-board-sidebar.component.html", + styleUrl: "./kanban-board-sidebar.component.scss", + imports: [CommonModule, AvatarComponent, KanbanBoardActionsComponent], + standalone: true, +}) +export class KanbanBoardSidebarComponent { + @Input({ required: true }) projectBoardInfo!: Project; +} diff --git a/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.html b/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.html index 2df97a01..d192016e 100644 --- a/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.html +++ b/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.html @@ -8,9 +8,11 @@

мои задачи

- открыть доску задач +
+ открыть доску задач +
diff --git a/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.ts b/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.ts index 8197bd5d..e1e743ef 100644 --- a/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/work-section/work-section.component.ts @@ -5,7 +5,7 @@ import { Component, inject, OnDestroy, OnInit } from "@angular/core"; import { ButtonComponent } from "@ui/components"; import { IconComponent } from "@uilib"; import { map, Subscription } from "rxjs"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, RouterLink } from "@angular/router"; import { VacancyResponse } from "@office/models/vacancy-response.model"; import { VacancyService } from "@office/services/vacancy.service"; @@ -13,15 +13,16 @@ import { VacancyService } from "@office/services/vacancy.service"; selector: "app-work-section", templateUrl: "./work-section.component.html", styleUrl: "./work-section.component.scss", - imports: [CommonModule, IconComponent, ButtonComponent], + imports: [CommonModule, IconComponent, ButtonComponent, RouterLink], standalone: true, }) export class ProjectWorkSectionComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly vacancyService = inject(VacancyService); - private subscriptions: Subscription[] = []; + private readonly subscriptions: Subscription[] = []; vacancies: VacancyResponse[] = []; + projectId?: number; ngOnInit(): void { const vacanciesSub$ = this.route.data.pipe(map(r => r["data"])).subscribe({ @@ -33,6 +34,8 @@ export class ProjectWorkSectionComponent implements OnInit, OnDestroy { }); this.subscriptions.push(vacanciesSub$); + + this.projectId = this.route.parent?.snapshot.params["projectId"]; } ngOnDestroy(): void { diff --git a/projects/social_platform/src/assets/icons/svg/arrow-wide.svg b/projects/social_platform/src/assets/icons/svg/arrow-wide.svg new file mode 100644 index 00000000..d096f016 --- /dev/null +++ b/projects/social_platform/src/assets/icons/svg/arrow-wide.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/social_platform/src/assets/icons/svg/calendar.svg b/projects/social_platform/src/assets/icons/svg/calendar.svg new file mode 100644 index 00000000..4fe4af35 --- /dev/null +++ b/projects/social_platform/src/assets/icons/svg/calendar.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg b/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg index 0d291c46..fc716876 100644 --- a/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg +++ b/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/social_platform/src/styles/_colors.scss b/projects/social_platform/src/styles/_colors.scss index 9deab347..9986d687 100644 --- a/projects/social_platform/src/styles/_colors.scss +++ b/projects/social_platform/src/styles/_colors.scss @@ -9,7 +9,8 @@ --accent: #8a63e6; --accent-dark: #{color.adjust(#8a63e6, $blackness: 20%)}; --accent-mild: #{color.adjust(#6c27ff, $alpha: -0.4)}; - --accent-light: #9a80e6; + --accent-light: #E8E0FA; + --accent-medium: #9764BA; // GOLD --gold: #e5b25d; @@ -28,7 +29,11 @@ // FUNCTIONAL --green: #88c9a1; + --green-light: #E3F0E8; --green-dark: #297373; --red: #d48a9e; --red-dark: #{color.adjust(#d48a9e, $blackness: 10%)}; + --blue-dark: #2F36AA; + --purple: #9a80e6; + --cyan: #4CD9F1; } From 42b4ca2c6d0c9bd2d2f4fae02cda4aa6553cd9f1 Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 11 Nov 2025 18:13:19 +0300 Subject: [PATCH 02/22] add styles for kanban-board, columns, tasks components --- .../kanban-board/kanban-board.component.scss | 34 +++++++++++-------- .../kanban-board-actions.component.scss | 6 ++-- .../kanban-board-sidebar.component.scss | 4 +-- .../social_platform/src/styles/_colors.scss | 10 +++--- 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss index fd75a40d..6cbf53f7 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -6,15 +6,16 @@ } &__column { - i, p { + i, + p { color: var(--accent); cursor: pointer; } &--locked { display: flex; - align-items: center; gap: 5px; + align-items: center; } &--header { @@ -22,15 +23,15 @@ align-items: center; justify-content: space-between; padding: 5px 12px; - border-radius: var(--rounded-xl); border: 0.5px solid var(--accent); + border-radius: var(--rounded-xl); } &--add-task { display: flex; - justify-content: center; align-items: center; align-self: center; + justify-content: center; margin-top: 20px; } } @@ -38,19 +39,19 @@ &__tasks { display: flex; flex-direction: column; - align-items: center; gap: 20px; + align-items: center; margin-top: 20px; } &__task { display: flex; gap: 10px; - padding: 12px; width: 100%; max-width: 245px; - border-radius: var(--rounded-lg); + padding: 12px; border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); &--left { display: flex; @@ -62,14 +63,14 @@ &--right { display: flex; flex-direction: column; - justify-content: space-between; align-items: center; + justify-content: space-between; } &--mid { display: flex; - justify-content: space-between; align-items: center; + justify-content: space-between; width: 100%; } @@ -79,10 +80,11 @@ border-radius: var(--rounded-xxl); } - &--deadline, &--bottom { + &--deadline, + &--bottom { display: flex; - align-items: center; gap: 2px; + align-items: center; p { color: var(--accent) !important; @@ -99,15 +101,17 @@ &--people { display: flex; - align-items: center; gap: 3px; + align-items: center; align-items: flex-start; } - &--responsible, &--performers, &--deadline { + &--responsible, + &--performers, + &--deadline { display: flex; - align-items: center; gap: 2px; + align-items: center; i { min-width: 6px; @@ -116,8 +120,8 @@ &--icons { display: flex; - align-items: center; gap: 5px; + align-items: center; } span { diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss index 77fb1356..b06ff549 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss @@ -1,13 +1,13 @@ .kanban { &__sidebar { &--item { - border-radius: var(--rounded-xxl); - padding: 12px; display: flex; + align-items: center; align-self: center; justify-content: center; - align-items: center; + padding: 12px; cursor: pointer; + border-radius: var(--rounded-xxl); } &--add-project { diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss index 96616bbb..70efd18d 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss @@ -2,14 +2,14 @@ &__sidebar { display: flex; flex-direction: column; + gap: 15px; align-items: center; justify-content: flex-start; height: 60vh; max-height: 373px; - gap: 15px; padding: 15px; + background-color: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); - background-color: var(--light-white); } } diff --git a/projects/social_platform/src/styles/_colors.scss b/projects/social_platform/src/styles/_colors.scss index 9986d687..55149b82 100644 --- a/projects/social_platform/src/styles/_colors.scss +++ b/projects/social_platform/src/styles/_colors.scss @@ -9,8 +9,8 @@ --accent: #8a63e6; --accent-dark: #{color.adjust(#8a63e6, $blackness: 20%)}; --accent-mild: #{color.adjust(#6c27ff, $alpha: -0.4)}; - --accent-light: #E8E0FA; - --accent-medium: #9764BA; + --accent-light: #e8e0fa; + --accent-medium: #9764ba; // GOLD --gold: #e5b25d; @@ -29,11 +29,11 @@ // FUNCTIONAL --green: #88c9a1; - --green-light: #E3F0E8; + --green-light: #e3f0e8; --green-dark: #297373; --red: #d48a9e; --red-dark: #{color.adjust(#d48a9e, $blackness: 10%)}; - --blue-dark: #2F36AA; + --blue-dark: #2f36aa; --purple: #9a80e6; - --cyan: #4CD9F1; + --cyan: #4cd9f1; } From 41a71f7dd7da6638b7eb8684f6f3677c031bbee1 Mon Sep 17 00:00:00 2001 From: Awakich Date: Wed, 12 Nov 2025 17:39:17 +0300 Subject: [PATCH 03/22] add detail task, task, mapper for color priority --- .../consts/lists/priority-info-list.const.ts | 47 ++++ .../kanban-board/kanban-board.component.html | 214 ++++++++++++++---- .../kanban-board/kanban-board.component.scss | 153 ++++++++----- .../kanban-board/kanban-board.component.ts | 67 +++++- .../kanban-board-actions.component.html | 6 +- .../kanban-board-actions.component.scss | 6 +- .../kanban-board-sidebar.component.html | 2 +- .../shared/task/kanban-task.component.html | 53 +++++ .../shared/task/kanban-task.component.scss | 106 +++++++++ .../shared/task/kanban-task.component.ts | 39 ++++ .../project-vacancy-card.component.ts | 2 +- .../src/app/utils/helpers/hexToRgba.ts | 10 + 12 files changed, 598 insertions(+), 107 deletions(-) create mode 100644 projects/core/src/consts/lists/priority-info-list.const.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts create mode 100644 projects/social_platform/src/app/utils/helpers/hexToRgba.ts diff --git a/projects/core/src/consts/lists/priority-info-list.const.ts b/projects/core/src/consts/lists/priority-info-list.const.ts new file mode 100644 index 00000000..c9ddcc8d --- /dev/null +++ b/projects/core/src/consts/lists/priority-info-list.const.ts @@ -0,0 +1,47 @@ +/** + * Информация для приоритетов + * + * @format + * @field name - выбор в выпадающем списке + * @field color - цвет для определенного типа приоритета + * @field priorityType - значение (от 0 до 5), которое соотносится на бэке + */ + +export const priorityInfoList = [ + { + id: 0, + name: "бэклог", + color: "#322299", + priorityType: 0, + }, + { + id: 1, + name: "в ближайшие часы", + color: "#A63838", + priorityType: 1, + }, + { + id: 2, + name: "высокий", + color: "#D48A9E", + priorityType: 2, + }, + { + id: 3, + name: "средний", + color: "#E5B25D", + priorityType: 3, + }, + { + id: 4, + name: "низкий", + color: "#297373", + priorityType: 4, + }, + { + id: 5, + name: "улучшение", + color: "#88C9A1", + priorityType: 5, + }, +]; diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html index 01b13486..10b9069e 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html @@ -1,14 +1,14 @@
-
+
@if (projectBoardInfo(); as projectBoardInfo) { } @for (boardColumn of boardColumns(); track $index) {
-
+

{{ boardColumn.tasks.length }}

-
+
@if (boardColumn.locked) { } @@ -19,57 +19,185 @@
@for (task of boardColumn.tasks; track task.id) { -
+ } +
+ + +
+ } @if (isTaskDetailOpen()) { +
+
+ прикрепить результат -
- {{ task.title }} -

{{ task.description ?? "" }}

-
-
-
- - -
- -
- - -
-
- -
- - -
+
+ +
+
+
+ +
+

Настроить процессы

+ +
+ +
+
+ +

Федор Е

+
+ +

12.10.2025 21:00

+
+ +
+
+
+ +
ответственный
+
+ +
+ +

Екатерина Ш

+
+
+ +
+
+ +
исполнители
+
+ +
+ +
+
+ +
+
+ +

начало

+
+ +
+

20.10.25

+ +
+

началась

+
+
+ +
+
+ +

дедлайн

+
-
-
- -

- 04.04.2026 -

-
+
+

25.10.25

- #аналитика - выход на рынок для чего +
+

10 дней

+
+
-
-
- +
+
+
+ +

тег

+ + #аналитика +
+ +
+
+ +

цель

+
+ + выход на рынок для... +
+ +
+
+ +

навыки

+
+ + word
- }
- +
+ +
+

+ @if (descriptionExpandable) { +
+ {{ readFullDescription ? "cкрыть" : "подробнее" }} +
+ } +
+ +
+ +
+ +
+ +
+
+ + + + + + +
+ @for (f of filesList; track f.id) { + + + } +
+ +
+
}
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss index 6cbf53f7..89a91010 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -3,6 +3,7 @@ display: grid; grid-template-columns: 1fr 3fr 3fr 3fr; grid-gap: 20px; + position: relative; } &__column { @@ -12,13 +13,13 @@ cursor: pointer; } - &--locked { + &-locked { display: flex; gap: 5px; align-items: center; } - &--header { + &-header { display: flex; align-items: center; justify-content: space-between; @@ -44,92 +45,138 @@ margin-top: 20px; } - &__task { - display: flex; - gap: 10px; + &__detail { + position: absolute; + right: 0%; + top: 0%; width: 100%; - max-width: 245px; - padding: 12px; - border: 0.5px solid var(--medium-grey-for-outline); + max-width: 422px; + padding: 24px; border-radius: var(--rounded-lg); + border: 0.5px solid var(--medium-grey-for-outline); + background-color: var(--light-white); - &--left { - display: flex; - flex-direction: column; - gap: 5px; - align-items: flex-start; - } - &--right { + &-top { display: flex; - flex-direction: column; align-items: center; justify-content: space-between; - } + margin-bottom: 24px; - &--mid { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; + &-menu { + display: flex; + align-items: center; + gap: 10px; + } } - &--priority { - width: 6px; - height: 6px; + &-priority { + width: 15px; + height: 15px; border-radius: var(--rounded-xxl); + background-color: var(--green-dark); } - &--deadline, - &--bottom { + &-general { display: flex; - gap: 2px; align-items: center; + justify-content: space-between; + margin-bottom: 10px; + border-bottom: 0.5px solid var(--accent); + margin-bottom: 5px; p { color: var(--accent) !important; } - - ::ng-deep { - app-tag { - .tag { - padding: 2px 10px; - } - } - } } - &--people { + p {color: var(--grey-for-text);} + + &-info { display: flex; - gap: 3px; - align-items: center; align-items: flex-start; - } + gap: 10px; + margin-top: 15px; - &--responsible, - &--performers, - &--deadline { - display: flex; - gap: 2px; - align-items: center; + &-name { + display: flex; + align-items: center; + gap: 3px; + + &--date { + display: flex; + align-items: center; + gap: 3px; - i { - min-width: 6px; + p, i { + color: var(--green-dark) !important; + } + } + + h6 { + color: var(--accent); + } } } - &--icons { + &-info-wrapper { display: flex; + flex-direction: column; gap: 5px; - align-items: center; } - span { - color: var(--black); + i { + color: var(--accent); } + } +} +.about { + /* stylelint-disable value-no-vendor-prefix */ + &__text { p { - color: var(--grey-for-text); + display: -webkit-box; + overflow: hidden; + color: var(--black); + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + transition: all 0.7s ease-in-out; + + &.expanded { + -webkit-line-clamp: unset; + } } + + ::ng-deep a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 3px; + transition: text-decoration-color 0.2s; + + &:hover { + text-decoration-color: var(--accent); + } + } + } + + /* stylelint-enable value-no-vendor-prefix */ + + &__read-full { + margin-top: 2px; + color: var(--accent); + cursor: pointer; + } +} + +.read-more { + margin-top: 12px; + color: var(--accent); + cursor: pointer; + transition: color 0.2s; + + &:hover { + color: var(--accent-dark); } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts index 58af6772..67b86b27 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts @@ -6,11 +6,16 @@ import { Subscription } from "rxjs"; import { ProjectDataService } from "../services/project-data.service"; import { Project } from "@office/models/project.model"; import { KanbanBoardSidebarComponent } from "./shared/sidebar/kanban-board-sidebar.component"; -import { ActivatedRoute } from "@angular/router"; -import { IconComponent, ButtonComponent } from "@ui/components"; +import { ActivatedRoute, Router } from "@angular/router"; +import { IconComponent, ButtonComponent, InputComponent } from "@ui/components"; +import { KanbanTaskComponent } from "./shared/task/kanban-task.component"; +import { ClickOutsideModule } from "ng-click-outside"; import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { TagComponent } from "@ui/components/tag/tag.component"; -import { EditorSubmitButtonDirective } from "@ui/directives/editor-submit-button.directive"; +import { expandElement } from "@utils/expand-element"; +import { ParseBreaksPipe, ParseLinksPipe } from "@corelib"; +import { FileItemComponent } from "@ui/components/file-item/file-item.component"; +import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; @Component({ selector: "app-kanban-board", @@ -20,22 +25,45 @@ import { EditorSubmitButtonDirective } from "@ui/directives/editor-submit-button CommonModule, KanbanBoardSidebarComponent, IconComponent, + KanbanTaskComponent, + ClickOutsideModule, ButtonComponent, AvatarComponent, TagComponent, - EditorSubmitButtonDirective, + ParseBreaksPipe, + ParseLinksPipe, + FileItemComponent, + InputComponent, + FileUploadItemComponent, ], standalone: true, }) export class KanbanBoardComponent implements OnInit, OnDestroy { private readonly projectDataService = inject(ProjectDataService); private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); private subscriptions: Subscription[] = []; projectBoardInfo = signal(null); boardColumns = signal([]); + isTaskDetailOpen = signal(false); + + descriptionExpandable!: boolean; // Флаг необходимости кнопки "Читать полностью" + readFullDescription = false; // Флаг показа всех вакансий + + filesList: any[] = []; ngOnInit(): void { + const detailInfoUrl$ = this.route.queryParams.subscribe({ + next: params => { + if (params["taskId"]) { + this.isTaskDetailOpen.set(true); + } + }, + }); + + this.subscriptions.push(detailInfoUrl$); + const projectInfo$ = this.projectDataService.project$.subscribe({ next: project => { if (project) { @@ -57,6 +85,7 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { title: "собрать требования", description: "Сейчас, чтобы создался аккаунт внтури скиллз, пользователю обязательно надо войти внутрь вкладки траектории и еще раз залогиниться...", + priority: 3, }, { id: 2, title: "создать дизайн макеты" }, ], @@ -88,5 +117,33 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { this.subscriptions.forEach($ => $.unsubscribe()); } - private buildColumns(): void {} + openDetailTask(taskId: number): void { + this.router.navigate([], { + queryParams: { + taskId, + }, + relativeTo: this.route, + queryParamsHandling: "merge", + }); + this.isTaskDetailOpen.set(true); + } + + closeDetailTask(): void { + this.router.navigate([], { + queryParams: {}, + replaceUrl: true, + }); + this.isTaskDetailOpen.set(false); + } + + /** + * Раскрытие/сворачивание описания профиля + * @param elem - DOM элемент описания + * @param expandedClass - CSS класс для раскрытого состояния + * @param isExpanded - текущее состояние (раскрыто/свернуто) + */ + onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void { + expandElement(elem, expandedClass, isExpanded); + this.readFullDescription = !isExpanded; + } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html index b821dd72..5a53c041 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.html @@ -2,16 +2,16 @@
-
+
-
+
-
+
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss index b06ff549..778d0790 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/actions/kanban-board-actions.component.scss @@ -1,6 +1,6 @@ .kanban { &__sidebar { - &--item { + &-item { display: flex; align-items: center; align-self: center; @@ -13,6 +13,10 @@ &--add-project { background-color: var(--light-white); border: 0.5px solid var(--accent); + + i { + color: var(--accent); + } } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html index 22056b21..93f54a17 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html @@ -2,7 +2,7 @@
@if (projectBoardInfo) { -
+
} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html new file mode 100644 index 00000000..2447d881 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html @@ -0,0 +1,53 @@ + + +
+
+ {{ task.title }} + @if (task.description) { +

{{ task.description }}

+ } @if (task?.responsible || task?.performers?.length || task?.files?.length || task.action) { +
+
+ @if (task?.responsible) { +
+ + +
+ } @if (task?.performers?.length) { +
+ + +
+ } +
+ + @if (task?.files?.length || task?.action) { +
+ + +
+ } +
+ } @if (task?.deadline || task?.tag || task?.goal) { +
+ @if (task.deadline) { +
+ +

04.04.2026

+
+ } @if (task.tag) { + #аналитика + } @if (task.goal) { + выход на рынок для чего + } +
+ } +
+ +
+ @if (task.priority) { +
+ } + +
+
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss new file mode 100644 index 00000000..e4a1dd27 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss @@ -0,0 +1,106 @@ +:host { + width: 100%; +} + +.kanban { + &__task { + display: flex; + gap: 10px; + width: 100%; + max-width: 245px; + padding: 12px; + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + transition: opacity 0.2s ease-in; + cursor: pointer; + + &:hover { + opacity: 0.7; + } + + &-left { + display: flex; + flex-direction: column; + gap: 5px; + flex-grow: 1; + align-items: flex-start; + } + + &-right { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + } + + &-mid { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + + &-priority { + width: 6px; + height: 6px; + border-radius: var(--rounded-xxl); + } + + &-deadline, + &-bottom { + display: flex; + gap: 2px; + align-items: center; + + p { + color: var(--accent) !important; + } + + ::ng-deep { + app-tag { + .tag { + padding: 2px 10px; + } + } + } + } + + &-people { + display: flex; + gap: 3px; + align-items: center; + align-items: flex-start; + } + + &-responsible, + &-performers, + &-deadline { + display: flex; + gap: 2px; + align-items: center; + + i { + min-width: 6px; + } + } + + &-icons { + display: flex; + gap: 5px; + align-items: center; + } + + span { + color: var(--black); + } + + p { + color: var(--grey-for-text); + } + + i { + color: var(--accent); + cursor: pointer; + } + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts new file mode 100644 index 00000000..438a7448 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts @@ -0,0 +1,39 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { IconComponent } from "@uilib"; +import { AvatarComponent } from "@ui/components/avatar/avatar.component"; +import { TagComponent } from "@ui/components/tag/tag.component"; +import { hexToRgba } from "@utils/helpers/hexToRgba"; +import { priorityInfoList } from "projects/core/src/consts/lists/priority-info-list.const"; + +@Component({ + selector: "app-kanban-task", + templateUrl: "./kanban-task.component.html", + styleUrl: "./kanban-task.component.scss", + imports: [CommonModule, IconComponent, AvatarComponent, TagComponent], + standalone: true, +}) +export class KanbanTaskComponent { + @Input({ required: true }) task: any; + + private readonly priorityInfoList = priorityInfoList; + private readonly hexToRgba = hexToRgba; + + getPriorityType(priorityId: number, type: "background" | "color", opacity = 0.25) { + const findedPriority = this.priorityInfoList.find( + priority => priority.priorityType === priorityId + ); + const baseColor = findedPriority?.color ?? "var(--light-white)"; + + if (!findedPriority) return; + + if (type === "color") { + return { "background-color": baseColor }; + } + + const rgbaColor = this.hexToRgba(baseColor, opacity); + return { "background-color": rgbaColor }; + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts b/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts index 104ec1a8..8dd59e13 100644 --- a/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts @@ -57,7 +57,7 @@ export class ProjectVacancyCardComponent implements OnInit { } } - descriptionExpandable!: boolean; // Флаг необходимости кнопки "Читать полностью" + descriptionExpandable!: boolean; // Флаг необходимости кнопки "подробнее" readFullDescription = false; // Флаг показа всех вакансий endSliceOfSkills = 0; diff --git a/projects/social_platform/src/app/utils/helpers/hexToRgba.ts b/projects/social_platform/src/app/utils/helpers/hexToRgba.ts new file mode 100644 index 00000000..568c6c3a --- /dev/null +++ b/projects/social_platform/src/app/utils/helpers/hexToRgba.ts @@ -0,0 +1,10 @@ +/** @format */ + +export const hexToRgba = (hex: string, alpha: number): string => { + const [r, g, b] = hex + .replace(/^#/, "") + .match(/.{1,2}/g)! + .map(x => parseInt(x, 16)); + + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +}; From c33a1f63ef41c37eb666547aff7610528dde822e Mon Sep 17 00:00:00 2001 From: Awakich Date: Wed, 12 Nov 2025 17:41:06 +0300 Subject: [PATCH 04/22] add styles for task-detail, task --- .../kanban-board/kanban-board.component.scss | 26 +++++++++---------- .../shared/task/kanban-task.component.scss | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss index 89a91010..c54a45d6 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -1,9 +1,9 @@ .kanban { &__wrapper { + position: relative; display: grid; grid-template-columns: 1fr 3fr 3fr 3fr; grid-gap: 20px; - position: relative; } &__column { @@ -47,15 +47,14 @@ &__detail { position: absolute; - right: 0%; top: 0%; + right: 0%; width: 100%; max-width: 422px; padding: 24px; - border-radius: var(--rounded-lg); - border: 0.5px solid var(--medium-grey-for-outline); background-color: var(--light-white); - + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); &-top { display: flex; @@ -65,16 +64,16 @@ &-menu { display: flex; - align-items: center; gap: 10px; + align-items: center; } } &-priority { width: 15px; height: 15px; - border-radius: var(--rounded-xxl); background-color: var(--green-dark); + border-radius: var(--rounded-xxl); } &-general { @@ -82,33 +81,34 @@ align-items: center; justify-content: space-between; margin-bottom: 10px; - border-bottom: 0.5px solid var(--accent); margin-bottom: 5px; + border-bottom: 0.5px solid var(--accent); p { color: var(--accent) !important; } } - p {color: var(--grey-for-text);} + p { color: var(--grey-for-text); } &-info { display: flex; - align-items: flex-start; gap: 10px; + align-items: flex-start; margin-top: 15px; &-name { display: flex; - align-items: center; gap: 3px; + align-items: center; &--date { display: flex; - align-items: center; gap: 3px; + align-items: center; - p, i { + p, + i { color: var(--green-dark) !important; } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss index e4a1dd27..097af114 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss @@ -9,10 +9,10 @@ width: 100%; max-width: 245px; padding: 12px; + cursor: pointer; border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); transition: opacity 0.2s ease-in; - cursor: pointer; &:hover { opacity: 0.7; @@ -21,8 +21,8 @@ &-left { display: flex; flex-direction: column; - gap: 5px; flex-grow: 1; + gap: 5px; align-items: flex-start; } From ce82961fe8ba35e3ffed5bef4861eff8450eaaa9 Mon Sep 17 00:00:00 2001 From: Awakich Date: Thu, 13 Nov 2025 17:29:32 +0300 Subject: [PATCH 05/22] filled & empty design of task-detail, models for kanban, badge component --- .../src/app/office/models/tag.model.ts | 7 + .../kanban-board/kanban-board.component.html | 182 ++++++++++++------ .../kanban-board/kanban-board.component.scss | 164 +++++++++++++++- .../kanban-board/kanban-board.component.ts | 65 ++++++- .../detail/kanban-board/models/board.model.ts | 6 + .../kanban-board/models/column.model.ts | 8 + .../kanban-board/models/comments.model.ts | 20 ++ .../detail/kanban-board/models/task.model.ts | 49 +++++ .../shared/task/kanban-task.component.html | 21 +- .../shared/task/kanban-task.component.scss | 5 +- .../shared/task/kanban-task.component.ts | 3 +- .../project-vacancy-card.component.ts | 24 ++- .../ui/components/badge/badge.component.html | 14 ++ .../ui/components/badge/badge.component.scss | 24 +++ .../ui/components/badge/badge.component.ts | 16 ++ .../components/button/button.component.scss | 6 +- .../app/ui/components/tag/tag.component.scss | 3 + .../src/assets/icons/svg/hastag.svg | 3 + .../assets/icons/symbol/svg/sprite.css.svg | 2 +- .../assets/images/projects/shared/divider.png | Bin 0 -> 401 bytes .../social_platform/src/styles/_colors.scss | 2 + 21 files changed, 549 insertions(+), 75 deletions(-) create mode 100644 projects/social_platform/src/app/office/models/tag.model.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/models/board.model.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/models/comments.model.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts create mode 100644 projects/social_platform/src/app/ui/components/badge/badge.component.html create mode 100644 projects/social_platform/src/app/ui/components/badge/badge.component.scss create mode 100644 projects/social_platform/src/app/ui/components/badge/badge.component.ts create mode 100644 projects/social_platform/src/assets/icons/svg/hastag.svg create mode 100644 projects/social_platform/src/assets/images/projects/shared/divider.png diff --git a/projects/social_platform/src/app/office/models/tag.model.ts b/projects/social_platform/src/app/office/models/tag.model.ts new file mode 100644 index 00000000..a5a5118a --- /dev/null +++ b/projects/social_platform/src/app/office/models/tag.model.ts @@ -0,0 +1,7 @@ +/** @format */ + +export interface Tag { + id: number; + name: string; + color: string; +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html index 10b9069e..91471ba8 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html @@ -26,7 +26,7 @@
} @if (isTaskDetailOpen()) { -
+
прикрепить результат12.10.2025 21:00

+
+ divider image +
+
+ @if (taskDetailForm.get("responsible"); as responsible) {
@@ -59,11 +64,17 @@
ответственный
- -

Екатерина Ш

+ @if (responsible.value) { + +

{{ responsible.value.name }}

+ } @else { +
+ +
+ }
- + } @if (taskDetailForm.get("performers"); as performers) {
@@ -71,10 +82,20 @@
исполнители
- + @if (performers.value.length > 0) { +
+ @for (performer of performers.value; track $index) { + + } +
+ } @else { +
+ +
+ }
- + } @if (taskDetailForm.get("startDate"); as startDate) {
@@ -82,14 +103,12 @@
исполнители
-

20.10.25

+

{{ startDate.value || todayDate | date: "dd.MM.yy" }}

-
-

началась

-
+ закончена
- + } @if (taskDetailForm.get("deadline"); as deadline) {
@@ -97,55 +116,82 @@
исполнители
-

25.10.25

+

{{ deadline.value || tommorowDate | date: "dd.MM.yy" }}

-
-

10 дней

-
+ 10 дней
+ }
+ @if (taskDetailForm.get("tags"); as tags) {
- +

тег

- #аналитика + @if (tags.value.length > 0) { +
+ @for (tag of tags.value; track $index) { + {{ "#" + tag.name }} + } +
+ } @else { +
+ +
+ }
- + } @if (taskDetailForm.get("goal"); as goal) {

цель

- выход на рынок для... + @if (goal.value) { + {{ + goal.value.name.length > 20 ? goal.value.name.slice(0, 15) + "..." : goal.value.name + }} + } @else { +
+ +
+ }
- + } @if (taskDetailForm.get("skills"); as skills) {

навыки

- word + @if (skills.value.length > 0) { +
+ @for (skill of skills.value; track $index) { + {{ skill.name }} + } +
+ } @else { +
+ +
+ }
+ } +
+ +
+ divider image
+ @if (taskDetailForm.get("description"); as description) {
- -
-

+
+ @if (isChangeDescriptionText() && description.value) { +

@if (descriptionExpandable) {
исполнители > {{ readFullDescription ? "cкрыть" : "подробнее" }}
+ } } @else { + }
-
+ }
- + @if (taskDetailForm.get("files"); as files) { @if (files.value.length >= 0 && + files.value.length <= 3) { @for (file of files.value; track $index) { + + } @if (!(files.value.length === 3)) { +
+
+ +
+ +

файл

+
+ } } }
+
- - - - - - +
+ + +
+ + +
+ +
@for (f of filesList; track f.id) { исполнители [loading]="f.loading" [error]="f.error" > - } -
-
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss index c54a45d6..9ba7f826 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -1,3 +1,5 @@ +@use "styles/typography"; + .kanban { &__wrapper { position: relative; @@ -72,7 +74,7 @@ &-priority { width: 15px; height: 15px; - background-color: var(--green-dark); + background-color: var(--green); border-radius: var(--rounded-xxl); } @@ -117,6 +119,12 @@ color: var(--accent); } } + + &-list { + display: flex; + flex-direction: column; + gap: 4px; + } } &-info-wrapper { @@ -125,10 +133,82 @@ gap: 5px; } + &-add-responsible { + cursor: pointer; + padding: 8px; + border-radius: var(--rounded-xxl); + border: 0.5px dashed var(--accent); + } + + &-performers { + display: flex; + gap: 4px; + align-items: center; + margin-left: 12px; + + app-avatar { + margin-left: -12px; + } + } + + &-add-object { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + align-self: center; + width: 100%; + padding: 2px 48px; + border: 0.5px solid var(--accent); + border-radius: var(--rounded-xl); + } + + &-description { + ::ng-deep { + app-textarea { + .field__input { + background-color: transparent; + border: none; + border-radius: 0; + margin-left: -10px; + min-height: 45px; + padding: 12px 12px 0px 12px; + + &:focus { + box-shadow: none; + border-color: none; + } + } + } + } + } + + &-files { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 40px; + } + + &-file { + width: 116px; + + &--empty { + display: flex; + align-items: center; + gap: 5px; + } + } + i { color: var(--accent); } } + + &__divider { + margin-top: 5px; + width: 100%; + } } .about { @@ -137,7 +217,7 @@ p { display: -webkit-box; overflow: hidden; - color: var(--black); + color: var(--grey-for-text); text-overflow: ellipsis; -webkit-box-orient: vertical; -webkit-line-clamp: 5; @@ -180,3 +260,83 @@ color: var(--accent-dark); } } + +.form { + padding: 10px 24px; + height: 28px; + width: 422px; + margin-bottom: -25px; + margin-left: -25px; + background-color: var(--light-white); + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + display: flex; + align-items: center; + justify-content: space-between; + + &__row { + display: flex; + align-items: center; + width: 100%; + + ::ng-deep { + app-input { + .field__input { + width: 100%; + background-color: transparent; + color: var(--grey-for-text) !important; + + @include typography.body-10; + } + } + } + } + + &__files { + display: flex; + flex-direction: column; + gap: 10px; + + &:not(:empty) { + margin-top: 20px; + } + } + + &__actions { + display: flex; + align-items: center; + gap: 5px; + + i { + color: var(--light-white); + } + + input { + display: none; + } + } + + &__attach { + i { + color: var(--dark-grey); + } + } + + &__send { + background-color: var(--accent); + width: 15px; + height: 15px; + border-radius: 3px; + padding: 4px; + color: var(--accent); + cursor: pointer; + } + + &__input { + flex-grow: 1; + resize: none; + background: transparent; + border: none; + outline: none; + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts index 67b86b27..f44e5783 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts @@ -1,7 +1,16 @@ /** @format */ -import { CommonModule } from "@angular/common"; -import { Component, inject, OnDestroy, OnInit, signal } from "@angular/core"; +import { CommonModule, DatePipe } from "@angular/common"; +import { + ChangeDetectorRef, + Component, + ElementRef, + inject, + OnDestroy, + OnInit, + signal, + ViewChild, +} from "@angular/core"; import { Subscription } from "rxjs"; import { ProjectDataService } from "../services/project-data.service"; import { Project } from "@office/models/project.model"; @@ -16,6 +25,9 @@ import { expandElement } from "@utils/expand-element"; import { ParseBreaksPipe, ParseLinksPipe } from "@corelib"; import { FileItemComponent } from "@ui/components/file-item/file-item.component"; import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { TextareaComponent } from "@ui/components/textarea/textarea.component"; +import { BadgeComponent } from "@ui/components/badge/badge.component"; @Component({ selector: "app-kanban-board", @@ -35,24 +47,52 @@ import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-up FileItemComponent, InputComponent, FileUploadItemComponent, + ReactiveFormsModule, + DatePipe, + TextareaComponent, + BadgeComponent, ], standalone: true, }) export class KanbanBoardComponent implements OnInit, OnDestroy { + @ViewChild("descEl") descEl?: ElementRef; + private readonly projectDataService = inject(ProjectDataService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); + private readonly cdRef = inject(ChangeDetectorRef); private subscriptions: Subscription[] = []; + constructor(private readonly fb: FormBuilder) { + this.taskDetailForm = this.fb.group({ + title: ["", Validators.required], + responsible: [null], + performers: this.fb.array([]), + startDate: [null], + deadline: [null], + tags: this.fb.array([]), + goal: [null], + skills: this.fb.array([]), + description: [null], + files: this.fb.array([]), + }); + } + projectBoardInfo = signal(null); boardColumns = signal([]); isTaskDetailOpen = signal(false); + isChangeDescriptionText = signal(false); descriptionExpandable!: boolean; // Флаг необходимости кнопки "Читать полностью" readFullDescription = false; // Флаг показа всех вакансий filesList: any[] = []; + todayDate = new Date(); + tommorowDate = new Date().setDate(this.todayDate.getDate() + 1); + + taskDetailForm: FormGroup; + ngOnInit(): void { const detailInfoUrl$ = this.route.queryParams.subscribe({ next: params => { @@ -113,6 +153,13 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { this.boardColumns.set(mockColumns); } + /** + * Проверка возможности расширения описания после инициализации представления + */ + ngAfterViewInit(): void { + this.checkDescriptionHeigth(); + } + ngOnDestroy(): void { this.subscriptions.forEach($ => $.unsubscribe()); } @@ -136,6 +183,13 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { this.isTaskDetailOpen.set(false); } + onChangeText(event: MouseEvent): void { + event.stopPropagation(); + this.isChangeDescriptionText.set(!this.isChangeDescriptionText()); + + setTimeout(() => this.checkDescriptionHeigth(), 0); + } + /** * Раскрытие/сворачивание описания профиля * @param elem - DOM элемент описания @@ -146,4 +200,11 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { expandElement(elem, expandedClass, isExpanded); this.readFullDescription = !isExpanded; } + + private checkDescriptionHeigth(): void { + const descElement = this.descEl?.nativeElement; + this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight; + + this.cdRef.detectChanges(); + } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/board.model.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/board.model.ts new file mode 100644 index 00000000..0de6d350 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/board.model.ts @@ -0,0 +1,6 @@ +/** @format */ + +export interface Board { + id: number; + name: string; +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts new file mode 100644 index 00000000..a97ace85 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts @@ -0,0 +1,8 @@ +/** @format */ + +import { TaskPreview } from "./task.model"; + +export interface Column { + id: number; + tasks: TaskPreview[]; +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/comments.model.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/comments.model.ts new file mode 100644 index 00000000..65421ae6 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/comments.model.ts @@ -0,0 +1,20 @@ +/** @format */ + +import { FileModel } from "@office/models/file.model"; +import { TaskDetail } from "./task.model"; +import { User } from "@auth/models/user.model"; + +export interface Comment { + id: number; + taskId: TaskDetail["id"]; + text: string; + file: FileModel; + user: { + id: User["id"]; + firstName: User["firstName"]; + lastName: User["lastName"]; + avatar: User["avatar"]; + role: User["speciality"]; + dateTimeCreated: string; + }; +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts new file mode 100644 index 00000000..dccffc93 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts @@ -0,0 +1,49 @@ +/** @format */ + +import { User } from "@auth/models/user.model"; +import { FileModel } from "@office/models/file.model"; +import { Goal } from "@office/models/goals.model"; +import { Skill } from "@office/models/skill"; +import { Tag } from "@office/models/tag.model"; +import { Column } from "./column.model"; + +export interface TaskPreview { + id: number; + columnId: Column["id"]; + name: string; + priority: number; + description: string; + deadlineDate: string; + tag: Tag; + Goal: Goal; + type: string; + file: FileModel; + responsible: { + id: User["id"]; + avatar: User["avatar"]; + }; + performers: { + id: User["id"]; + avatar: User["avatar"]; + }[]; +} + +export interface TaskDetail extends TaskPreview { + score: number; + creator: { + id: User["id"]; + firstName: User["firstName"]; + lastName: User["lastName"]; + avatar: User["avatar"]; + }; + datetimeCreated: string; + dateTaskStart: string; + requiredSkills: Skill[]; + projectGoal: Goal["title"]; +} + +export interface TaskResult { + id: number; + description: string; + file: FileModel; +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html index 2447d881..435af216 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.html @@ -8,17 +8,18 @@ } @if (task?.responsible || task?.performers?.length || task?.files?.length || task.action) {
- @if (task?.responsible) { -
+ @if (task?.responsible; as responsible) { +
- +
- } @if (task?.performers?.length) { + } @if (task?.performers; as performers) { @if (performers.length > 0) { @for (performer of + performers; track $index) {
- +
- } + } } }
@if (task?.files?.length || task?.action) { @@ -35,10 +36,10 @@

04.04.2026

- } @if (task.tag) { - #аналитика - } @if (task.goal) { - выход на рынок для чего + } @if (task.tag; as tag) { + {{ "#" + tag.text }} + } @if (task.goal; as goal) { + {{ goal.text }} }
} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss index 097af114..a4d6accf 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.scss @@ -73,12 +73,15 @@ } &-responsible, - &-performers, &-deadline { display: flex; gap: 2px; align-items: center; + app-avatar { + margin-left: -2px; + } + i { min-width: 6px; } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts index 438a7448..e18cecef 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts @@ -7,12 +7,13 @@ import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { TagComponent } from "@ui/components/tag/tag.component"; import { hexToRgba } from "@utils/helpers/hexToRgba"; import { priorityInfoList } from "projects/core/src/consts/lists/priority-info-list.const"; +import { RouterLink } from "@angular/router"; @Component({ selector: "app-kanban-task", templateUrl: "./kanban-task.component.html", styleUrl: "./kanban-task.component.scss", - imports: [CommonModule, IconComponent, AvatarComponent, TagComponent], + imports: [CommonModule, IconComponent, AvatarComponent, TagComponent, RouterLink], standalone: true, }) export class KanbanTaskComponent { diff --git a/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts b/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts index 8dd59e13..7ea01e81 100644 --- a/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/shared/project-vacancy-card/project-vacancy-card.component.ts @@ -1,6 +1,14 @@ /** @format */ import { CommonModule } from "@angular/common"; -import { Component, Input, OnInit } from "@angular/core"; +import { + ChangeDetectorRef, + Component, + ElementRef, + inject, + Input, + OnInit, + ViewChild, +} from "@angular/core"; import { RouterLink } from "@angular/router"; import { Vacancy } from "@office/models/vacancy.model"; import { IconComponent } from "@uilib"; @@ -49,6 +57,10 @@ export class ProjectVacancyCardComponent implements OnInit { @Input({ required: true }) vacancy!: Vacancy; // Данные вакансии (обязательное поле) @Input() type: "vacancies" | "project" = "project"; + @ViewChild("descEl") descEl?: ElementRef; + + private readonly cdRef = inject(ChangeDetectorRef); + ngOnInit(): void { if (this.type === "project") { this.endSliceOfSkills = 5; @@ -57,6 +69,16 @@ export class ProjectVacancyCardComponent implements OnInit { } } + /** + * Проверка возможности расширения описания после инициализации представления + */ + ngAfterViewInit(): void { + const descElement = this.descEl?.nativeElement; + this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight; + + this.cdRef.detectChanges(); + } + descriptionExpandable!: boolean; // Флаг необходимости кнопки "подробнее" readFullDescription = false; // Флаг показа всех вакансий endSliceOfSkills = 0; diff --git a/projects/social_platform/src/app/ui/components/badge/badge.component.html b/projects/social_platform/src/app/ui/components/badge/badge.component.html new file mode 100644 index 00000000..36473f90 --- /dev/null +++ b/projects/social_platform/src/app/ui/components/badge/badge.component.html @@ -0,0 +1,14 @@ + + +
+ + + +
diff --git a/projects/social_platform/src/app/ui/components/badge/badge.component.scss b/projects/social_platform/src/app/ui/components/badge/badge.component.scss new file mode 100644 index 00000000..9bf82238 --- /dev/null +++ b/projects/social_platform/src/app/ui/components/badge/badge.component.scss @@ -0,0 +1,24 @@ +.badge { + padding: 4px; + border-width: 0.5px; + border-style: solid; + border-radius: var(--rounded-xl); + + &--green { + color: var(--green); + border-color: var(--green); + background-color: var(--green-light); + } + + &--red { + color: var(--red); + background-color: var(--red-light); + border-color: var(--red); + } + + &--gold { + color: var(--gold); + background-color: var(--gold-light); + border-color: var(--gold); + } +} diff --git a/projects/social_platform/src/app/ui/components/badge/badge.component.ts b/projects/social_platform/src/app/ui/components/badge/badge.component.ts new file mode 100644 index 00000000..55c71a76 --- /dev/null +++ b/projects/social_platform/src/app/ui/components/badge/badge.component.ts @@ -0,0 +1,16 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +@Component({ + selector: "app-badge", + templateUrl: "./badge.component.html", + styleUrl: "./badge.component.scss", + imports: [CommonModule], + standalone: true, +}) +export class BadgeComponent { + @Input() color: "green" | "red" | "gold" = "red"; + @Input() type: "deadline" | "start" = "deadline"; +} diff --git a/projects/social_platform/src/app/ui/components/button/button.component.scss b/projects/social_platform/src/app/ui/components/button/button.component.scss index 01f70b2c..222f881e 100644 --- a/projects/social_platform/src/app/ui/components/button/button.component.scss +++ b/projects/social_platform/src/app/ui/components/button/button.component.scss @@ -21,7 +21,7 @@ outline: none; &:hover { - background-color: var(--accent-light); + background-color: var(--accent-medium); box-shadow: -2px 3px 3px rgb(51 51 51 / 20%); } @@ -80,8 +80,8 @@ border: 0.5px solid var(--accent); &:hover { - color: var(--accent-light); - border-color: var(--accent-light); + color: var(--accent-medium); + border-color: var(--accent-medium); box-shadow: -2px 3px 3px rgb(51 51 51 / 20%); } diff --git a/projects/social_platform/src/app/ui/components/tag/tag.component.scss b/projects/social_platform/src/app/ui/components/tag/tag.component.scss index ab402eef..781ea3f3 100644 --- a/projects/social_platform/src/app/ui/components/tag/tag.component.scss +++ b/projects/social_platform/src/app/ui/components/tag/tag.component.scss @@ -3,6 +3,9 @@ .tag { display: flex; align-items: center; + align-self: center; + justify-content: center; + text-align: center; max-width: 300px; padding: 2px 20px; overflow: hidden; diff --git a/projects/social_platform/src/assets/icons/svg/hastag.svg b/projects/social_platform/src/assets/icons/svg/hastag.svg new file mode 100644 index 00000000..75a1a822 --- /dev/null +++ b/projects/social_platform/src/assets/icons/svg/hastag.svg @@ -0,0 +1,3 @@ + + + diff --git a/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg b/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg index fc716876..cffdd94d 100644 --- a/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg +++ b/projects/social_platform/src/assets/icons/symbol/svg/sprite.css.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/projects/social_platform/src/assets/images/projects/shared/divider.png b/projects/social_platform/src/assets/images/projects/shared/divider.png new file mode 100644 index 0000000000000000000000000000000000000000..eaebf52a7b480203293c677b0942c2a099ab32a3 GIT binary patch literal 401 zcmeAS@N?(olHy`uVBq!ia0y~yU~B}kxjEQ?I!*5i zYVYZ*Y_aQU)>l}U$G6YvVED@$hRMhD7P?MvIehGV4+J#IYwS<+J3YHUfA{}R zi#>LEMvM$Dhp)fxe7*QxpEwgkN89@AbDb6bTIR4a2=KvN?Lx+UT|whX@{lh=1&%Iu7e$_5s;s;?85E@U#g+3leygrmW@yOx pQFZsP%a-G*EDR6+)a?Do%^G^`Uhui2%)k(2@O1TaS?83{1OPU&s5k%s literal 0 HcmV?d00001 diff --git a/projects/social_platform/src/styles/_colors.scss b/projects/social_platform/src/styles/_colors.scss index 55149b82..4c8b2b49 100644 --- a/projects/social_platform/src/styles/_colors.scss +++ b/projects/social_platform/src/styles/_colors.scss @@ -14,6 +14,7 @@ // GOLD --gold: #e5b25d; + --gold-light: #FDF8F0; --gold-dark: #c69849; // GRAY @@ -32,6 +33,7 @@ --green-light: #e3f0e8; --green-dark: #297373; --red: #d48a9e; + --red-light: #F4E2E7; --red-dark: #{color.adjust(#d48a9e, $blackness: 10%)}; --blue-dark: #2f36aa; --purple: #9a80e6; From d4e74c36f52e055cfe43f5e2013c7de265720244 Mon Sep 17 00:00:00 2001 From: Awakich Date: Thu, 13 Nov 2025 17:30:43 +0300 Subject: [PATCH 06/22] add colors for badge, styles for badge, task-detail type filled & empty --- .../kanban-board/kanban-board.component.scss | 50 +++++++++---------- .../ui/components/badge/badge.component.scss | 4 +- .../app/ui/components/tag/tag.component.scss | 2 +- .../social_platform/src/styles/_colors.scss | 4 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss index 9ba7f826..abf715b7 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -134,10 +134,10 @@ } &-add-responsible { - cursor: pointer; padding: 8px; - border-radius: var(--rounded-xxl); + cursor: pointer; border: 0.5px dashed var(--accent); + border-radius: var(--rounded-xxl); } &-performers { @@ -152,13 +152,13 @@ } &-add-object { - cursor: pointer; display: flex; align-items: center; - justify-content: center; align-self: center; + justify-content: center; width: 100%; padding: 2px 48px; + cursor: pointer; border: 0.5px solid var(--accent); border-radius: var(--rounded-xl); } @@ -167,16 +167,16 @@ ::ng-deep { app-textarea { .field__input { + min-height: 45px; + padding: 12px 12px 0; + margin-left: -10px; background-color: transparent; border: none; border-radius: 0; - margin-left: -10px; - min-height: 45px; - padding: 12px 12px 0px 12px; &:focus { - box-shadow: none; border-color: none; + box-shadow: none; } } } @@ -185,8 +185,8 @@ &-files { display: flex; - align-items: center; gap: 10px; + align-items: center; margin-bottom: 40px; } @@ -195,8 +195,8 @@ &--empty { display: flex; - align-items: center; gap: 5px; + align-items: center; } } @@ -206,8 +206,8 @@ } &__divider { - margin-top: 5px; width: 100%; + margin-top: 5px; } } @@ -262,17 +262,17 @@ } .form { - padding: 10px 24px; - height: 28px; + display: flex; + align-items: center; + justify-content: space-between; width: 422px; + height: 28px; + padding: 10px 24px; margin-bottom: -25px; margin-left: -25px; background-color: var(--light-white); border: 0.5px solid var(--medium-grey-for-outline); border-radius: var(--rounded-lg); - display: flex; - align-items: center; - justify-content: space-between; &__row { display: flex; @@ -281,15 +281,15 @@ ::ng-deep { app-input { - .field__input { - width: 100%; - background-color: transparent; - color: var(--grey-for-text) !important; + .field__input { + width: 100%; + color: var(--grey-for-text) !important; + background-color: transparent; - @include typography.body-10; + @include typography.body-10; + } } } - } } &__files { @@ -304,8 +304,8 @@ &__actions { display: flex; - align-items: center; gap: 5px; + align-items: center; i { color: var(--light-white); @@ -323,13 +323,13 @@ } &__send { - background-color: var(--accent); width: 15px; height: 15px; - border-radius: 3px; padding: 4px; color: var(--accent); cursor: pointer; + background-color: var(--accent); + border-radius: 3px; } &__input { diff --git a/projects/social_platform/src/app/ui/components/badge/badge.component.scss b/projects/social_platform/src/app/ui/components/badge/badge.component.scss index 9bf82238..5b16d494 100644 --- a/projects/social_platform/src/app/ui/components/badge/badge.component.scss +++ b/projects/social_platform/src/app/ui/components/badge/badge.component.scss @@ -1,13 +1,13 @@ .badge { padding: 4px; - border-width: 0.5px; border-style: solid; + border-width: 0.5px; border-radius: var(--rounded-xl); &--green { color: var(--green); - border-color: var(--green); background-color: var(--green-light); + border-color: var(--green); } &--red { diff --git a/projects/social_platform/src/app/ui/components/tag/tag.component.scss b/projects/social_platform/src/app/ui/components/tag/tag.component.scss index 781ea3f3..261fbd97 100644 --- a/projects/social_platform/src/app/ui/components/tag/tag.component.scss +++ b/projects/social_platform/src/app/ui/components/tag/tag.component.scss @@ -5,11 +5,11 @@ align-items: center; align-self: center; justify-content: center; - text-align: center; max-width: 300px; padding: 2px 20px; overflow: hidden; color: var(--accent); + text-align: center; text-overflow: ellipsis; white-space: nowrap; border-radius: var(--rounded-xxl); diff --git a/projects/social_platform/src/styles/_colors.scss b/projects/social_platform/src/styles/_colors.scss index 4c8b2b49..15918483 100644 --- a/projects/social_platform/src/styles/_colors.scss +++ b/projects/social_platform/src/styles/_colors.scss @@ -14,7 +14,7 @@ // GOLD --gold: #e5b25d; - --gold-light: #FDF8F0; + --gold-light: #fdf8f0; --gold-dark: #c69849; // GRAY @@ -33,7 +33,7 @@ --green-light: #e3f0e8; --green-dark: #297373; --red: #d48a9e; - --red-light: #F4E2E7; + --red-light: #f4e2e7; --red-dark: #{color.adjust(#d48a9e, $blackness: 10%)}; --blue-dark: #2f36aa; --purple: #9a80e6; From d033e35e436303c55e743e3e796a3e3b0595d984 Mon Sep 17 00:00:00 2001 From: Awakich Date: Tue, 18 Nov 2025 18:50:10 +0300 Subject: [PATCH 07/22] add form & initialization for task-detail, add dropdown, create-tag component, fix websockets to subprotocol --- .../consts/lists/actiion-type-list.const.ts | 22 + .../consts/lists/priority-info-list.const.ts | 12 +- .../src/consts/lists/tag-colots.list.const.ts | 36 ++ .../core/src/lib/services/token.service.ts | 6 +- .../app/core/services/websocket.service.ts | 11 +- .../context-menu/context-menu.component.html | 24 + .../context-menu/context-menu.component.scss | 61 +++ .../context-menu/context-menu.component.ts | 195 ++++++++ .../message-input.component.scss | 2 +- .../news-card/news-card.component.scss | 6 +- .../stage-zero/stage-zero.component.ts | 4 +- .../profile/detail/main/main.component.scss | 1 + .../app/office/profile/edit/edit.component.ts | 6 +- .../program/detail/main/main.component.html | 7 +- .../program/detail/main/main.component.ts | 4 +- .../detail/register/register.component.scss | 2 +- .../shared/news-card/news-card.component.html | 113 ----- .../shared/news-card/news-card.component.scss | 239 ---------- .../news-card/news-card.component.spec.ts | 50 -- .../shared/news-card/news-card.component.ts | 363 -------------- .../kanban-board/kanban-board.component.html | 283 ++--------- .../kanban-board/kanban-board.component.scss | 319 +------------ .../kanban-board/kanban-board.component.ts | 99 +--- .../kanban-board/kanban-board.service.ts | 6 +- .../detail/kanban-board/models/task.model.ts | 13 +- .../create-tag-form.component.html | 31 ++ .../create-tag-form.component.scss | 71 +++ .../create-tag-form.component.ts | 61 +++ .../kanban-board-sidebar.component.html | 18 +- .../kanban-board-sidebar.component.scss | 1 + .../sidebar/kanban-board-sidebar.component.ts | 47 +- .../task/detail/task-detail.component.html | 448 ++++++++++++++++++ .../task/detail/task-detail.component.scss | 395 +++++++++++++++ .../task/detail/task-detail.component.ts | 432 +++++++++++++++++ .../shared/task/kanban-task.component.ts | 26 +- .../edit/services/project-form.service.ts | 2 +- .../services/project-resources.service.ts | 1 - .../edit/services/project-vacancy.service.ts | 2 +- .../office/projects/list/list.component.ts | 2 +- .../app/office/projects/projects.component.ts | 2 +- .../src/app/office/services/chat.service.ts | 2 +- .../dropdown/dropdown.component.html | 61 +++ .../dropdown/dropdown.component.scss | 74 +++ .../components/dropdown/dropdown.component.ts | 108 +++++ .../ui/components/input/input.component.html | 20 + .../ui/components/input/input.component.scss | 2 +- .../ui/components/input/input.component.ts | 124 ++--- .../ui/components/modal/modal.component.html | 2 +- .../components/select/select.component.html | 23 +- .../components/select/select.component.scss | 38 -- .../ui/components/select/select.component.ts | 12 +- .../app/ui/components/tag/tag.component.html | 12 +- .../app/ui/components/tag/tag.component.scss | 26 +- .../app/ui/components/tag/tag.component.ts | 30 +- .../src/app/utils/days-untit.ts | 13 + .../src/app/utils/helpers/getPriorityType.ts | 22 + .../{ => helpers}/inviteToProjectMapper.ts | 0 .../src/app/utils/{ => helpers}/stripNull.ts | 0 .../app/utils/{ => helpers}/transformYear.ts | 0 .../{ => helpers}/yearRangeValidators.ts | 0 60 files changed, 2372 insertions(+), 1620 deletions(-) create mode 100644 projects/core/src/consts/lists/actiion-type-list.const.ts create mode 100644 projects/core/src/consts/lists/tag-colots.list.const.ts create mode 100644 projects/social_platform/src/app/office/features/context-menu/context-menu.component.html create mode 100644 projects/social_platform/src/app/office/features/context-menu/context-menu.component.scss create mode 100644 projects/social_platform/src/app/office/features/context-menu/context-menu.component.ts delete mode 100644 projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html delete mode 100644 projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.scss delete mode 100644 projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.spec.ts delete mode 100644 projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.scss create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss create mode 100644 projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts create mode 100644 projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html create mode 100644 projects/social_platform/src/app/ui/components/dropdown/dropdown.component.scss create mode 100644 projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts create mode 100644 projects/social_platform/src/app/utils/days-untit.ts create mode 100644 projects/social_platform/src/app/utils/helpers/getPriorityType.ts rename projects/social_platform/src/app/utils/{ => helpers}/inviteToProjectMapper.ts (100%) rename projects/social_platform/src/app/utils/{ => helpers}/stripNull.ts (100%) rename projects/social_platform/src/app/utils/{ => helpers}/transformYear.ts (100%) rename projects/social_platform/src/app/utils/{ => helpers}/yearRangeValidators.ts (100%) diff --git a/projects/core/src/consts/lists/actiion-type-list.const.ts b/projects/core/src/consts/lists/actiion-type-list.const.ts new file mode 100644 index 00000000..399e0d35 --- /dev/null +++ b/projects/core/src/consts/lists/actiion-type-list.const.ts @@ -0,0 +1,22 @@ +/** @format */ + +export const actionTypeList = [ + { + id: 1, + value: "action", + label: "действие", + additionalInfo: "task", + }, + { + id: 2, + value: "call", + label: "звонок", + additionalInfo: "phone", + }, + { + id: 3, + value: "meet", + label: "встреча", + additionalInfo: "people-bold", + }, +]; diff --git a/projects/core/src/consts/lists/priority-info-list.const.ts b/projects/core/src/consts/lists/priority-info-list.const.ts index c9ddcc8d..688e69a0 100644 --- a/projects/core/src/consts/lists/priority-info-list.const.ts +++ b/projects/core/src/consts/lists/priority-info-list.const.ts @@ -10,37 +10,37 @@ export const priorityInfoList = [ { id: 0, - name: "бэклог", + label: "бэклог", color: "#322299", priorityType: 0, }, { id: 1, - name: "в ближайшие часы", + label: "в ближайшие часы", color: "#A63838", priorityType: 1, }, { id: 2, - name: "высокий", + label: "высокий", color: "#D48A9E", priorityType: 2, }, { id: 3, - name: "средний", + label: "средний", color: "#E5B25D", priorityType: 3, }, { id: 4, - name: "низкий", + label: "низкий", color: "#297373", priorityType: 4, }, { id: 5, - name: "улучшение", + label: "улучшение", color: "#88C9A1", priorityType: 5, }, diff --git a/projects/core/src/consts/lists/tag-colots.list.const.ts b/projects/core/src/consts/lists/tag-colots.list.const.ts new file mode 100644 index 00000000..4a8d1e21 --- /dev/null +++ b/projects/core/src/consts/lists/tag-colots.list.const.ts @@ -0,0 +1,36 @@ +/** @format */ + +export const tagColorsList = [ + { + id: 1, + color: "#8A63E6", + }, + { + id: 2, + color: "#9764BA", + }, + { + id: 3, + color: "#2F36AA", + }, + { + id: 4, + color: "#4CD9F1", + }, + { + id: 5, + color: "#88C9A1", + }, + { + id: 6, + color: "#D48A9E", + }, + { + id: 7, + color: "#E5B25D", + }, + { + id: 8, + color: "#297373", + }, +]; diff --git a/projects/core/src/lib/services/token.service.ts b/projects/core/src/lib/services/token.service.ts index 0fe3ba9e..06f74b0e 100644 --- a/projects/core/src/lib/services/token.service.ts +++ b/projects/core/src/lib/services/token.service.ts @@ -5,7 +5,7 @@ import { map, Observable } from "rxjs"; import { RefreshResponse } from "@auth/models/http.model"; import { plainToInstance } from "class-transformer"; import { Tokens } from "@auth/models/tokens.model"; -import Cookies from "js-cookie"; +import Cookies, { CookieAttributes } from "js-cookie"; import { ApiService, PRODUCTION } from "@corelib"; /** @@ -66,11 +66,13 @@ export class TokenService { * - Используются дефолтные настройки браузера * - Cookies привязаны к текущему домену */ - getCookieOptions() { + getCookieOptions(): CookieAttributes { if (this.production) { return { domain: ".procollab.ru", // Домен для production окружения expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), // 30 дней + secure: true, + sameSite: "None", }; } diff --git a/projects/social_platform/src/app/core/services/websocket.service.ts b/projects/social_platform/src/app/core/services/websocket.service.ts index 224f55e3..a99043f9 100644 --- a/projects/social_platform/src/app/core/services/websocket.service.ts +++ b/projects/social_platform/src/app/core/services/websocket.service.ts @@ -1,9 +1,10 @@ /** @format */ -import { Injectable } from "@angular/core"; +import { inject, Injectable } from "@angular/core"; import { filter, map, Observable, Observer, retry, Subject } from "rxjs"; import { environment } from "@environment"; import * as snakecaseKeys from "snakecase-keys"; import camelcaseKeys from "camelcase-keys"; +import { TokenService } from "@corelib"; /** * Сервис для работы с WebSocket соединениями @@ -20,6 +21,8 @@ export class WebsocketService { /** Subject для обработки входящих сообщений */ private messages$ = new Subject(); + private readonly tokenService = inject(TokenService); + /** Флаг состояния соединения */ public isConnected = false; @@ -36,7 +39,11 @@ export class WebsocketService { */ public connect(path: string): Observable { return new Observable((observer: Observer) => { - this.socket = new WebSocket(environment.websocketUrl + path); + const tokens = this.tokenService.getTokens(); + + const tokenAccess = tokens?.access ? tokens.access : ""; + + this.socket = new WebSocket(environment.websocketUrl + path, ["Bearer", tokenAccess]); this.socket.onopen = () => { this.isConnected = true; diff --git a/projects/social_platform/src/app/office/features/context-menu/context-menu.component.html b/projects/social_platform/src/app/office/features/context-menu/context-menu.component.html new file mode 100644 index 00000000..51877321 --- /dev/null +++ b/projects/social_platform/src/app/office/features/context-menu/context-menu.component.html @@ -0,0 +1,24 @@ + + +
    + @for (item of visibleItems; track item.id) { +
  • + {{ item.label }} +
  • + @if (item.divider && !$last) { +
  • + } } +
diff --git a/projects/social_platform/src/app/office/features/context-menu/context-menu.component.scss b/projects/social_platform/src/app/office/features/context-menu/context-menu.component.scss new file mode 100644 index 00000000..b9c0093d --- /dev/null +++ b/projects/social_platform/src/app/office/features/context-menu/context-menu.component.scss @@ -0,0 +1,61 @@ +/** @format */ + +.context-menu { + position: fixed; + z-index: 9999; + + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + + padding: 8px 0; + margin: 0; + list-style: none; + + transition: opacity 0.15s ease; + outline: none; + + &__item { + padding: 10px 16px; + cursor: pointer; + white-space: nowrap; + user-select: none; + transition: background-color 0.15s ease; + + &:hover { + background-color: rgba(0, 0, 0, 0.04); + } + + &:active { + background-color: rgba(0, 0, 0, 0.08); + } + + &--red { + color: #f44336; + + &:hover { + background-color: rgba(244, 67, 54, 0.08); + } + + &:active { + background-color: rgba(244, 67, 54, 0.16); + } + } + + &--primary { + color: #2196f3; + } + + &--disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + } + } + + &__divider { + height: 1px; + margin: 8px 0; + background-color: rgba(0, 0, 0, 0.08); + } +} diff --git a/projects/social_platform/src/app/office/features/context-menu/context-menu.component.ts b/projects/social_platform/src/app/office/features/context-menu/context-menu.component.ts new file mode 100644 index 00000000..79a7d3ca --- /dev/null +++ b/projects/social_platform/src/app/office/features/context-menu/context-menu.component.ts @@ -0,0 +1,195 @@ +/** @format */ + +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + OnDestroy, + Output, + ViewChild, +} from "@angular/core"; +import { DomPortal } from "@angular/cdk/portal"; +import { Overlay, OverlayRef } from "@angular/cdk/overlay"; +import { ClickOutsideModule } from "ng-click-outside"; +import { CommonModule } from "@angular/common"; + +/** + * Универсальный компонент контекстного меню + * + * Особенности: + * - Автоматическое позиционирование с учетом границ экрана + * - Гибкая конфигурация пунктов меню + * - Поддержка условного отображения пунктов + * - Автоматическое закрытие при клике вне меню + * + * @example + * ```html + * + * ``` + */ +@Component({ + selector: "app-context-menu", + templateUrl: "./context-menu.component.html", + styleUrl: "./context-menu.component.scss", + standalone: true, + imports: [CommonModule, ClickOutsideModule], +}) +export class ContextMenuComponent implements AfterViewInit, OnDestroy { + constructor(private readonly overlay: Overlay) {} + + /** Список пунктов меню */ + @Input({ required: true }) items: any[] = []; + + /** Дополнительный CSS класс для меню */ + @Input() customClass?: string; + + /** Минимальная ширина меню */ + @Input() minWidth?: number; + + /** Событие клика по пункту меню */ + @Output() itemClick = new EventEmitter(); + + /** Событие открытия меню */ + @Output() menuOpen = new EventEmitter(); + + /** Событие закрытия меню */ + @Output() menuClose = new EventEmitter(); + + /** Ссылка на элемент контекстного меню */ + @ViewChild("contextMenu", { static: false }) contextMenu!: ElementRef; + + /** Ссылка на overlay */ + private overlayRef?: OverlayRef; + + /** Portal для контекстного меню */ + private portal?: DomPortal; + + /** Состояние открытия контекстного меню */ + isOpen = false; + + /** Инициализация overlay для контекстного меню */ + ngAfterViewInit(): void { + this.overlayRef = this.overlay.create({ + hasBackdrop: false, + scrollStrategy: this.overlay.scrollStrategies.close(), + }); + this.portal = new DomPortal(this.contextMenu); + } + + /** Очистка ресурсов overlay */ + ngOnDestroy(): void { + this.overlayRef?.dispose(); + } + + /** + * Открытие контекстного меню в заданной позиции + * @param event - событие мыши (для автоматического позиционирования) + * @param position - пользовательская позиция меню + */ + open(event?: MouseEvent, position?: { x: number; y: number }): void { + if (this.isOpen) return; + + this.isOpen = true; + this.menuOpen.emit(); + + // Ждем рендеринга меню для получения его размеров + setTimeout(() => { + const menuHeight = this.contextMenu.nativeElement.offsetHeight; + const menuWidth = this.contextMenu.nativeElement.offsetWidth; + + let positionX: number; + let positionY: number; + + if (event) { + // Автоматическое позиционирование относительно курсора + positionX = event.clientX; + positionY = event.clientY; + + // Проверка выхода за правую границу экрана + if (positionX + menuWidth > window.innerWidth) { + positionX = window.innerWidth - menuWidth - 10; + } + + // Проверка выхода за нижнюю границу экрана + if (positionY + menuHeight > window.innerHeight) { + positionY = event.clientY - menuHeight; + } + } else if (position) { + // Использование пользовательской позиции + positionX = position.x; + positionY = position.y; + } else { + // Позиция по умолчанию + positionX = 0; + positionY = 0; + } + + const positionStrategy = this.overlay + .position() + .global() + .left(positionX + "px") + .top(positionY + "px"); + + this.overlayRef?.updatePositionStrategy(positionStrategy); + + if (!this.overlayRef?.hasAttached()) { + this.overlayRef?.attach(this.portal); + } + + this.contextMenu.nativeElement.focus(); + }, 0); + } + + /** + * Закрытие контекстного меню + */ + close(): void { + if (!this.isOpen) return; + + this.isOpen = false; + this.menuClose.emit(); + this.overlayRef?.detach(); + } + + /** + * Переключение состояния меню + * @param event - событие мыши + */ + toggle(event?: MouseEvent): void { + if (this.isOpen) { + this.close(); + } else { + this.open(event); + } + } + + /** + * Обработчик клика по пункту меню + * @param event - событие клика + * @param itemId - идентификатор пункта меню + */ + onItemClick(event: MouseEvent, itemId: string): void { + event.stopPropagation(); + this.itemClick.emit(itemId); + this.close(); + } + + /** + * Обработчик клика вне меню + */ + onClickOutside(): void { + this.close(); + } + + /** + * Фильтр видимых пунктов меню + */ + get visibleItems(): any[] { + return this.items.filter(item => item.visible !== false); + } +} diff --git a/projects/social_platform/src/app/office/features/message-input/message-input.component.scss b/projects/social_platform/src/app/office/features/message-input/message-input.component.scss index f9f689b1..ad62c820 100644 --- a/projects/social_platform/src/app/office/features/message-input/message-input.component.scss +++ b/projects/social_platform/src/app/office/features/message-input/message-input.component.scss @@ -216,7 +216,7 @@ $button-size: 40px; padding: 36px; background-color: var(--white); border: 2px dashed var(--accent); - border-radius: 5px; + border-radius: var(--rounded-md); transform: translate(-50%, -50%); } diff --git a/projects/social_platform/src/app/office/features/news-card/news-card.component.scss b/projects/social_platform/src/app/office/features/news-card/news-card.component.scss index d449adec..f49fdc35 100644 --- a/projects/social_platform/src/app/office/features/news-card/news-card.component.scss +++ b/projects/social_platform/src/app/office/features/news-card/news-card.component.scss @@ -30,6 +30,8 @@ &__options { position: absolute; + top: 0%; + right: 0%; z-index: 2; padding: 20px 0; background-color: var(--white); @@ -40,10 +42,12 @@ &__option { padding: 5px 20px; cursor: pointer; + width: 120px; transition: background-color 0.2s; + color: var(--grey-for-text); &:hover { - background-color: var(--light-gray); + color: var(--accent); } } diff --git a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts index c640864b..8c8dc8ed 100644 --- a/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts +++ b/projects/social_platform/src/app/office/onboarding/stage-zero/stage-zero.component.ts @@ -21,8 +21,8 @@ import { languageNamesList, } from "projects/core/src/consts/lists/language-info-list.const"; import { IconComponent } from "@uilib"; -import { transformYearStringToNumber } from "@utils/transformYear"; -import { yearRangeValidators } from "@utils/yearRangeValidators"; +import { transformYearStringToNumber } from "@utils/helpers/transformYear"; +import { yearRangeValidators } from "@utils/helpers/yearRangeValidators"; import { ModalComponent } from "@ui/components/modal/modal.component"; import { TooltipComponent } from "@ui/components/tooltip/tooltip.component"; import { generateOptionsList } from "@utils/generate-options-list"; diff --git a/projects/social_platform/src/app/office/profile/detail/main/main.component.scss b/projects/social_platform/src/app/office/profile/detail/main/main.component.scss index 6f5c6b43..89b19e5e 100644 --- a/projects/social_platform/src/app/office/profile/detail/main/main.component.scss +++ b/projects/social_platform/src/app/office/profile/detail/main/main.component.scss @@ -364,6 +364,7 @@ display: flex; gap: 6px; align-items: center; + color: var(--grey-for-text); &--status { padding: 8px; diff --git a/projects/social_platform/src/app/office/profile/edit/edit.component.ts b/projects/social_platform/src/app/office/profile/edit/edit.component.ts index e597b74e..2546a80a 100644 --- a/projects/social_platform/src/app/office/profile/edit/edit.component.ts +++ b/projects/social_platform/src/app/office/profile/edit/edit.component.ts @@ -46,8 +46,8 @@ import { languageLevelsList, languageNamesList, } from "projects/core/src/consts/lists/language-info-list.const"; -import { transformYearStringToNumber } from "@utils/transformYear"; -import { yearRangeValidators } from "@utils/yearRangeValidators"; +import { transformYearStringToNumber } from "@utils/helpers/transformYear"; +import { yearRangeValidators } from "@utils/helpers/yearRangeValidators"; import { Achievement, User } from "@auth/models/user.model"; import { generateOptionsList } from "@utils/generate-options-list"; import { UploadFileComponent } from "@ui/components/upload-file/upload-file.component"; @@ -164,7 +164,7 @@ export class ProfileEditComponent implements OnInit, OnDestroy, AfterViewInit { // skills speciality: ["", [Validators.required]], skills: [[]], - avatar: [""], + avatar: ["", Validators.required], aboutMe: [""], typeSpecific: this.fb.group({}), }); diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.html b/projects/social_platform/src/app/office/program/detail/main/main.component.html index b4c9e367..b83598b6 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.html +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.html @@ -45,14 +45,15 @@

о программе

@if (program.isUserManager) { } @for (n of news(); track n.id) { - + > }
diff --git a/projects/social_platform/src/app/office/program/detail/main/main.component.ts b/projects/social_platform/src/app/office/program/detail/main/main.component.ts index 6c0583b1..176476ca 100644 --- a/projects/social_platform/src/app/office/program/detail/main/main.component.ts +++ b/projects/social_platform/src/app/office/program/detail/main/main.component.ts @@ -27,7 +27,6 @@ import { FeedNews } from "@office/projects/models/project-news.model"; import { expandElement } from "@utils/expand-element"; import { ParseBreaksPipe, ParseLinksPipe } from "projects/core"; import { UserLinksPipe } from "@core/pipes/user-links.pipe"; -import { ProgramNewsCardComponent } from "../shared/news-card/news-card.component"; import { ButtonComponent, IconComponent } from "@ui/components"; import { ApiPagination } from "@models/api-pagination.model"; import { TagComponent } from "@ui/components/tag/tag.component"; @@ -40,6 +39,7 @@ import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component" import { NewsFormComponent } from "@office/features/news-form/news-form.component"; import { AsyncPipe } from "@angular/common"; import { AvatarComponent } from "@uilib"; +import { NewsCardComponent } from "@office/features/news-card/news-card.component"; @Component({ selector: "app-main", @@ -49,7 +49,6 @@ import { AvatarComponent } from "@uilib"; imports: [ IconComponent, ButtonComponent, - ProgramNewsCardComponent, UserLinksPipe, AsyncPipe, ParseBreaksPipe, @@ -63,6 +62,7 @@ import { AvatarComponent } from "@uilib"; AvatarComponent, TagComponent, RouterModule, + NewsCardComponent, ], }) export class ProgramDetailMainComponent implements OnInit, OnDestroy { diff --git a/projects/social_platform/src/app/office/program/detail/register/register.component.scss b/projects/social_platform/src/app/office/program/detail/register/register.component.scss index e8f280db..089025b0 100644 --- a/projects/social_platform/src/app/office/program/detail/register/register.component.scss +++ b/projects/social_platform/src/app/office/program/detail/register/register.component.scss @@ -7,7 +7,7 @@ .form { padding: 24px; background-color: var(--white); - border-radius: 5px; + border-radius: var(--rounded-md); &__fieldset { &:not(:last-child) { diff --git a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html b/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html deleted file mode 100644 index 58febdee..00000000 --- a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.html +++ /dev/null @@ -1,113 +0,0 @@ - -
-
-
- -
-
-
{{ newsItem.name }}
- @if (newsItem.pin) { - - } -
-
-
- @if(isOwner) { -
-
- -
- @if (menuOpen) { -
    -
  • Удалить
  • -
- } -
- } -
- @if (newsItem.text) { -
-

-
- } @if (editMode) { -
    - @for (f of imagesEditList; track f.id) { - - } -
-
    - @for (f of filesEditList; track f.id) { - - } -
- } @if (newsTextExpandable && !editMode) { -
- {{ readMore ? "скрыть" : "читать полностью" }} -
- } @if (!editMode) { - - } @if (!editMode && filesViewList.length) { -
- @for (f of filesViewList; track $index) { - - } -
- } @if (!editMode) { - - } -
diff --git a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.scss b/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.scss deleted file mode 100644 index 3c819c82..00000000 --- a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.scss +++ /dev/null @@ -1,239 +0,0 @@ -@use "styles/responsive"; -@use "styles/typography"; - -.card { - padding: 24px 12px; - background-color: var(--light-white); - border: 0.5px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-lg); - - &__head { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 10px; - } - - &__menu { - position: relative; - } - - &__dots { - display: flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - color: var(--black); - cursor: pointer; - } - - &__options { - position: absolute; - z-index: 2; - padding: 20px 0; - background-color: var(--white); - border: 1px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-xl); - } - - &__option { - padding: 5px 20px; - cursor: pointer; - transition: background-color 0.2s; - - &:hover { - background-color: var(--light-gray); - } - } - - &__avatar { - width: 40px; - height: 40px; - margin-right: 10px; - border-radius: 50%; - } - - &__title { - display: flex; - align-items: center; - } - - &__top { - display: flex; - gap: 10px; - align-items: center; - } - - &__name { - color: var(--black); - } - - &__date { - color: var(--dark-grey); - } - - /* stylelint-disable value-no-vendor-prefix */ - &__text { - white-space: break-spaces; - - @include typography.body-10; - - p { - display: -webkit-box; - overflow: hidden; - color: var(--black); - text-overflow: ellipsis; - -webkit-box-orient: vertical; - -webkit-line-clamp: 4; - transition: all 0.7s ease-in-out; - - &.expanded { - -webkit-line-clamp: unset; - } - } - } - - /* stylelint-enable value-no-vendor-prefix */ - - &__edit-files { - display: flex; - flex-direction: column; - gap: 10px; - - &:not(:empty) { - margin-top: 30px; - } - } - - &__gallery { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 10px; - margin-bottom: 10px; - } - - &__files { - display: flex; - flex-direction: column; - gap: 10px; - margin-bottom: 20px; - } - - &__img { - position: relative; - - img { - width: 100%; - object-fit: cover; - } - } - - &__img-like { - position: absolute; - top: 50%; - left: 50%; - display: flex; - align-items: center; - justify-content: center; - width: 75px; - height: 75px; - color: var(--accent); - background-color: var(--white); - border-radius: var(--rounded-xl); - transition: transform 0.1s ease-in-out; - transform: translate(-50%, -50%) scale(0); - - &--show { - transform: translate(-50%, -50%) scale(1); - } - } - - &__footer { - margin-top: 10px; - } - - &__read-more { - margin-bottom: 10px; - } -} - -.footer { - display: flex; - align-items: center; - justify-content: space-between; - - &__left { - display: flex; - gap: 5px; - align-items: center; - } - - &__item { - display: flex; - align-items: center; - color: var(--dark-grey); - - &:not(:last-child) { - margin-right: 5px; - } - - i { - margin-right: 3px; - } - } - - &__like { - cursor: pointer; - - &--active { - color: var(--accent); - } - } -} - -.share { - color: var(--dark-grey); - - &__icon { - cursor: pointer; - } -} - -.read-more { - margin-top: 12px; - color: var(--accent); - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: var(--accent-dark); - } -} - -.editor-footer { - display: flex; - justify-content: space-between; - padding-top: 10px; - margin-top: 20px; - border-top: 1px solid var(--medium-grey-for-outline); - - &__actions { - display: flex; - - app-button { - display: block; - margin-right: 10px; - } - } - - &__attach { - color: var(--dark-grey); - cursor: pointer; - - input { - display: none; - } - } -} diff --git a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.spec.ts b/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.spec.ts deleted file mode 100644 index 9dbf5bff..00000000 --- a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** @format */ - -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { ProgramNewsCardComponent } from "./news-card.component"; -import { RouterTestingModule } from "@angular/router/testing"; -import { ReactiveFormsModule } from "@angular/forms"; -import { of } from "rxjs"; -import { ProjectNewsService } from "@office/projects/detail/services/project-news.service"; -import { AuthService } from "@auth/services"; -import { HttpClientTestingModule } from "@angular/common/http/testing"; -import { DayjsPipe } from "projects/core"; -import { FeedNews } from "@office/projects/models/project-news.model"; - -describe("NewsCardComponent", () => { - let component: ProgramNewsCardComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - const projectNewsServiceSpy = jasmine.createSpyObj(["addNews"]); - const authSpy = { - profile: of({}), - }; - - await TestBed.configureTestingModule({ - imports: [ - RouterTestingModule, - ReactiveFormsModule, - HttpClientTestingModule, - ProgramNewsCardComponent, - DayjsPipe, - ], - providers: [ - { provide: ProjectNewsService, useValue: projectNewsServiceSpy }, - { provide: AuthService, useValue: authSpy }, - ], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(ProgramNewsCardComponent); - component = fixture.componentInstance; - component.newsItem = FeedNews.default(); - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.ts b/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.ts deleted file mode 100644 index 2c632969..00000000 --- a/projects/social_platform/src/app/office/program/detail/shared/news-card/news-card.component.ts +++ /dev/null @@ -1,363 +0,0 @@ -/** @format */ - -import { - AfterViewInit, - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - OnInit, - Output, - ViewChild, -} from "@angular/core"; -import { SnackbarService } from "@ui/services/snackbar.service"; -import { ActivatedRoute } from "@angular/router"; -import { expandElement } from "@utils/expand-element"; -import { FileModel } from "@office/models/file.model"; -import { nanoid } from "nanoid"; -import { FileService } from "@core/services/file.service"; -import { forkJoin, noop, Observable, tap } from "rxjs"; -import { DayjsPipe, ParseLinksPipe } from "projects/core"; -import { FileItemComponent } from "@ui/components/file-item/file-item.component"; -import { IconComponent } from "@ui/components"; -import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; -import { ImgCardComponent } from "@office/shared/img-card/img-card.component"; -import { FeedNews } from "@office/projects/models/project-news.model"; - -/** - * Компонент карточки новости программы - * - * Отображает отдельную новость в ленте программы с полным функционалом: - * - Просмотр текста новости с возможностью развернуть/свернуть - * - Отображение прикрепленных файлов (изображения и документы) - * - Режим редактирования новости (для владельца) - * - Взаимодействие с новостью (лайки, копирование ссылки) - * - Загрузка и удаление файлов в режиме редактирования - * - * Принимает: - * @Input newsItem: FeedNews - Объект новости для отображения - * @Input isOwner: boolean - Является ли пользователь владельцем новости - * - * Генерирует события: - * @Output delete: EventEmitter - Удаление новости - * @Output like: EventEmitter - Лайк/дизлайк новости - * @Output edited: EventEmitter - Редактирование новости - * - * Зависимости: - * @param {SnackbarService} snackbarService - Для уведомлений - * @param {FileService} fileService - Для работы с файлами - * @param {ActivatedRoute} route - Для получения ID программы - * @param {ChangeDetectorRef} cdRef - Для обновления представления - * - * Состояние: - * @property {boolean} newsTextExpandable - Можно ли развернуть текст - * @property {boolean} readMore - Развернут ли текст новости - * @property {boolean} editMode - Активен ли режим редактирования - * @property {boolean[]} showLikes - Массив состояний показа лайков для изображений - * - * Файлы: - * @property {FileModel[]} imagesViewList - Изображения для просмотра - * @property {FileModel[]} filesViewList - Файлы для просмотра - * @property {Array} imagesEditList - Изображения в режиме редактирования - * @property {Array} filesEditList - Файлы в режиме редактирования - * - * Методы: - * @method onCopyLink() - Копирует ссылку на новость в буфер обмена - * @method onUploadFile(event) - Загружает новые файлы - * @method onDeletePhoto(fId) - Удаляет изображение - * @method onDeleteFile(fId) - Удаляет файл - * @method onRetryUpload(id) - Повторяет загрузку файла при ошибке - * @method onTouchImg(event, imgIdx) - Обработчик двойного тапа для лайка - * @method onExpandNewsText() - Разворачивает/сворачивает текст новости - * - * Особенности: - * - Разделение файлов на изображения и документы - * - Поддержка drag&drop для загрузки файлов - * - Обработка ошибок загрузки с возможностью повтора - * - Двойной тап на изображениях для лайка (мобильные устройства) - * - Автоматическое определение высоты текста для кнопки "Читать далее" - * - * Возвращает: - * HTML шаблон карточки новости со всем функционалом - */ -@Component({ - selector: "app-program-news-card", - templateUrl: "./news-card.component.html", - styleUrl: "./news-card.component.scss", - standalone: true, - imports: [ - ImgCardComponent, - FileUploadItemComponent, - IconComponent, - FileItemComponent, - DayjsPipe, - ParseLinksPipe, - ], -}) -export class ProgramNewsCardComponent implements OnInit, AfterViewInit { - constructor( - private readonly snackbarService: SnackbarService, - private readonly fileService: FileService, - private readonly route: ActivatedRoute, - private readonly cdRef: ChangeDetectorRef - ) {} - - @Input({ required: true }) newsItem!: FeedNews; - @Input() isOwner!: boolean; - @Output() delete = new EventEmitter(); - @Output() like = new EventEmitter(); - @Output() edited = new EventEmitter(); - - newsTextExpandable!: boolean; - readMore = false; - editMode = false; - - /** Состояние меню действий */ - menuOpen = false; - - /** - * Закрытие меню действий - */ - onCloseMenu() { - this.menuOpen = false; - } - - ngOnInit(): void { - this.showLikes = this.newsItem.files.map(() => false); - - this.imagesViewList = this.newsItem.files.filter( - f => f.mimeType.split("/")[0] === "image" || f.mimeType.split("/")[1] === "x-empty" - ); - this.filesViewList = this.newsItem.files.filter( - f => f.mimeType.split("/")[0] !== "image" && f.mimeType.split("/")[1] !== "x-empty" - ); - - this.imagesEditList = this.imagesViewList.map(file => ({ - src: file.link, - id: nanoid(), - error: false, - loading: false, - tempFile: null, - })); - - this.filesEditList = this.filesViewList.map(file => ({ - src: file.link, - id: nanoid(), - error: "", - loading: false, - name: file.name, - size: file.size, - type: file.mimeType, - tempFile: null, - })); - } - - imagesViewList: FileModel[] = []; - filesViewList: FileModel[] = []; - - @ViewChild("newsTextEl") newsTextEl?: ElementRef; - - ngAfterViewInit(): void { - const newsTextElem = this.newsTextEl?.nativeElement; - this.newsTextExpandable = newsTextElem?.clientHeight < newsTextElem?.scrollHeight; - - this.cdRef.detectChanges(); - } - - onCopyLink(): void { - const programId = this.route.snapshot.params["programId"]; - - navigator.clipboard - .writeText(`https://app.procollab.ru/office/program/${programId}/news/${this.newsItem.id}`) - .then(() => { - this.snackbarService.success("Ссылка скопирована"); - }); - } - - imagesEditList: { - id: string; - src: string; - loading: boolean; - error: boolean; - tempFile: File | null; - }[] = []; - - filesEditList: { - id: string; - src: string; - loading: boolean; - error: string; - name: string; - size: number; - type: string; - tempFile: File | null; - }[] = []; - - onUploadFile(event: Event) { - const files = (event.currentTarget as HTMLInputElement).files; - if (!files) return; - - const observableArray: Observable[] = []; - for (let i = 0; i < files.length; i++) { - const fileType = files[i].type.split("/")[0]; - - if (fileType === "image") { - const fileObj: ProgramNewsCardComponent["imagesEditList"][0] = { - id: nanoid(2), - src: "", - loading: true, - error: false, - tempFile: files[0], - }; - this.imagesEditList.push(fileObj); - - observableArray.push( - this.fileService.uploadFile(files[i]).pipe( - tap(file => { - fileObj.src = file.url; - fileObj.loading = false; - - if (fileObj.tempFile) { - this.imagesViewList.push({ - name: fileObj.tempFile.name, - size: fileObj.tempFile.size, - mimeType: fileObj.tempFile.type, - link: fileObj.src, - datetimeUploaded: "", - extension: "", - user: 0, - }); - } - - fileObj.tempFile = null; - }) - ) - ); - } else { - const fileObj: ProgramNewsCardComponent["filesEditList"][0] = { - id: nanoid(2), - loading: true, - error: "", - src: "", - tempFile: files[0], - name: "", - size: 0, - type: "", - }; - this.filesEditList.push(fileObj); - - observableArray.push( - this.fileService.uploadFile(files[i]).pipe( - tap(file => { - fileObj.loading = false; - fileObj.src = file.url; - - if (fileObj.tempFile) { - this.filesViewList.push({ - name: fileObj.tempFile.name, - size: fileObj.tempFile.size, - mimeType: fileObj.tempFile.type, - link: fileObj.src, - datetimeUploaded: "", - extension: "", - user: 0, - }); - } - }) - ) - ); - } - } - - forkJoin(observableArray).subscribe(noop); - // const fileObj: NewsCardComponent["imagesEditList"][0] = { - // id: nanoid(2), - // src: "", - // loading: true, - // error: false, - // tempFile: files[0], - // }; - // this.imagesEditList.push(fileObj); - // this.fileService.uploadFile(files[0]).subscribe({ - // next: file => { - // fileObj.src = file.url; - // fileObj.loading = false; - // - // fileObj.tempFile = null; - // }, - // error: () => { - // fileObj.error = true; - // fileObj.loading = false; - // }, - // }); - } - - onDeletePhoto(fId: string) { - const fileIdx = this.imagesEditList.findIndex(f => f.id === fId); - - if (this.imagesEditList[fileIdx].src) { - this.imagesEditList[fileIdx].loading = true; - this.fileService.deleteFile(this.imagesEditList[fileIdx].src).subscribe(() => { - this.imagesEditList.splice(fileIdx, 1); - }); - } else { - this.imagesEditList.splice(fileIdx, 1); - } - } - - onDeleteFile(fId: string) { - const fileIdx = this.filesEditList.findIndex(f => f.id === fId); - - if (this.filesEditList[fileIdx].src) { - this.filesEditList[fileIdx].loading = true; - this.fileService.deleteFile(this.filesEditList[fileIdx].src).subscribe(() => { - this.filesEditList.splice(fileIdx, 1); - }); - } else { - this.filesEditList.splice(fileIdx, 1); - } - } - - onRetryUpload(id: string) { - const fileObj = this.imagesEditList.find(f => f.id === id); - if (!fileObj || !fileObj.tempFile) return; - - fileObj.loading = true; - fileObj.error = false; - - this.fileService.uploadFile(fileObj.tempFile).subscribe({ - next: file => { - fileObj.src = file.url; - fileObj.loading = false; - - fileObj.tempFile = null; - }, - error: () => { - fileObj.error = true; - fileObj.loading = false; - }, - }); - } - - showLikes: boolean[] = []; - - lastTouch = 0; - onTouchImg(_event: TouchEvent, imgIdx: number) { - if (Date.now() - this.lastTouch < 300) { - this.like.emit(this.newsItem.id); - this.showLikes[imgIdx] = true; - - setTimeout(() => { - this.showLikes[imgIdx] = false; - }, 1000); - } - - this.lastTouch = Date.now(); - } - - onExpandNewsText(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void { - expandElement(elem, expandedClass, isExpanded); - this.readMore = !isExpanded; - } -} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html index 91471ba8..c2291a1e 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.html @@ -4,275 +4,46 @@
@if (projectBoardInfo(); as projectBoardInfo) { - } @for (boardColumn of boardColumns(); track $index) { -
-
-

{{ boardColumn.tasks.length }}

-
- @if (boardColumn.locked) { - - } -

{{ boardColumn.name }}

-
- -
- -
- @for (task of boardColumn.tasks; track task.id) { - - } -
- - -
- } @if (isTaskDetailOpen()) { -
-
- прикрепить результат -
- -
-
-
- -
-

Настроить процессы

- -
- -
-
- -

Федор Е

-
- -

12.10.2025 21:00

-
- -
- divider image -
- -
- @if (taskDetailForm.get("responsible"); as responsible) { -
-
- -
ответственный
-
- -
- @if (responsible.value) { - -

{{ responsible.value.name }}

- } @else { -
- -
- } -
-
- } @if (taskDetailForm.get("performers"); as performers) { -
-
- -
исполнители
-
- -
- @if (performers.value.length > 0) { -
- @for (performer of performers.value; track $index) { - - } -
- } @else { -
- -
- } -
-
- } @if (taskDetailForm.get("startDate"); as startDate) { -
-
- -

начало

-
- -
-

{{ startDate.value || todayDate | date: "dd.MM.yy" }}

- - закончена -
-
- } @if (taskDetailForm.get("deadline"); as deadline) { -
-
- -

дедлайн

-
- -
-

{{ deadline.value || tommorowDate | date: "dd.MM.yy" }}

- - 10 дней -
-
- } -
- -
- @if (taskDetailForm.get("tags"); as tags) { -
-
- -

тег

-
- - @if (tags.value.length > 0) { -
- @for (tag of tags.value; track $index) { - {{ "#" + tag.name }} + } +
+ @for (boardColumn of boardColumns(); track $index) { +
+
+

{{ boardColumn.tasks.length }}

+
+ @if (boardColumn.locked) { + } +

{{ boardColumn.name }}

- } @else { -
- -
- } -
- } @if (taskDetailForm.get("goal"); as goal) { -
-
- -

цель

-
- - @if (goal.value) { - {{ - goal.value.name.length > 20 ? goal.value.name.slice(0, 15) + "..." : goal.value.name - }} - } @else { -
- -
- } +
- } @if (taskDetailForm.get("skills"); as skills) { -
-
- -

навыки

-
- @if (skills.value.length > 0) { -
- @for (skill of skills.value; track $index) { - {{ skill.name }} - } -
- } @else { -
- -
+
+ @for (task of boardColumn.tasks; track task.id) { + }
- } -
-
- divider image -
- - @if (taskDetailForm.get("description"); as description) { -
-
- @if (isChangeDescriptionText() && description.value) { -

- @if (descriptionExpandable) { -
- {{ readFullDescription ? "cкрыть" : "подробнее" }} -
- } } @else { - - } -
+
} -
- @if (taskDetailForm.get("files"); as files) { @if (files.value.length >= 0 && - files.value.length <= 3) { @for (file of files.value; track $index) { - - } @if (!(files.value.length === 3)) { -
-
- -
- -

файл

+
+
+
- } } } -
- -
- -
-
- - -
- - -
- -
-
- @for (f of filesList; track f.id) { - - } - -
-
+ + @if (isTaskDetailOpen()) { + }
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss index abf715b7..e5cfe76f 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.scss @@ -1,18 +1,21 @@ -@use "styles/typography"; + .kanban { &__wrapper { position: relative; display: grid; - grid-template-columns: 1fr 3fr 3fr 3fr; + grid-template-columns: 1fr 9fr; grid-gap: 20px; } &__column { - i, - p { - color: var(--accent); - cursor: pointer; + flex: 0 0 calc(33.333% - 14px); + min-width: calc(33.333% - 14px); + + &-wrapper { + display: flex; + gap: 20px; + overflow-y: hidden; } &-locked { @@ -28,315 +31,33 @@ padding: 5px 12px; border: 0.5px solid var(--accent); border-radius: var(--rounded-xl); - } - - &--add-task { - display: flex; - align-items: center; - align-self: center; - justify-content: center; - margin-top: 20px; - } - } - - &__tasks { - display: flex; - flex-direction: column; - gap: 20px; - align-items: center; - margin-top: 20px; - } - - &__detail { - position: absolute; - top: 0%; - right: 0%; - width: 100%; - max-width: 422px; - padding: 24px; - background-color: var(--light-white); - border: 0.5px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-lg); - - &-top { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 24px; - - &-menu { - display: flex; - gap: 10px; - align-items: center; - } - } - - &-priority { - width: 15px; - height: 15px; - background-color: var(--green); - border-radius: var(--rounded-xxl); - } - &-general { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 10px; - margin-bottom: 5px; - border-bottom: 0.5px solid var(--accent); - - p { - color: var(--accent) !important; - } - } - - p { color: var(--grey-for-text); } - - &-info { - display: flex; - gap: 10px; - align-items: flex-start; - margin-top: 15px; - - &-name { - display: flex; - gap: 3px; - align-items: center; - - &--date { - display: flex; - gap: 3px; - align-items: center; - - p, - i { - color: var(--green-dark) !important; - } - } - - h6 { - color: var(--accent); - } - } - - &-list { - display: flex; - flex-direction: column; - gap: 4px; - } - } - - &-info-wrapper { - display: flex; - flex-direction: column; - gap: 5px; - } - - &-add-responsible { - padding: 8px; - cursor: pointer; - border: 0.5px dashed var(--accent); - border-radius: var(--rounded-xxl); - } - - &-performers { - display: flex; - gap: 4px; - align-items: center; - margin-left: 12px; - - app-avatar { - margin-left: -12px; + &--empty { + cursor: pointer; + justify-content: center; } } - &-add-object { + &--add-task { display: flex; align-items: center; align-self: center; justify-content: center; - width: 100%; - padding: 2px 48px; - cursor: pointer; - border: 0.5px solid var(--accent); - border-radius: var(--rounded-xl); - } - - &-description { - ::ng-deep { - app-textarea { - .field__input { - min-height: 45px; - padding: 12px 12px 0; - margin-left: -10px; - background-color: transparent; - border: none; - border-radius: 0; - - &:focus { - border-color: none; - box-shadow: none; - } - } - } - } - } - - &-files { - display: flex; - gap: 10px; - align-items: center; - margin-bottom: 40px; - } - - &-file { - width: 116px; - - &--empty { - display: flex; - gap: 5px; - align-items: center; - } - } - - i { - color: var(--accent); - } - } - - &__divider { - width: 100%; - margin-top: 5px; - } -} - -.about { - /* stylelint-disable value-no-vendor-prefix */ - &__text { - p { - display: -webkit-box; - overflow: hidden; - color: var(--grey-for-text); - text-overflow: ellipsis; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - transition: all 0.7s ease-in-out; - - &.expanded { - -webkit-line-clamp: unset; - } + margin-top: 20px; } - ::ng-deep a { + i, p { color: var(--accent); - text-decoration: underline; - text-decoration-color: transparent; - text-underline-offset: 3px; - transition: text-decoration-color 0.2s; - - &:hover { - text-decoration-color: var(--accent); - } - } - } - - /* stylelint-enable value-no-vendor-prefix */ - - &__read-full { - margin-top: 2px; - color: var(--accent); - cursor: pointer; - } -} - -.read-more { - margin-top: 12px; - color: var(--accent); - cursor: pointer; - transition: color 0.2s; - - &:hover { - color: var(--accent-dark); - } -} - -.form { - display: flex; - align-items: center; - justify-content: space-between; - width: 422px; - height: 28px; - padding: 10px 24px; - margin-bottom: -25px; - margin-left: -25px; - background-color: var(--light-white); - border: 0.5px solid var(--medium-grey-for-outline); - border-radius: var(--rounded-lg); - - &__row { - display: flex; - align-items: center; - width: 100%; - - ::ng-deep { - app-input { - .field__input { - width: 100%; - color: var(--grey-for-text) !important; - background-color: transparent; - - @include typography.body-10; - } - } + cursor: pointer; } } - &__files { + &__tasks { display: flex; flex-direction: column; - gap: 10px; - - &:not(:empty) { - margin-top: 20px; - } - } - - &__actions { - display: flex; - gap: 5px; + gap: 20px; align-items: center; - - i { - color: var(--light-white); - } - - input { - display: none; - } - } - - &__attach { - i { - color: var(--dark-grey); - } - } - - &__send { - width: 15px; - height: 15px; - padding: 4px; - color: var(--accent); - cursor: pointer; - background-color: var(--accent); - border-radius: 3px; - } - - &__input { - flex-grow: 1; - resize: none; - background: transparent; - border: none; - outline: none; + margin-top: 20px; + overflow-y: scroll; } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts index f44e5783..5b924209 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts @@ -1,33 +1,16 @@ /** @format */ -import { CommonModule, DatePipe } from "@angular/common"; -import { - ChangeDetectorRef, - Component, - ElementRef, - inject, - OnDestroy, - OnInit, - signal, - ViewChild, -} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, inject, OnDestroy, OnInit, signal } from "@angular/core"; import { Subscription } from "rxjs"; import { ProjectDataService } from "../services/project-data.service"; import { Project } from "@office/models/project.model"; import { KanbanBoardSidebarComponent } from "./shared/sidebar/kanban-board-sidebar.component"; import { ActivatedRoute, Router } from "@angular/router"; -import { IconComponent, ButtonComponent, InputComponent } from "@ui/components"; +import { IconComponent } from "@ui/components"; import { KanbanTaskComponent } from "./shared/task/kanban-task.component"; import { ClickOutsideModule } from "ng-click-outside"; -import { AvatarComponent } from "@ui/components/avatar/avatar.component"; -import { TagComponent } from "@ui/components/tag/tag.component"; -import { expandElement } from "@utils/expand-element"; -import { ParseBreaksPipe, ParseLinksPipe } from "@corelib"; -import { FileItemComponent } from "@ui/components/file-item/file-item.component"; -import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; -import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; -import { TextareaComponent } from "@ui/components/textarea/textarea.component"; -import { BadgeComponent } from "@ui/components/badge/badge.component"; +import { TaskDetailComponent } from "./shared/task/detail/task-detail.component"; @Component({ selector: "app-kanban-board", @@ -39,59 +22,19 @@ import { BadgeComponent } from "@ui/components/badge/badge.component"; IconComponent, KanbanTaskComponent, ClickOutsideModule, - ButtonComponent, - AvatarComponent, - TagComponent, - ParseBreaksPipe, - ParseLinksPipe, - FileItemComponent, - InputComponent, - FileUploadItemComponent, - ReactiveFormsModule, - DatePipe, - TextareaComponent, - BadgeComponent, + TaskDetailComponent, ], standalone: true, }) export class KanbanBoardComponent implements OnInit, OnDestroy { - @ViewChild("descEl") descEl?: ElementRef; - private readonly projectDataService = inject(ProjectDataService); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); - private readonly cdRef = inject(ChangeDetectorRef); private subscriptions: Subscription[] = []; - constructor(private readonly fb: FormBuilder) { - this.taskDetailForm = this.fb.group({ - title: ["", Validators.required], - responsible: [null], - performers: this.fb.array([]), - startDate: [null], - deadline: [null], - tags: this.fb.array([]), - goal: [null], - skills: this.fb.array([]), - description: [null], - files: this.fb.array([]), - }); - } - projectBoardInfo = signal(null); boardColumns = signal([]); isTaskDetailOpen = signal(false); - isChangeDescriptionText = signal(false); - - descriptionExpandable!: boolean; // Флаг необходимости кнопки "Читать полностью" - readFullDescription = false; // Флаг показа всех вакансий - - filesList: any[] = []; - - todayDate = new Date(); - tommorowDate = new Date().setDate(this.todayDate.getDate() + 1); - - taskDetailForm: FormGroup; ngOnInit(): void { const detailInfoUrl$ = this.route.queryParams.subscribe({ @@ -153,13 +96,6 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { this.boardColumns.set(mockColumns); } - /** - * Проверка возможности расширения описания после инициализации представления - */ - ngAfterViewInit(): void { - this.checkDescriptionHeigth(); - } - ngOnDestroy(): void { this.subscriptions.forEach($ => $.unsubscribe()); } @@ -182,29 +118,4 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { }); this.isTaskDetailOpen.set(false); } - - onChangeText(event: MouseEvent): void { - event.stopPropagation(); - this.isChangeDescriptionText.set(!this.isChangeDescriptionText()); - - setTimeout(() => this.checkDescriptionHeigth(), 0); - } - - /** - * Раскрытие/сворачивание описания профиля - * @param elem - DOM элемент описания - * @param expandedClass - CSS класс для раскрытого состояния - * @param isExpanded - текущее состояние (раскрыто/свернуто) - */ - onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void { - expandElement(elem, expandedClass, isExpanded); - this.readFullDescription = !isExpanded; - } - - private checkDescriptionHeigth(): void { - const descElement = this.descEl?.nativeElement; - this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight; - - this.cdRef.detectChanges(); - } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts index c67156a0..ccd279e2 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.service.ts @@ -2,6 +2,8 @@ import { inject, Injectable } from "@angular/core"; import { ApiService } from "@corelib"; +import { TaskDetail } from "./models/task.model"; +import { Observable } from "rxjs"; @Injectable({ providedIn: "root", @@ -18,7 +20,7 @@ export class KanbanBoardService { return this.apiService.get(`${this.KANBAN_BOARD_URL}/`); } - getTaskById(taskId: number) { - return this.apiService.get(`${this.KANBAN_BOARD_URL}/`); + getTaskById(taskId: number): Observable { + return this.apiService.get(`${this.KANBAN_BOARD_URL}/`); } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts index dccffc93..78ebff94 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/task.model.ts @@ -10,14 +10,14 @@ import { Column } from "./column.model"; export interface TaskPreview { id: number; columnId: Column["id"]; - name: string; + title: string; priority: number; description: string; deadlineDate: string; - tag: Tag; - Goal: Goal; + tags: Tag[]; + goal: Goal; type: string; - file: FileModel; + files: FileModel; responsible: { id: User["id"]; avatar: User["avatar"]; @@ -39,7 +39,10 @@ export interface TaskDetail extends TaskPreview { datetimeCreated: string; dateTaskStart: string; requiredSkills: Skill[]; - projectGoal: Goal["title"]; + projectGoal: { + id: Goal["id"]; + title: Goal["title"]; + }; } export interface TaskResult { diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html new file mode 100644 index 00000000..a1358ec0 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html @@ -0,0 +1,31 @@ + + +
+ +
+ + @if (openPickColors) { +
+
+ @for (colorItem of tagColors; track $index) { +
+ } +
+
+ } +
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.scss new file mode 100644 index 00000000..d72879c1 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.scss @@ -0,0 +1,71 @@ +.create-tag { + &__form { + display: flex; + align-items: center; + gap: 5px; + } + + &__pick-color { + border-radius: var(--rounded-sm); + height: 9px; + width: 9px; + position: relative; + cursor: pointer; + } + + &__input { + padding: 3px 2px 2px 10px; + outline: none; + transition: all 0.2s; + + &::placeholder { + color: var(--dark-grey); + } + + &:focus { + border-color: var(--accent); + } + } + + &__colors { + position: absolute; + bottom: -25%; + right: -12%; + padding: 7px; + background-color: var(--light-white); + border-radius: var(--rounded-md); + border: 0.5px solid var(--medium-grey-for-outline); + + &-wrapper { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-template-rows: repeat(2, 1fr); + gap: 3px; + } + } +} + +.field { + &__option { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + gap: 5px; + transition: color 0.2s ease-in-out; + + &--add-object { + display: flex; + align-items: center; + justify-content: center; + width: 80px; + cursor: pointer; + border: 0.5px solid var(--accent); + border-radius: var(--rounded-xl); + + i { + color: var(--accent); + } + } + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts new file mode 100644 index 00000000..9409a5a8 --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts @@ -0,0 +1,61 @@ +/** @format */ + +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, inject, Output } from "@angular/core"; +import { tagColorsList } from "projects/core/src/consts/lists/tag-colots.list.const"; +import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; + +@Component({ + selector: "app-create-tag-form", + templateUrl: "./create-tag-form.component.html", + styleUrl: "./create-tag-form.component.scss", + imports: [CommonModule, FormsModule, ReactiveFormsModule], + standalone: true, +}) +export class CreateTagFormComponent { + @Output() createTag = new EventEmitter<{ name: string; color: string }>(); + + private readonly fb = inject(FormBuilder); + + constructor() { + this.tagForm = this.fb.group({ + tagName: [""], + tagColor: [tagColorsList[0].color], + }); + } + + tagForm: FormGroup; + openPickColors = false; + + get tagColors() { + return tagColorsList; + } + + get selectedColor(): string { + return this.tagForm.get("tagColor")?.value || tagColorsList[0].color; + } + + selectTagColor(color: string) { + this.tagForm.patchValue({ tagColor: color }); + this.openPickColors = false; + } + + confirmCreateTag(event: Event) { + event.stopPropagation(); + event.preventDefault(); + + const { tagName, tagColor } = this.tagForm.value; + + if (!tagName?.trim() || !tagColor) return; + + this.createTag.emit({ + name: tagName, + color: tagColor, + }); + + this.tagForm.reset({ + tagName: "", + tagColor: tagColorsList[0].color, + }); + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html index 93f54a17..8a82eba8 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.html @@ -1,9 +1,23 @@ -
+
@if (projectBoardInfo) {
- +
+ +
+ +
} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss index 70efd18d..93bd6b89 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.scss @@ -1,5 +1,6 @@ .kanban { &__sidebar { + position: relative; display: flex; flex-direction: column; gap: 15px; diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts index 39730f99..8323cf32 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts @@ -1,18 +1,61 @@ /** @format */ import { CommonModule } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { ChangeDetectorRef, Component, inject, Input, signal } from "@angular/core"; import { Project } from "@office/models/project.model"; import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { KanbanBoardActionsComponent } from "../actions/kanban-board-actions.component"; +import { DropdownComponent } from "@ui/components/dropdown/dropdown.component"; +import { ClickOutsideModule } from "ng-click-outside"; @Component({ selector: "app-kanban-board-sidebar", templateUrl: "./kanban-board-sidebar.component.html", styleUrl: "./kanban-board-sidebar.component.scss", - imports: [CommonModule, AvatarComponent, KanbanBoardActionsComponent], + imports: [ + CommonModule, + AvatarComponent, + KanbanBoardActionsComponent, + DropdownComponent, + ClickOutsideModule, + ], standalone: true, }) export class KanbanBoardSidebarComponent { @Input({ required: true }) projectBoardInfo!: Project; + + isContextMenuOpen = signal(false); + + onMouseDown(event: MouseEvent, projectBoardInfo: Project): void { + event.stopPropagation(); + + if (event.button === 2 || event.ctrlKey) { + event.preventDefault(); + this.isContextMenuOpen.set(true); + return; + } + + if (event.button === 0) { + this.navigateToDifferentBoard(projectBoardInfo); + } + } + + private navigateToDifferentBoard(projectBoardInfo: Project): void { + console.log(projectBoardInfo.id); + } + + get contextMenuOptions() { + return [ + { + id: 1, + label: "выгрузить", + value: "", + }, + { + id: 2, + label: "архив выполнено", + value: "", + }, + ]; + } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html new file mode 100644 index 00000000..e1d6829a --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html @@ -0,0 +1,448 @@ + + +
+
+ прикрепить результат +
+
+ + +
+ +
+
+ +
+ +
+ + +
+
+
+ +
+ @if (taskDetailForm.get("title")?.value; as title) { +

{{ title }}

+ } + +
+ +
+
+ +

Федор Е

+
+ +

12.10.2025 21:00

+
+ +
+ divider image +
+ +
+ @if (taskDetailForm.get("responsible"); as responsible) { +
+
+ +
ответственный
+
+ +
+ @if (responsible.value) { +
+ + + @if (showEditAvatarIcon) { + + } + + +
+ +

{{ responsible.value.name }}

+ } @else { +
+ + +
+ } +
+
+ } @if (taskDetailForm.get("performers"); as performers) { +
+
+ +
исполнители
+
+ +
+ @if (performers.value.length >= 0 && performers.value.length <= 3) { +
+ @for (performer of performers.value; track $index) { + + } +
+ } @if (!(performers.value.length === 3)) { +
+ + +
+ } +
+
+ } @if (taskDetailForm.get("startDate"); as startDate) { +
+
+ +

начало

+
+ +
+ @if (!showEditStartDatePicker) { +

+ {{ startDate.value | date: "dd.MM.yy" }} +

+ } @else { + + } + + {{ statusOfTask.text }} +
+
+ } @if (taskDetailForm.get("deadline"); as deadline) { +
+
+ +

дедлайн

+
+ +
+ @if (!showEditDeadlineDatePicker) { +

+ {{ deadline.value | date: "dd.MM.yy" }} +

+ } @else { + + } + + {{ + remainingDaysDeadline() + + " " + + (remainingDaysDeadline() | pluralize: ["день", "дня", "дней"]) + }} +
+
+ } +
+ +
+ @if (taskDetailForm.get("tags"); as tags) { +
+
+ +

тег

+
+ + @if (tags.value.length > 0) { +
+ @for (tag of tags.value; track $index) { + {{ "#" + tag.name }} + } +
+ } @else { +
+ + +
+ } +
+ } @if (taskDetailForm.get("goal"); as goal) { +
+
+ +

цель

+
+ + @if (goal.value) { + {{ + goal.value.name.length > 20 ? goal.value.name.slice(0, 15) + "..." : goal.value.name + }} + } @else { +
+ +
+ } + +
+ } @if (taskDetailForm.get("skills"); as skills) { +
+
+ +

навыки

+
+ + @if (skills.value.length > 0) { +
+ @for (skill of skills.value; track $index) { + {{ + skill.name + }} + } +
+ } @else { +
+ + + + + +
+ } +
+ } +
+ +
+ divider image +
+ + @if (taskDetailForm.get("description"); as description) { +
+
+ @if (isChangeDescriptionText() && description.value) { +

+ @if (descriptionExpandable) { +
+ {{ readFullDescription ? "cкрыть" : "подробнее" }} +
+ } } @else { + + } +
+
+ } + +
+ @if (taskDetailForm.get("files"); as files) { @if (files.value.length >= 0 && files.value.length + <= 3) { @for (file of files.value; track $index) { + + } @if (!(files.value.length === 3)) { +
+ + +

файл

+
+ } } } +
+ +
+ +
+
+ + +
+ + +
+ +
+
+ @for (f of filesList; track f.id) { + + } + +
+
+
+
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss new file mode 100644 index 00000000..0acc66ae --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss @@ -0,0 +1,395 @@ +@use "styles/typography"; + +.kanban { + &__detail { + position: absolute; + top: 0%; + right: 0%; + width: 100%; + max-width: 422px; + padding: 24px; + background-color: var(--light-white); + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + + &-top { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + + &-menu { + display: flex; + gap: 10px; + align-items: center; + } + } + + &-type, &-priority-wrapper, &-delete-wrapper { + cursor: pointer; + } + + &-priority { + width: 15px; + height: 15px; + background-color: var(--green); + border-radius: var(--rounded-xxl); + + } + + &-general { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + margin-bottom: 5px; + border-bottom: 0.5px solid var(--accent); + + p { + color: var(--accent) !important; + } + } + + p { color: var(--grey-for-text); } + + &-info { + display: flex; + gap: 10px; + align-items: flex-start; + margin-top: 15px; + + &-name { + display: flex; + gap: 3px; + align-items: center; + + ::ng-deep { + app-input { + + .field { + width: 44px !important; + height: 23px !important; + } + + .field__input { + border: 0; + background: transparent; + border-radius: 0; + padding: 0; + max-width: 44px; + + &:focus { + border-color: 0; + box-shadow: none; + } + + @include typography.body-10; + } + } + } + + &--date { + display: flex; + gap: 3px; + align-items: center; + + p, + i { + color: var(--green-dark) !important; + } + } + + h6 { + color: var(--accent); + } + + p { + color: var(--accent); + } + } + + &-list { + display: flex; + flex-direction: column; + gap: 4px; + } + } + + &-info-wrapper { + display: flex; + flex-direction: column; + gap: 5px; + } + + &-add-responsible { + padding: 8px; + cursor: pointer; + border: 0.5px dashed var(--accent); + border-radius: var(--rounded-xxl); + } + + &-avatar { + position: relative; + cursor: pointer; + + i { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--light-white) !important; + } + } + + &-performers { + display: flex; + gap: 4px; + align-items: center; + margin-left: 12px; + + app-avatar { + margin-left: -12px; + } + } + + &-add-object { + display: flex; + align-items: center; + align-self: center; + justify-content: center; + width: 100%; + padding: 2px 48px; + cursor: pointer; + border: 0.5px solid var(--accent); + border-radius: var(--rounded-xl); + + ::ng-deep { + app-tag { + .tag { + max-width: 100px; + } + } + } + } + + &-description { + margin: 12px 0px; + + ::ng-deep { + app-textarea { + .field__input { + min-height: 40px; + padding: 12px 12px 0; + margin-left: -10px; + background-color: transparent; + border: none; + border-radius: 0; + + &:focus { + border-color: none; + box-shadow: none; + } + } + } + } + } + + &-files { + display: flex; + gap: 10px; + align-items: center; + margin-bottom: 40px; + } + + &-file { + width: 116px; + + &--empty { + display: flex; + gap: 5px; + align-items: center; + } + } + + i { + color: var(--accent); + } + } + + &__divider { + width: 100%; + margin-top: 5px; + } +} + +.about { + /* stylelint-disable value-no-vendor-prefix */ + &__text { + p { + display: -webkit-box; + overflow: hidden; + color: var(--grey-for-text); + text-overflow: ellipsis; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + transition: all 0.7s ease-in-out; + + &.expanded { + -webkit-line-clamp: unset; + } + } + + ::ng-deep a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 3px; + transition: text-decoration-color 0.2s; + + &:hover { + text-decoration-color: var(--accent); + } + } + } + + /* stylelint-enable value-no-vendor-prefix */ + + &__read-full { + margin-top: 2px; + color: var(--accent); + cursor: pointer; + } +} + +.read-more { + margin-top: 12px; + color: var(--accent); + cursor: pointer; + transition: color 0.2s; + + &:hover { + color: var(--accent-dark); + } +} + +.form { + display: flex; + align-items: center; + justify-content: space-between; + width: 422px; + height: 28px; + padding: 10px 24px; + margin-bottom: -25px; + margin-left: -25px; + background-color: var(--light-white); + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + + &__row { + display: flex; + align-items: center; + width: 100%; + + ::ng-deep { + app-input { + .field__input { + width: 100%; + color: var(--grey-for-text) !important; + background-color: transparent; + } + } + } + } + + &__files { + display: flex; + flex-direction: column; + gap: 10px; + + &:not(:empty) { + margin-top: 20px; + } + } + + &__actions { + display: flex; + gap: 5px; + align-items: center; + + i { + color: var(--light-white); + } + + input { + display: none; + } + } + + &__attach { + i { + color: var(--dark-grey); + } + } + + &__send { + width: 15px; + height: 15px; + padding: 4px; + color: var(--accent); + cursor: pointer; + background-color: var(--accent); + border-radius: var(--rounded-sm) + } + + &__input { + flex-grow: 1; + resize: none; + background: transparent; + border: none; + outline: none; + } +} + +.modal { + &__wrapper { + display: flex; + flex-direction: column; + gap: 20px; + align-items: center; + width: 672px; + } + + &__content { + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; + max-width: 536px; + height: 480px; + background-color: var(--white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: 8px; + box-shadow: 5px 5px 25px 0 var(--gray-for-shadow); + } + + &__specs-groups, + &__skills-groups { + height: 100%; + overflow: auto; + scrollbar-width: thin; + + ul { + display: flex; + flex-direction: column; + gap: 20px; + padding: 14px; + } + + li { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + } + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts new file mode 100644 index 00000000..8448506c --- /dev/null +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts @@ -0,0 +1,432 @@ +/** @format */ + +import { CommonModule, DatePipe } from "@angular/common"; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + computed, + ElementRef, + inject, + Input, + OnInit, + signal, + ViewChild, +} from "@angular/core"; +import { FileUploadItemComponent } from "@ui/components/file-upload-item/file-upload-item.component"; +import { InputComponent, ButtonComponent } from "@ui/components"; +import { FileItemComponent } from "@ui/components/file-item/file-item.component"; +import { TextareaComponent } from "@ui/components/textarea/textarea.component"; +import { TagComponent } from "@ui/components/tag/tag.component"; +import { BadgeComponent } from "@ui/components/badge/badge.component"; +import { AvatarComponent } from "@ui/components/avatar/avatar.component"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { ControlErrorPipe, ParseBreaksPipe, ParseLinksPipe, PluralizePipe } from "@corelib"; +import { expandElement } from "@utils/expand-element"; +import { IconComponent } from "@uilib"; +import { nanoid } from "nanoid"; +import { DropdownComponent } from "@ui/components/dropdown/dropdown.component"; +import { priorityInfoList } from "projects/core/src/consts/lists/priority-info-list.const"; +import { ClickOutsideModule } from "ng-click-outside"; +import { actionTypeList } from "projects/core/src/consts/lists/actiion-type-list.const"; +import { Project } from "@office/models/project.model"; +import { SkillsGroupComponent } from "@office/shared/skills-group/skills-group.component"; +import { ModalComponent } from "@ui/components/modal/modal.component"; +import { SkillsService } from "@office/services/skills.service"; +import { Skill } from "@office/models/skill"; +import { Subscription } from "rxjs"; +import { TaskDetail } from "../../../models/task.model"; +import { daysUntil } from "@utils/days-untit"; +import { KanbanBoardService } from "../../../kanban-board.service"; + +@Component({ + selector: "app-task-detail", + templateUrl: "./task-detail.component.html", + styleUrl: "./task-detail.component.scss", + imports: [ + CommonModule, + ReactiveFormsModule, + FileUploadItemComponent, + InputComponent, + IconComponent, + FileItemComponent, + TextareaComponent, + TagComponent, + BadgeComponent, + AvatarComponent, + ButtonComponent, + ParseLinksPipe, + ParseBreaksPipe, + DatePipe, + DropdownComponent, + ClickOutsideModule, + SkillsGroupComponent, + ModalComponent, + ControlErrorPipe, + PluralizePipe, + ], + standalone: true, +}) +export class TaskDetailComponent implements OnInit, AfterViewInit { + @Input() collaborators?: Project["collaborators"]; + @Input() goals?: Project["goals"]; + + @ViewChild("descEl") descEl?: ElementRef; + + private readonly kanbanBoardService = inject(KanbanBoardService); + private readonly skillsService = inject(SkillsService); + private readonly cdRef = inject(ChangeDetectorRef); + + constructor(private readonly fb: FormBuilder) { + this.taskDetailForm = this.fb.group({ + title: ["", Validators.required], + responsible: [], + performers: this.fb.array([]), + startDate: [null], + deadline: [null], + tags: this.fb.array([]), + goal: [null], + skills: this.fb.array([]), + description: [null], + files: this.fb.array([]), + priority: [null], + action: [null], + }); + } + + isChangeDescriptionText = signal(false); + + remainingDaysDeadline = signal(0); + + /** Уникальный ID для элемента input */ + controlId = nanoid(3); + + descriptionExpandable!: boolean; // Флаг необходимости кнопки "подробнее" + readFullDescription = false; // Флаг показа всех вакансий + + isActionTypeOpen = false; + isPriorityTypeOpen = false; + isDeleteTypeOpen = false; + isResponsiblePickOpen = false; + isPerformersPickOpen = false; + isGoalPickOpen = false; + isTagsPickOpen = false; + + showEditAvatarIcon = false; + showEditDeadlineDatePicker = false; + showEditStartDatePicker = false; + + skillsGroupsModalOpen = signal(false); + nestedSkills$ = this.skillsService.getSkillsNested(); + openGroupIds = new Set(); + + filesList: any[] = []; + + taskDetailForm: FormGroup; + + subscriptions: Subscription[] = []; + + get actionTypeOptions() { + return actionTypeList; + } + + get priorityTypeOptions() { + return priorityInfoList + .map(priority => ({ + id: priority.id, + label: priority.label, + value: priority.priorityType, + additionalInfo: priority.priorityType.toString(), + })) + .reverse(); + } + + get responsiblePickOpenOptions() { + return this.collaborators + ? this.collaborators.map(collaborator => ({ + id: collaborator.userId, + label: collaborator.firstName + " " + collaborator.lastName[0], + value: collaborator.userId, + additionalInfo: collaborator.avatar, + })) + : []; + } + + get goalPickOptions() { + return this.goals + ? this.goals.map((goal, index) => ({ + id: index, + label: goal.title, + value: goal.id, + additionalInfo: goal.title, + })) + : []; + } + + get tagsPickOptions() { + return [ + { + id: 1, + label: "аналитика", + value: "аналитика", + additionalInfo: "primary", + }, + { + id: 2, + label: "продажи", + value: "продажи", + additionalInfo: "complete", + }, + ]; + } + + get priorityDeleteOptions() { + return [ + { + id: 1, + label: "удалить задачу", + value: "delete", + }, + ]; + } + + get hasOpenSkillsGroups(): boolean { + return this.openGroupIds.size > 0; + } + + get statusOfTask() { + const start = new Date(this.taskDetailForm.value.startDate); + const deadline = new Date(this.taskDetailForm.value.deadline); + + const status = this.getTaskStatus(start, deadline); + + let days: number | null = null; + + if (status === "ожидание") { + days = daysUntil(start); + } else if (status === "началась") { + days = daysUntil(deadline); + } + + const color = status === "закончена" ? "red" : this.getColorByDays(days!); + + return { + text: status, + color, + }; + } + + ngOnInit(): void {} + + /** + * Проверка возможности расширения описания после инициализации представления + */ + ngAfterViewInit(): void { + const todayDate = new Date(); + const tomorrowDate = new Date(todayDate); + tomorrowDate.setDate(todayDate.getDate() + 1); + + this.initializeTaskDetailInfo(todayDate, tomorrowDate); + this.checkDescriptionHeigth(); + } + + onDeleteTaskGoal(): void {} + + onDeleteTaskTag(): void {} + + onEditTaskTag(): void {} + + getCreatedTagInfo(createdTagInfo: { name: string; color: string }): void { + console.log(createdTagInfo); + } + + toggleDropdown( + type: "action" | "priority" | "responsible" | "performers" | "goal" | "tags" | "delete", + state: boolean + ) { + switch (type) { + case "action": + this.isActionTypeOpen = state; + break; + + case "priority": + this.isPriorityTypeOpen = state; + break; + + case "responsible": + this.isResponsiblePickOpen = state; + break; + + case "performers": + this.isPerformersPickOpen = state; + break; + + case "goal": + this.isGoalPickOpen = state; + break; + + case "tags": + this.isTagsPickOpen = state; + break; + + case "delete": + this.isDeleteTypeOpen = state; + break; + + default: + break; + } + } + + onTypeSelect( + typeId: number, + type: "action" | "priority" | "responsible" | "performers" | "goal" | "tags" | "delete" + ): void { + this.toggleDropdown(type, false); + console.log(typeId); + } + + onChangeText(event: MouseEvent): void { + event.stopPropagation(); + this.isChangeDescriptionText.set(!this.isChangeDescriptionText()); + + setTimeout(() => this.checkDescriptionHeigth(), 0); + } + + /** + * Раскрытие/сворачивание описания профиля + * @param elem - DOM элемент описания + * @param expandedClass - CSS класс для раскрытого состояния + * @param isExpanded - текущее состояние (раскрыто/свернуто) + */ + onExpandDescription(elem: HTMLElement, expandedClass: string, isExpanded: boolean): void { + expandElement(elem, expandedClass, isExpanded); + this.readFullDescription = !isExpanded; + } + + /** Обработчик загрузки файла */ + onUpdate(event: Event): void { + const files = (event.currentTarget as HTMLInputElement).files; + if (!files?.length) { + return; + } + + console.log("123"); + + // this.loading = true; + + // this.fileService.uploadFile(files[0]).subscribe(res => { + // this.loading = false; + + // this.value = res.url; + // this.onChange(res.url); + // }); + } + + /** + * Переключение навыка в списке выбранных + * @param toggledSkill - навык для переключения + */ + onToggleSkill(toggledSkill: Skill): void { + const { skills }: { skills: Skill[] } = this.taskDetailForm.value; + const isPresent = skills.some(skill => skill.id === toggledSkill.id); + + if (isPresent) { + this.onRemoveSkill(toggledSkill); + } else { + this.onAddSkill(toggledSkill); + } + } + + onGroupToggled(isOpen: boolean, skillsGroupId: number): void { + this.openGroupIds.clear(); + if (isOpen) { + this.openGroupIds.add(skillsGroupId); + } + + this.cdRef.markForCheck(); + } + + /** + * Переключение модального окна групп навыков + */ + toggleSkillsGroupsModal(): void { + this.skillsGroupsModalOpen.update(open => !open); + } + + /** + * Добавление навыка + * @param newSkill - новый навык + */ + private onAddSkill(newSkill: Skill): void { + const { skills }: { skills: Skill[] } = this.taskDetailForm.value; + const isPresent = skills.some(skill => skill.id === newSkill.id); + + if (isPresent) return; + + this.taskDetailForm.patchValue({ skills: [newSkill, ...skills] }); + } + + /** + * Удаление навыка + * @param oddSkill - навык для удаления + */ + private onRemoveSkill(oddSkill: Skill): void { + const { skills }: { skills: Skill[] } = this.taskDetailForm.value; + + this.taskDetailForm.patchValue({ + skills: skills.filter(skill => skill.id !== oddSkill.id), + }); + } + + private checkDescriptionHeigth(): void { + const descElement = this.descEl?.nativeElement; + this.descriptionExpandable = descElement?.clientHeight < descElement?.scrollHeight; + + this.cdRef.detectChanges(); + } + + private initializeTaskDetailInfo(todayDate: Date, tommorowDate: Date): void { + const taskDetailInfo$ = this.kanbanBoardService.getTaskById(123).subscribe({ + next: (taskDetailInfo: TaskDetail) => { + this.taskDetailForm.patchValue({ + title: taskDetailInfo.title ?? "Настроить процессы", + responsible: taskDetailInfo.responsible ?? null, + performers: taskDetailInfo.performers ?? [], + startDate: taskDetailInfo.dateTaskStart ?? todayDate, + deadline: taskDetailInfo.deadlineDate + ? new Date(taskDetailInfo.deadlineDate) + : tommorowDate, + tags: taskDetailInfo.tags ?? [], + goal: taskDetailInfo.goal ?? null, + skills: taskDetailInfo.requiredSkills ?? [], + description: taskDetailInfo.description ?? null, + files: taskDetailInfo.files ?? [], + }); + this.remainingDaysDeadline.set( + daysUntil( + taskDetailInfo.deadlineDate + ? new Date(taskDetailInfo.deadlineDate) + : new Date(tommorowDate) + ) + ); + }, + }); + + this.subscriptions.push(taskDetailInfo$); + } + + private getColorByDays(days: number): "red" | "gold" | "green" { + if (days <= 3) return "red"; + if (days <= 7) return "gold"; + return "green"; + } + + private getTaskStatus(start: Date, deadline: Date) { + const now = new Date(); + + if (now < start) return "ожидание"; + if (now > deadline) return "закончена"; + return "началась"; + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts index e18cecef..d7136c98 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/kanban-task.component.ts @@ -5,36 +5,18 @@ import { Component, Input } from "@angular/core"; import { IconComponent } from "@uilib"; import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { TagComponent } from "@ui/components/tag/tag.component"; -import { hexToRgba } from "@utils/helpers/hexToRgba"; -import { priorityInfoList } from "projects/core/src/consts/lists/priority-info-list.const"; -import { RouterLink } from "@angular/router"; +import { RouterModule } from "@angular/router"; +import { getPriorityType } from "@utils/helpers/getPriorityType"; @Component({ selector: "app-kanban-task", templateUrl: "./kanban-task.component.html", styleUrl: "./kanban-task.component.scss", - imports: [CommonModule, IconComponent, AvatarComponent, TagComponent, RouterLink], + imports: [CommonModule, RouterModule, IconComponent, AvatarComponent, TagComponent], standalone: true, }) export class KanbanTaskComponent { @Input({ required: true }) task: any; - private readonly priorityInfoList = priorityInfoList; - private readonly hexToRgba = hexToRgba; - - getPriorityType(priorityId: number, type: "background" | "color", opacity = 0.25) { - const findedPriority = this.priorityInfoList.find( - priority => priority.priorityType === priorityId - ); - const baseColor = findedPriority?.color ?? "var(--light-white)"; - - if (!findedPriority) return; - - if (type === "color") { - return { "background-color": baseColor }; - } - - const rgbaColor = this.hexToRgba(baseColor, opacity); - return { "background-color": rgbaColor }; - } + getPriorityType = getPriorityType; } diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts index d500e168..7e065b4e 100644 --- a/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts +++ b/projects/social_platform/src/app/office/projects/edit/services/project-form.service.ts @@ -13,7 +13,7 @@ import { ActivatedRoute } from "@angular/router"; import { PartnerProgramFields } from "@office/models/partner-program-fields.model"; import { Project } from "@office/models/project.model"; import { ProjectService } from "@office/services/project.service"; -import { stripNullish } from "@utils/stripNull"; +import { stripNullish } from "@utils/helpers/stripNull"; import { concatMap, filter } from "rxjs"; /** * Сервис для управления основной формой проекта и формой дополнительных полей партнерской программы. diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-resources.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-resources.service.ts index 15b66153..cc9e17c9 100644 --- a/projects/social_platform/src/app/office/projects/edit/services/project-resources.service.ts +++ b/projects/social_platform/src/app/office/projects/edit/services/project-resources.service.ts @@ -233,7 +233,6 @@ export class ProjectResourceService { public editResources(projectId: number) { const resources = this.getResourcesData(); - console.log(resources); const requests = resources.map(resource => { const payload: Omit = { diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts index 997a65f5..26782b1e 100644 --- a/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts +++ b/projects/social_platform/src/app/office/projects/edit/services/project-vacancy.service.ts @@ -7,7 +7,7 @@ import { ValidationService } from "@corelib"; import { Skill } from "@office/models/skill"; import { Vacancy } from "@office/models/vacancy.model"; import { VacancyService } from "@office/services/vacancy.service"; -import { stripNullish } from "@utils/stripNull"; +import { stripNullish } from "@utils/helpers/stripNull"; import { rolesMembersList } from "projects/core/src/consts/lists/roles-members-list.const"; import { ProjectFormService } from "./project-form.service"; import { workExperienceList } from "projects/core/src/consts/lists/work-experience-list.const"; diff --git a/projects/social_platform/src/app/office/projects/list/list.component.ts b/projects/social_platform/src/app/office/projects/list/list.component.ts index 3cba85c1..02987d18 100644 --- a/projects/social_platform/src/app/office/projects/list/list.component.ts +++ b/projects/social_platform/src/app/office/projects/list/list.component.ts @@ -34,7 +34,7 @@ import { ApiPagination } from "@models/api-pagination.model"; import { InfoCardComponent } from "../../features/info-card/info-card.component"; import { IconComponent } from "@ui/components"; import { SubscriptionService } from "@office/services/subscription.service"; -import { inviteToProjectMapper } from "@utils/inviteToProjectMapper"; +import { inviteToProjectMapper } from "@utils/helpers/inviteToProjectMapper"; /** * КОМПОНЕНТ СПИСКА ПРОЕКТОВ diff --git a/projects/social_platform/src/app/office/projects/projects.component.ts b/projects/social_platform/src/app/office/projects/projects.component.ts index 695b5323..d98f3d45 100644 --- a/projects/social_platform/src/app/office/projects/projects.component.ts +++ b/projects/social_platform/src/app/office/projects/projects.component.ts @@ -12,7 +12,7 @@ import { BackComponent } from "@uilib"; import { SoonCardComponent } from "@office/shared/soon-card/soon-card.component"; import { ProjectsFilterComponent } from "./projects-filter/projects-filter.component"; import { Project } from "@office/models/project.model"; -import { inviteToProjectMapper } from "@utils/inviteToProjectMapper"; +import { inviteToProjectMapper } from "@utils/helpers/inviteToProjectMapper"; import { ProjectsService } from "./services/projects.service"; import { InfoCardComponent } from "@office/features/info-card/info-card.component"; diff --git a/projects/social_platform/src/app/office/services/chat.service.ts b/projects/social_platform/src/app/office/services/chat.service.ts index e7d81d30..dee4ed65 100644 --- a/projects/social_platform/src/app/office/services/chat.service.ts +++ b/projects/social_platform/src/app/office/services/chat.service.ts @@ -57,7 +57,7 @@ export class ChatService { const tokens = this.tokenService.getTokens(); if (!tokens) throw new Error("No token provided"); - return this.websocketService.connect(`/chat/?token=${tokens.access}`); + return this.websocketService.connect(`/chat/`); } /** diff --git a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html new file mode 100644 index 00000000..95c1b26a --- /dev/null +++ b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html @@ -0,0 +1,61 @@ + + +
+ +
+ + +
    + @for (option of options; track option.id; let index = $index) { +
  • +
    + @if (option.additionalInfo) { @switch (type) { @case('icons') { + + + } @case ('avatars') { + + + } @case ('shapes') { +
    + + } @case ('goals') { + {{ + option.label.length > 20 ? option.label.slice(0, 17) + "..." : option.label + }} + } @case ('tags') { + #{{ + option.label.length > 15 ? option.label.slice(0, 12) + "..." : option.label + }} + } } } +
    + + @if (type !== 'tags' && type !== 'goals') { +

    {{ option.label }}

    + } + + +
  • + } @if (type === 'tags') { @if (!creatingTag) { +
  • + +
  • + } @else { + + } } +
+
diff --git a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.scss b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.scss new file mode 100644 index 00000000..61b38c8a --- /dev/null +++ b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.scss @@ -0,0 +1,74 @@ +.field { + &__options { + width: 100%; + max-height: 200px; + padding: 12px; + overflow-y: auto; + background-color: var(--light-white); + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + z-index: 1000; + display: flex; + flex-direction: column; + gap: 5px; + align-items: flex-start; + } + + &__option { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + gap: 5px; + transition: color 0.2s ease-in-out; + + &--add-object { + display: flex; + align-items: center; + justify-content: center; + width: 80px; + padding: 3px; + cursor: pointer; + border: 0.5px solid var(--accent); + border-radius: var(--rounded-xl); + + i { + color: var(--accent); + } + } + + &:hover { + p { + color: var(--accent); + } + } + + &--additional { + display: flex; + align-items: center; + gap: 5px; + + i { + color: var(--accent); + } + + app-tag { + width: 80px; + } + } + + &-priority { + width: 15px; + height: 15px; + border-radius: var(--rounded-xxl); + } + + &--highlighted { + background-color: var(--light-gray); + } + + &--point { + color: var(--accent); + } + } +} diff --git a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts new file mode 100644 index 00000000..1e329234 --- /dev/null +++ b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts @@ -0,0 +1,108 @@ +/** @format */ + +import { ConnectedPosition, OverlayModule } from "@angular/cdk/overlay"; +import { CommonModule } from "@angular/common"; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from "@angular/core"; +import { AvatarComponent } from "../avatar/avatar.component"; +import { IconComponent } from "@uilib"; +import { getPriorityType } from "@utils/helpers/getPriorityType"; +import { TagComponent } from "../tag/tag.component"; +import { ClickOutsideModule } from "ng-click-outside"; +import { CreateTagFormComponent } from "@office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component"; + +@Component({ + selector: "app-dropdown", + standalone: true, + imports: [ + CommonModule, + OverlayModule, + AvatarComponent, + IconComponent, + TagComponent, + ClickOutsideModule, + CreateTagFormComponent, + ], + templateUrl: "./dropdown.component.html", + styleUrl: "./dropdown.component.scss", +}) +export class DropdownComponent { + /** Состояние для определения списка элементов */ + @Input() options: { id: number; label: string; value: any; additionalInfo?: any }[] = []; + + @Input() type: "icons" | "avatars" | "shapes" | "tags" | "goals" | "text" = "text"; + + /** Состояние для открытия списка выпадающего */ + @Input() isOpen = false; + + /** Состояние для выделения элемента списка выпадающего */ + @Input() highlightedIndex = -1; + + @Input() colorText: "grey" | "black" | "red" = "grey"; + + /** Событие для выбора элемента */ + @Output() select = new EventEmitter(); + + /** Событие для логики при клике вне списка выпадающего */ + @Output() outside = new EventEmitter(); + + @Output() tagInfo = new EventEmitter<{ name: string; color: string }>(); + + @ViewChild("dropdown", { static: true }) dropdown!: ElementRef; + + /** режим создания тега */ + creatingTag = false; + + getPriorityType = getPriorityType; + + positions: ConnectedPosition[] = [ + { + originX: "start", + originY: "bottom", + overlayX: "start", + overlayY: "top", + offsetY: 4, + }, + { + originX: "start", + originY: "top", + overlayX: "start", + overlayY: "bottom", + offsetY: -4, + }, + ]; + + /** Метод для выбора элемента и emit для родительского компонента */ + onSelect(event: Event, id: number) { + event.stopPropagation(); + this.select.emit(id); + } + + /** Метод для клика вне списка выпадающего */ + onClickOutside() { + this.outside.emit(); + } + + startCreatingTag(event: Event) { + event.stopPropagation(); + this.creatingTag = true; + } + + onConfirmCreateTag(tagInfo: { name: string; color: string }): void { + this.tagInfo.emit(tagInfo); + this.creatingTag = false; + this.isOpen = false; + } + + getTextColor(colorText: "grey" | "black" | "red") { + switch (colorText) { + case "red": + return "color: var(--red)"; + + case "black": + return "color: var(--black)"; + + case "grey": + return "color: var(--grey-for-text)"; + } + } +} diff --git a/projects/social_platform/src/app/ui/components/input/input.component.html b/projects/social_platform/src/app/ui/components/input/input.component.html index 0edc178d..44d83b38 100644 --- a/projects/social_platform/src/app/ui/components/input/input.component.html +++ b/projects/social_platform/src/app/ui/components/input/input.component.html @@ -4,6 +4,7 @@
+ @if (type === 'radio') { + } @else if (type === 'date') { + + } @else {
} +
(); - - /** Событие нажатия Enter */ @Output() enter = new EventEmitter(); - - /** Событие изменения состояния радио (для внешних обработчиков) */ @Output() change = new EventEmitter(); - /** Обработчик для радио */ + /** Обработчик для radвариант io */ onRadioChange(event: Event): void { if (this.type === "radio") { const target = event.target as HTMLInputElement; this.value = target.value; this.onChange(this.value); this.appValueChange.emit(this.value); - this.change.emit(event); // Эмитим событие для внешних обработчиков + this.change.emit(event); + this.onTouch(); + } + } + + /** Обработчик изменения даты в datepicker */ + onDateChange(event: any): void { + if (this.type === "date" && event.value) { + const date = event.value as Date; + // Форматируем дату в нужный формат (например, DD.MM.YY) + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = String(date.getFullYear()).slice(-2); + const formattedDate = `${day}.${month}.${year}`; + + this.value = formattedDate; + this.onChange(formattedDate); + this.appValueChange.emit(formattedDate); this.onTouch(); } } - /** Показать подсказку */ showTooltip(): void { this.isTooltipVisible = true; } - /** Скрыть подсказку */ hideTooltip(): void { this.isTooltipVisible = false; } - /** Обработчик ввода текста */ onInput(event: Event): void { const value = (event.target as HTMLInputElement).value; this.onChange(value); this.appValueChange.emit(value); } - /** Обработчик потери фокуса */ onBlur(): void { this.onTouch(); } - /** Текущее значение поля */ value = ""; - // Методы ControlValueAccessor + // Геттер для преобразования строковой даты в объект Date для datepicker + get dateValue(): Date | null { + if (!this.value || this.type !== "date") return null; + + // Парсим дату из формата DD.MM.YY и преобразуем дату + const parts = this.value.split("."); + if (parts.length === 3) { + const day = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10) - 1; + const year = parseInt(parts[2], 10) + 2000; + return new Date(year, month, day); + } + + return null; + } + writeValue(value: string): void { setTimeout(() => { this.value = value; @@ -170,14 +148,12 @@ export class InputComponent implements ControlValueAccessor { this.onTouch = fn; } - /** Состояние блокировки */ disabled = false; setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } - /** Обработчик нажатия Enter */ onEnter(event: Event) { event.preventDefault(); this.enter.emit(); diff --git a/projects/social_platform/src/app/ui/components/modal/modal.component.html b/projects/social_platform/src/app/ui/components/modal/modal.component.html index 27a13b45..1e8b0690 100644 --- a/projects/social_platform/src/app/ui/components/modal/modal.component.html +++ b/projects/social_platform/src/app/ui/components/modal/modal.component.html @@ -1,7 +1,7 @@ - + +
+ + + @if (isColumnInfoOpen && (selectedColumnId === boardColumn.order)) { + + } +
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts index 5b924209..0e4f7ee1 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/kanban-board.component.ts @@ -11,6 +11,8 @@ import { IconComponent } from "@ui/components"; import { KanbanTaskComponent } from "./shared/task/kanban-task.component"; import { ClickOutsideModule } from "ng-click-outside"; import { TaskDetailComponent } from "./shared/task/detail/task-detail.component"; +import { DropdownComponent } from "@ui/components/dropdown/dropdown.component"; +import { kanbanColumnInfo } from "projects/core/src/consts/other/kanban-column-info.const"; @Component({ selector: "app-kanban-board", @@ -23,6 +25,7 @@ import { TaskDetailComponent } from "./shared/task/detail/task-detail.component" KanbanTaskComponent, ClickOutsideModule, TaskDetailComponent, + DropdownComponent, ], standalone: true, }) @@ -36,6 +39,13 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { boardColumns = signal([]); isTaskDetailOpen = signal(false); + isColumnInfoOpen = false; + selectedColumnId = 0; + + get columnInfoOptions() { + return kanbanColumnInfo; + } + ngOnInit(): void { const detailInfoUrl$ = this.route.queryParams.subscribe({ next: params => { @@ -61,6 +71,7 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { { id: 0, name: "бэклог", + order: 0, locked: true, tasks: [ { @@ -76,6 +87,7 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { { id: 1, locked: false, + order: 1, name: "в работе", tasks: [ { id: 3, title: "настроить API" }, @@ -85,6 +97,7 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { { id: 2, locked: false, + order: 2, name: "Готово", tasks: [ { id: 5, title: "верстка страницы входа" }, @@ -100,6 +113,34 @@ export class KanbanBoardComponent implements OnInit, OnDestroy { this.subscriptions.forEach($ => $.unsubscribe()); } + toggleDropDown(columnId: number): void { + if (this.selectedColumnId === columnId) { + this.isColumnInfoOpen = !this.isColumnInfoOpen; + } else { + this.selectedColumnId = columnId; + this.isColumnInfoOpen = true; + } + } + + onTypeSelect(option: any, state: boolean, columnId?: number): void { + if (!option) { + this.isColumnInfoOpen = state; + return; + } + + switch (option) { + case 1: + console.log("EDIT in column:", columnId); + break; + + case 2: + console.log("DELETE in column:", columnId); + break; + } + + this.isColumnInfoOpen = state; + } + openDetailTask(taskId: number): void { this.router.navigate([], { queryParams: { diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts index a97ace85..240cac32 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/models/column.model.ts @@ -5,4 +5,6 @@ import { TaskPreview } from "./task.model"; export interface Column { id: number; tasks: TaskPreview[]; + order: number; + // TODO: добавить даты создания и удаления } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html index a1358ec0..b2c15dc5 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.html @@ -21,7 +21,7 @@ @for (colorItem of tagColors; track $index) {
} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts index 9409a5a8..43ddb6b1 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component.ts @@ -1,9 +1,30 @@ /** @format */ import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, inject, Output } from "@angular/core"; +import { + Component, + EventEmitter, + inject, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from "@angular/core"; import { tagColorsList } from "projects/core/src/consts/lists/tag-colots.list.const"; -import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { + FormBuilder, + FormGroup, + FormsModule, + ReactiveFormsModule, + Validators, +} from "@angular/forms"; + +export interface TagData { + id?: number; + name: string; + color: string; +} @Component({ selector: "app-create-tag-form", @@ -12,15 +33,17 @@ import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from "@angul imports: [CommonModule, FormsModule, ReactiveFormsModule], standalone: true, }) -export class CreateTagFormComponent { - @Output() createTag = new EventEmitter<{ name: string; color: string }>(); +export class CreateTagFormComponent implements OnInit, OnChanges { + @Input() editingTag: TagData | null = null; + @Output() createTag = new EventEmitter(); + @Output() updateTag = new EventEmitter(); private readonly fb = inject(FormBuilder); constructor() { this.tagForm = this.fb.group({ - tagName: [""], - tagColor: [tagColorsList[0].color], + tagName: ["", [Validators.required, Validators.maxLength(30)]], + tagColor: [tagColorsList[0].name, [Validators.required]], }); } @@ -32,11 +55,27 @@ export class CreateTagFormComponent { } get selectedColor(): string { - return this.tagForm.get("tagColor")?.value || tagColorsList[0].color; + const colorName = this.tagForm.get("tagColor")?.value; + const found = tagColorsList.find(c => c.name === colorName); + return found ? found.color : tagColorsList[0].color; + } + + get isEditMode(): boolean { + return !!this.editingTag; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes["editingTag"]) { + this.initFormFromEditingTag(); + } + } + + ngOnInit(): void { + this.initFormFromEditingTag(); } - selectTagColor(color: string) { - this.tagForm.patchValue({ tagColor: color }); + selectTagColor(colorName: string) { + this.tagForm.patchValue({ tagColor: colorName }); this.openPickColors = false; } @@ -45,17 +84,36 @@ export class CreateTagFormComponent { event.preventDefault(); const { tagName, tagColor } = this.tagForm.value; + const tagData: TagData = { + name: tagName, + color: tagColor, + }; if (!tagName?.trim() || !tagColor) return; - this.createTag.emit({ - name: tagName, - color: tagColor, - }); + if (this.isEditMode && this.editingTag?.id) { + this.updateTag.emit({ ...tagData, id: this.editingTag.id }); + } else { + this.createTag.emit(tagData); + } + this.resetForm(); + } + private resetForm(): void { this.tagForm.reset({ tagName: "", - tagColor: tagColorsList[0].color, + tagColor: tagColorsList[0].name, }); } + + private initFormFromEditingTag() { + if (this.editingTag) { + this.tagForm.patchValue({ + tagName: this.editingTag.name, + tagColor: this.editingTag.color, + }); + } else { + this.resetForm(); + } + } } diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts index 8323cf32..4ffd7a86 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/sidebar/kanban-board-sidebar.component.ts @@ -1,7 +1,7 @@ /** @format */ import { CommonModule } from "@angular/common"; -import { ChangeDetectorRef, Component, inject, Input, signal } from "@angular/core"; +import { Component, Input, signal } from "@angular/core"; import { Project } from "@office/models/project.model"; import { AvatarComponent } from "@ui/components/avatar/avatar.component"; import { KanbanBoardActionsComponent } from "../actions/kanban-board-actions.component"; diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html index e1d6829a..7139513a 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.html @@ -4,50 +4,53 @@
прикрепить результат
+ @if (taskDetailForm.get("action"); as action) {
- +
- + } @if (taskDetailForm.get("priority"); as priority) {
-
+
+ }
@@ -57,8 +60,12 @@
@if (taskDetailForm.get("title")?.value; as title) {

{{ title }}

+ } @if (taskDetailForm.get("score")?.value; as score) { +
+ +

{{ score }}

+
} -
@@ -100,7 +107,7 @@
ответственный
appIcon icon="edit-pen" appSquare="8" - (click)="toggleDropdown('responsible', !isResponsiblePickOpen)" + (click)="onTypeSelect('responsible', !isResponsiblePickOpen)" > } @@ -108,24 +115,24 @@
ответственный
[isOpen]="isResponsiblePickOpen" type="avatars" [options]="responsiblePickOpenOptions" - (select)="onTypeSelect($event, 'responsible')" + (select)="onTypeSelect('responsible', false, $event)" (outside)="isResponsiblePickOpen = false" >
-

{{ responsible.value.name }}

+

{{ responsible.value.name }}

} @else {
@@ -140,24 +147,24 @@
исполнители
- @if (performers.value.length >= 0 && performers.value.length <= 3) { + @if (performers.value && performers.value.length >= 0 && performers.value.length <= 7) {
@for (performer of performers.value; track $index) { }
- } @if (!(performers.value.length === 3)) { + } @if (!(performers.value && performers.value.length === 7)) {
@@ -241,26 +248,29 @@
исполнители
@if (tags.value.length > 0) {
- @for (tag of tags.value; track $index) { + @for (tag of tags.value; track tag.id) { {{ "#" + tag.name }}{{ "#" + tag.title }} }
- } @else { -
+ } @if (tags.value.length < 3) { +
@@ -278,21 +288,48 @@
исполнители
color="soft" [canDelete]="true" (delete)="onDeleteTaskGoal()" - (click)="isGoalPickOpen = !isGoalPickOpen" + (click)=" + $event.stopPropagation(); + showChangeGoalModal = true; + onTypeSelect('goal', !isGoalPickOpen) + " >{{ - goal.value.name.length > 20 ? goal.value.name.slice(0, 15) + "..." : goal.value.name + goal.value.title.length > 20 ? goal.value.title.slice(0, 15) + "..." : goal.value.title }} + + +
+

вы хотите изменить цель задачи?

+ +
+ отмена + изменить +
+
+
+ } @else { -
+
}
@@ -305,51 +342,55 @@
исполнители
@if (skills.value.length > 0) {
- @for (skill of skills.value; track $index) { - {{ - skill.name - }} + @for (skill of skills.value; track skill.id) { + {{ skill.name.length > 20 ? skill.name.slice(0, 17) + "..." : skill.name }} }
- } @else { + } @if (skills.value.length < 3) {
- - - -
}
}
+ + + +
divider image
@@ -357,7 +398,7 @@
исполнители
@if (taskDetailForm.get("description"); as description) {
- @if (isChangeDescriptionText() && description.value) { + @if (isChangeDescriptionText()) {

@if (descriptionExpandable) {
исполнители
} } @else { исполнители }
- @if (taskDetailForm.get("files"); as files) { @if (files.value.length >= 0 && files.value.length - <= 3) { @for (file of files.value; track $index) { + @if (taskDetailForm.get("files")?.value; as files) { @if (files.length > 0) { @for (file of + files; track $index) { - } @if (!(files.value.length === 3)) { + } } @if (files.length < 3) {

файл

- } } } + } }
-
- -
+
- +
-
@for (f of filesList; track f.id) { @@ -439,10 +481,10 @@
исполнители
[size]="f.tempFile.size" [loading]="f.loading" [error]="f.error" + (delete)="onDeleteFile(f.id)" > } -
-
+
-->
diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss index 5e513e0d..ed92bcce 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.scss @@ -46,6 +46,12 @@ margin-bottom: 5px; border-bottom: 0.5px solid var(--accent); + &-score { + display: flex; + align-items: center; + gap: 3px; + } + p { color: var(--accent) !important; } @@ -119,6 +125,10 @@ display: flex; flex-direction: column; gap: 5px; + + app-tag { + cursor: pointer; + } } &-add-responsible { @@ -383,6 +393,15 @@ flex-direction: column; gap: 20px; padding: 14px; + + @for $i from 1 through 8 { + > li:nth-child(#{$i}) { + ::ng-deep app-skills-group .content--open { + top: 0px; + margin-top: -#{($i - 1) * 50}px; + } + } + } } li { @@ -393,3 +412,28 @@ } } } + +.cancel { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + max-height: calc(100vh - 40px); + padding: 30px 60px; + overflow-y: auto; + + &__title { + text-align: center; + } + + &__buttons { + display: flex; + gap: 10px; + align-items: center; + margin-top: 20px; + } + + &__button { + margin-top: 20px; + } +} diff --git a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts index 8448506c..3dc72113 100644 --- a/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts +++ b/projects/social_platform/src/app/office/projects/detail/kanban-board/shared/task/detail/task-detail.component.ts @@ -5,7 +5,6 @@ import { AfterViewInit, ChangeDetectorRef, Component, - computed, ElementRef, inject, Input, @@ -38,6 +37,11 @@ import { Subscription } from "rxjs"; import { TaskDetail } from "../../../models/task.model"; import { daysUntil } from "@utils/days-untit"; import { KanbanBoardService } from "../../../kanban-board.service"; +import { getPriorityType } from "@utils/helpers/getPriorityType"; +import { getActionType } from "@utils/helpers/getActionType"; +import { ActivatedRoute } from "@angular/router"; +import { TagData } from "../../create-tag-form/create-tag-form.component"; +import { FileService } from "@core/services/file.service"; @Component({ selector: "app-task-detail", @@ -73,24 +77,29 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { @ViewChild("descEl") descEl?: ElementRef; - private readonly kanbanBoardService = inject(KanbanBoardService); private readonly skillsService = inject(SkillsService); + private readonly kanbanBoardService = inject(KanbanBoardService); + private readonly fileService = inject(FileService); + private readonly route = inject(ActivatedRoute); private readonly cdRef = inject(ChangeDetectorRef); constructor(private readonly fb: FormBuilder) { this.taskDetailForm = this.fb.group({ title: ["", Validators.required], responsible: [], - performers: this.fb.array([]), + performers: [], startDate: [null], deadline: [null], - tags: this.fb.array([]), + tags: [[]], goal: [null], - skills: this.fb.array([]), + skills: [[]], description: [null], - files: this.fb.array([]), + files: [[]], priority: [null], + status: [null], action: [null], + score: [null], + tagsLib: [[]], }); } @@ -98,6 +107,8 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { remainingDaysDeadline = signal(0); + editingTag: TagData | null = null; + /** Уникальный ID для элемента input */ controlId = nanoid(3); @@ -112,18 +123,26 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { isGoalPickOpen = false; isTagsPickOpen = false; + creatingTag = false; + loadingFile = false; + showEditAvatarIcon = false; showEditDeadlineDatePicker = false; showEditStartDatePicker = false; + showChangeGoalModal = false; skillsGroupsModalOpen = signal(false); nestedSkills$ = this.skillsService.getSkillsNested(); openGroupIds = new Set(); + openGroupIndex: number | null = null; filesList: any[] = []; taskDetailForm: FormGroup; + getPriorityType = getPriorityType; + getActionType = getActionType; + subscriptions: Subscription[] = []; get actionTypeOptions() { @@ -154,8 +173,8 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { get goalPickOptions() { return this.goals - ? this.goals.map((goal, index) => ({ - id: index, + ? this.goals.map(goal => ({ + id: goal.id, label: goal.title, value: goal.id, additionalInfo: goal.title, @@ -164,20 +183,8 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { } get tagsPickOptions() { - return [ - { - id: 1, - label: "аналитика", - value: "аналитика", - additionalInfo: "primary", - }, - { - id: 2, - label: "продажи", - value: "продажи", - additionalInfo: "complete", - }, - ]; + const tagsLib = this.taskDetailForm.get("tagsLib")?.value || []; + return [...tagsLib]; } get priorityDeleteOptions() { @@ -191,7 +198,11 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { } get hasOpenSkillsGroups(): boolean { - return this.openGroupIds.size > 0; + return this.openGroupIndex !== null; + } + + get selectedSkills(): Skill[] { + return this.taskDetailForm.getRawValue().skills || []; } get statusOfTask() { @@ -216,7 +227,13 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { }; } - ngOnInit(): void {} + ngOnInit(): void { + this.taskDetailForm.valueChanges.subscribe({ + next: value => { + console.log(value); + }, + }); + } /** * Проверка возможности расширения описания после инициализации представления @@ -230,44 +247,169 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { this.checkDescriptionHeigth(); } - onDeleteTaskGoal(): void {} + onDeleteTaskGoal(): void { + this.taskDetailForm.patchValue({ goal: null }); + this.isGoalPickOpen = false; + } + + onDeleteTaskTag(tagId: number): void { + const tags = this.taskDetailForm.get("tags")?.value || []; + + const remainingTags = tags.filter((tag: any) => tag.id !== tagId); + this.taskDetailForm.patchValue({ tags: remainingTags }); + + this.isTagsPickOpen = false; + } - onDeleteTaskTag(): void {} + onEditTaskTag(tagId: number): void { + this.isTagsPickOpen = !this.isTagsPickOpen; + this.creatingTag = true; - onEditTaskTag(): void {} + const tags = this.taskDetailForm.get("tags")?.value || []; + const tag = tags.find((t: any) => t.id === tagId); - getCreatedTagInfo(createdTagInfo: { name: string; color: string }): void { - console.log(createdTagInfo); + if (tag) { + this.editingTag = { + id: tag.id, + name: tag.title, + color: tag.color, + }; + } } - toggleDropdown( + onUpdateTag({ id, name, color }: TagData): void { + const tagsLib = [...(this.taskDetailForm.get("tagsLib")?.value || [])]; + const libIndex = tagsLib.findIndex((t: any) => t.id === id); + + if (libIndex !== -1) { + tagsLib[libIndex] = { + ...tagsLib[libIndex], + label: name, + value: name, + additionalInfo: color, + }; + + this.taskDetailForm.patchValue({ tagsLib: [...tagsLib] }); + } + + const tags = [...(this.taskDetailForm.get("tags")?.value || [])]; + const tagIndex = tags.findIndex((t: any) => t.id === id); + + if (tagIndex !== -1) { + tags[tagIndex] = { + ...tags[tagIndex], + title: name, + color, + }; + this.taskDetailForm.patchValue({ tags: [...tags] }); + } + + this.editingTag = null; + this.creatingTag = false; + } + + createTag({ name, color }: { name: string; color: string }): void { + const { tagsLib } = this.taskDetailForm.value; + const tagInfo = { id: tagsLib.length + 1, label: name, value: name, additionalInfo: color }; + tagsLib.push(tagInfo); + } + + onTypeSelect( type: "action" | "priority" | "responsible" | "performers" | "goal" | "tags" | "delete", - state: boolean + state: boolean, + typeId?: number ) { switch (type) { case "action": this.isActionTypeOpen = state; + this.taskDetailForm.patchValue({ action: typeId }); break; case "priority": this.isPriorityTypeOpen = state; + this.taskDetailForm.patchValue({ priority: typeId }); break; - case "responsible": + case "responsible": { this.isResponsiblePickOpen = state; + + if (typeId !== undefined) { + if (!this.collaborators) return; + + const responsible = this.collaborators.find( + collaborator => collaborator.userId === typeId + ); + this.taskDetailForm.patchValue({ + responsible: { + id: responsible?.userId, + avatar: responsible?.avatar, + name: responsible?.firstName + " " + responsible?.lastName[0], + }, + }); + } break; + } - case "performers": + case "performers": { this.isPerformersPickOpen = state; + + if (typeId !== undefined) { + if (!this.collaborators) return; + + const collaborator = this.collaborators.find( + collaborator => collaborator.userId === typeId + ); + const currentPerformers = this.taskDetailForm.get("performers")?.value || []; + const payload = { + id: collaborator?.userId, + avatar: collaborator?.avatar, + name: collaborator?.firstName + " " + collaborator?.lastName[0], + }; + + if (currentPerformers.some((performer: any) => performer.id === payload.id)) return; + + this.taskDetailForm.patchValue({ + performers: [...currentPerformers, payload], + }); + } break; + } - case "goal": + case "goal": { this.isGoalPickOpen = state; + + if (typeId !== undefined) { + if (!this.goals) return; + + const goal = this.goals.find(goal => goal.id === typeId); + if (goal) this.taskDetailForm.patchValue({ goal: { id: goal.id, title: goal.title } }); + } + break; + } - case "tags": + case "tags": { this.isTagsPickOpen = state; + + if (!state) { + this.editingTag = null; + this.creatingTag = false; + } + + if (typeId !== undefined) { + const tag = this.tagsPickOptions.find((tag: any) => tag.id === typeId); + const payload = { id: tag?.id, title: tag?.label, color: tag?.additionalInfo }; + + const currentTags = this.taskDetailForm.get("tags")?.value || []; + if (currentTags.some((tag: any) => tag.id === payload.id)) return; + this.taskDetailForm.patchValue({ tags: [...currentTags, payload] }); + } else { + this.editingTag = null; + this.creatingTag = false; + } + break; + } case "delete": this.isDeleteTypeOpen = state; @@ -278,17 +420,9 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { } } - onTypeSelect( - typeId: number, - type: "action" | "priority" | "responsible" | "performers" | "goal" | "tags" | "delete" - ): void { - this.toggleDropdown(type, false); - console.log(typeId); - } - onChangeText(event: MouseEvent): void { event.stopPropagation(); - this.isChangeDescriptionText.set(!this.isChangeDescriptionText()); + this.isChangeDescriptionText.set(false); setTimeout(() => this.checkDescriptionHeigth(), 0); } @@ -306,21 +440,29 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { /** Обработчик загрузки файла */ onUpdate(event: Event): void { - const files = (event.currentTarget as HTMLInputElement).files; - if (!files?.length) { + const input = event.currentTarget as HTMLInputElement; + const file = input.files?.[0]; + if (!file) { return; } - console.log("123"); + this.loadingFile = true; + + this.fileService.uploadFile(file).subscribe(url => { + const currentFiles = this.taskDetailForm.get("files")?.value || []; - // this.loading = true; + const newFile = { + name: file.name.split, + link: url, + extension: file.type, + size: file.size, + }; - // this.fileService.uploadFile(files[0]).subscribe(res => { - // this.loading = false; + this.taskDetailForm.get("files")?.setValue([...currentFiles, newFile]); + input.value = ""; - // this.value = res.url; - // this.onChange(res.url); - // }); + this.loadingFile = false; + }); } /** @@ -334,19 +476,21 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { if (isPresent) { this.onRemoveSkill(toggledSkill); } else { - this.onAddSkill(toggledSkill); + if (skills.length < 3) { + this.onAddSkill(toggledSkill); + } } } onGroupToggled(isOpen: boolean, skillsGroupId: number): void { - this.openGroupIds.clear(); - if (isOpen) { - this.openGroupIds.add(skillsGroupId); - } - + this.openGroupIndex = isOpen ? skillsGroupId : null; this.cdRef.markForCheck(); } + isGroupDisabled(skillsGroupId: number): boolean { + return this.openGroupIndex !== null && this.openGroupIndex !== skillsGroupId; + } + /** * Переключение модального окна групп навыков */ @@ -365,6 +509,7 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { if (isPresent) return; this.taskDetailForm.patchValue({ skills: [newSkill, ...skills] }); + this.cdRef.markForCheck(); } /** @@ -377,6 +522,7 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { this.taskDetailForm.patchValue({ skills: skills.filter(skill => skill.id !== oddSkill.id), }); + this.cdRef.markForCheck(); } private checkDescriptionHeigth(): void { @@ -387,10 +533,11 @@ export class TaskDetailComponent implements OnInit, AfterViewInit { } private initializeTaskDetailInfo(todayDate: Date, tommorowDate: Date): void { - const taskDetailInfo$ = this.kanbanBoardService.getTaskById(123).subscribe({ + const taskId = this.route.snapshot.queryParams["taskId"]; + const taskDetailInfo$ = this.kanbanBoardService.getTaskById(taskId).subscribe({ next: (taskDetailInfo: TaskDetail) => { this.taskDetailForm.patchValue({ - title: taskDetailInfo.title ?? "Настроить процессы", + title: taskDetailInfo.title ?? "", responsible: taskDetailInfo.responsible ?? null, performers: taskDetailInfo.performers ?? [], startDate: taskDetailInfo.dateTaskStart ?? todayDate, diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts index 72ae92bd..90f9f3e7 100644 --- a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts +++ b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.ts @@ -20,7 +20,7 @@ import { } from "@angular/forms"; import { AuthService } from "@auth/services"; import { ErrorMessage } from "@error/models/error-message"; -import { directionProjectList } from "projects/core/src/consts/lists/ldirection-project-list.const"; +import { directionProjectList } from "projects/core/src/consts/lists/direction-project-list.const"; import { trackProjectList } from "projects/core/src/consts/lists/track-project-list.const"; import { Observable, Subscription } from "rxjs"; import { AvatarControlComponent } from "@ui/components/avatar-control/avatar-control.component"; diff --git a/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.scss b/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.scss index e7031401..c6ac5f6f 100644 --- a/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.scss +++ b/projects/social_platform/src/app/office/shared/skills-group/skills-group.component.scss @@ -13,6 +13,7 @@ height: 32px; cursor: pointer; transition: opacity 0.2s ease; + position: relative; i { transition: transform 0.2s ease; @@ -57,6 +58,9 @@ &--open { width: 40%; + position: absolute; + top: 0%; + right: 0%; } &__option { diff --git a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html index 95c1b26a..fe0bf3ab 100644 --- a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html +++ b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.html @@ -1,6 +1,6 @@ -
+
@@ -55,7 +55,11 @@ } @else { - + } } diff --git a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts index 1e329234..166651e4 100644 --- a/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts +++ b/projects/social_platform/src/app/ui/components/dropdown/dropdown.component.ts @@ -8,7 +8,10 @@ import { IconComponent } from "@uilib"; import { getPriorityType } from "@utils/helpers/getPriorityType"; import { TagComponent } from "../tag/tag.component"; import { ClickOutsideModule } from "ng-click-outside"; -import { CreateTagFormComponent } from "@office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component"; +import { + CreateTagFormComponent, + TagData, +} from "@office/projects/detail/kanban-board/shared/create-tag-form/create-tag-form.component"; @Component({ selector: "app-dropdown", @@ -34,10 +37,17 @@ export class DropdownComponent { /** Состояние для открытия списка выпадающего */ @Input() isOpen = false; + /** режим создания тега */ + @Input() creatingTag = false; + /** Состояние для выделения элемента списка выпадающего */ @Input() highlightedIndex = -1; - @Input() colorText: "grey" | "black" | "red" = "grey"; + @Input() colorText: "grey" | "red" = "grey"; + + @Input() editingTag: TagData | null = null; + + @Output() updateTag = new EventEmitter(); /** Событие для выбора элемента */ @Output() select = new EventEmitter(); @@ -49,9 +59,6 @@ export class DropdownComponent { @ViewChild("dropdown", { static: true }) dropdown!: ElementRef; - /** режим создания тега */ - creatingTag = false; - getPriorityType = getPriorityType; positions: ConnectedPosition[] = [ @@ -87,20 +94,21 @@ export class DropdownComponent { this.creatingTag = true; } + onConfirmUpdateTag(tagData: TagData): void { + this.updateTag.emit(tagData); + this.creatingTag = false; + } + onConfirmCreateTag(tagInfo: { name: string; color: string }): void { this.tagInfo.emit(tagInfo); this.creatingTag = false; - this.isOpen = false; } - getTextColor(colorText: "grey" | "black" | "red") { + getTextColor(colorText: "grey" | "red") { switch (colorText) { case "red": return "color: var(--red)"; - case "black": - return "color: var(--black)"; - case "grey": return "color: var(--grey-for-text)"; } diff --git a/projects/social_platform/src/app/ui/components/modal/modal.component.html b/projects/social_platform/src/app/ui/components/modal/modal.component.html index 1e8b0690..2f3e9692 100644 --- a/projects/social_platform/src/app/ui/components/modal/modal.component.html +++ b/projects/social_platform/src/app/ui/components/modal/modal.component.html @@ -1,6 +1,6 @@ -