From ca0bd71820976bfcb559b527104fd1aeadf266de Mon Sep 17 00:00:00 2001 From: Fedor Date: Tue, 22 Sep 2026 19:14:09 +0300 Subject: [PATCH] feat(projects): show user activity card --- docs/project-activity/README.md | 73 +++++ docs/project-activity/dashboard-after.png | Bin 0 -> 17936 bytes docs/project-activity/dashboard-before.png | Bin 0 -> 17066 bytes docs/project-activity/mobile.png | Bin 0 -> 17716 bytes docs/project-activity/my-after.png | Bin 0 -> 15237 bytes docs/project-activity/my-before.png | Bin 0 -> 14269 bytes .../facades/projects-info.service.spec.ts | 77 ++++++ .../project/facades/projects-info.service.ts | 36 ++- .../project/ports/project.repository.port.ts | 3 +- .../src/app/domain/project/project.model.ts | 5 + .../adapters/project/dto/project.dto.ts | 3 + .../project/project-count-contract.spec.ts | 51 ++++ .../project/project.repository.spec.ts | 64 ++++- .../repository/project/project.repository.ts | 51 ++-- .../project-activity-card.component.html | 25 ++ .../project-activity-card.component.scss | 71 +++++ .../project-activity-card.component.spec.ts | 58 ++++ ...project-activity-card.component.stories.ts | 256 ++++++++++++++++++ .../project-activity-card.component.ts | 36 +++ .../projects-activity-placement.spec.ts | 28 ++ .../ui/pages/projects/projects.component.html | 9 +- .../ui/pages/projects/projects.component.ts | 9 +- 22 files changed, 820 insertions(+), 35 deletions(-) create mode 100644 docs/project-activity/README.md create mode 100644 docs/project-activity/dashboard-after.png create mode 100644 docs/project-activity/dashboard-before.png create mode 100644 docs/project-activity/mobile.png create mode 100644 docs/project-activity/my-after.png create mode 100644 docs/project-activity/my-before.png create mode 100644 projects/social_platform/src/app/api/project/facades/projects-info.service.spec.ts create mode 100644 projects/social_platform/src/app/infrastructure/adapters/project/project-count-contract.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html create mode 100644 projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss create mode 100644 projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts create mode 100644 projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.stories.ts create mode 100644 projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.ts create mode 100644 projects/social_platform/src/app/ui/pages/projects/projects-activity-placement.spec.ts diff --git a/docs/project-activity/README.md b/docs/project-activity/README.md new file mode 100644 index 000000000..7a7dec394 --- /dev/null +++ b/docs/project-activity/README.md @@ -0,0 +1,73 @@ + + +# Блок «Моя активность» в проектах + +На dashboard проектов и в разделе «Мои проекты» прежняя заглушка +`app-soon-card` заменена отдельным информационным компонентом +`ProjectActivityCardComponent`. Общий `SoonCardComponent` не изменён. + +Источник данных — существующий `ProjectRepositoryPort.refreshCount()` и +`count$`, которые получают authoritative ответ `GET /projects/count/`. +Компонент не выполняет HTTP-запросы и не вычисляет статистику по первым +16 проектам dashboard. + +Показатели: + +- «Проектов» — все уникальные legacy-проекты пользователя; +- «Я лидер» — проекты, где пользователь является лидером; +- «В программе» — опубликованные проекты с канонической, ещё не сданной + связью `PartnerProgramProject`; +- «Сдано» — проекты, у которых каноническая связь с программой сдана. + +Каноническая связь определяется backend по минимальному `pk`. После +успешного принятия приглашения Angular повторно запрашивает весь счётчик, +не пытаясь оптимистично вычислять lifecycle. + +Во время загрузки и при ошибке вместо недостоверных чисел отображается +нейтральный прочерк с доступной screen reader подписью. Для пользователя без +проектов отображаются четыре нуля. + +## Геометрия + +| Элемент | До | После | +| --------------------------- | ----------------: | ----------------: | +| Ширина карточки | 157 px | 157 px | +| Высота карточки | 140,2 px | 141 px | +| X правой колонки | 1075,8 px | 1075,8 px | +| Y карточки | 203,8 px | 203,8 px | +| Положение блока приглашений | 1075,8 × 113,8 px | 1075,8 × 113,8 px | + +Замеры сделаны в desktop Storybook-истории на реальных Angular-компонентах. +Разница высоты менее одного пикселя не меняет положение карточки, блока +приглашений или ширину правой колонки. + +## Визуальная проверка + +### Dashboard + +До: + +![Dashboard до](./dashboard-before.png) + +После: + +![Dashboard после](./dashboard-after.png) + +### Мои проекты + +До: + +![Мои проекты до](./my-before.png) + +После: + +![Мои проекты после](./my-after.png) + +### Mobile + +![Mobile](./mobile.png) + +Снимки созданы из Storybook-историй с реальными `SoonCardComponent` и +`ProjectActivityCardComponent`. Они фиксируют desktop и mobile footprint; +данные в истории демонстрационные, production-значения приходят только из +backend count endpoint. diff --git a/docs/project-activity/dashboard-after.png b/docs/project-activity/dashboard-after.png new file mode 100644 index 0000000000000000000000000000000000000000..a497dd940e4d7391ca3dda72ad6d98f884746fa8 GIT binary patch literal 17936 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 literal 0 HcmV?d00001 diff --git a/docs/project-activity/dashboard-before.png b/docs/project-activity/dashboard-before.png new file mode 100644 index 0000000000000000000000000000000000000000..02df4d6bee706cf9fdb92d2e8c3fb96bd50db552 GIT binary patch literal 17066 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 X^i#|sRgGvC{1b5z8R1ev-S7VmswI@j literal 0 HcmV?d00001 diff --git a/docs/project-activity/mobile.png b/docs/project-activity/mobile.png new file mode 100644 index 0000000000000000000000000000000000000000..6677b5179cd7b8abfd2ae7d041ba1b732fc751a8 GIT binary patch literal 17716 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

p!HR}+SD!~ju3 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 u^64Pv5e^Opqlw(~7-3$95 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 literal 0 HcmV?d00001 diff --git a/docs/project-activity/my-before.png b/docs/project-activity/my-before.png new file mode 100644 index 0000000000000000000000000000000000000000..c67758a249cd8c5ffedee8cdcf5a55f6eb2f3fe3 GIT binary patch literal 14269 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 literal 0 HcmV?d00001 diff --git a/projects/social_platform/src/app/api/project/facades/projects-info.service.spec.ts b/projects/social_platform/src/app/api/project/facades/projects-info.service.spec.ts new file mode 100644 index 000000000..6f29b87e7 --- /dev/null +++ b/projects/social_platform/src/app/api/project/facades/projects-info.service.spec.ts @@ -0,0 +1,77 @@ +/** @format */ + +import { signal } from "@angular/core"; +import { FormControl, FormGroup } from "@angular/forms"; +import { TestBed } from "@angular/core/testing"; +import { ActivatedRoute, NavigationEnd, Router } from "@angular/router"; +import { BehaviorSubject, of, Subject } from "rxjs"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { NavService } from "@api/shared/nav.service"; +import { InviteInfoService } from "@api/invite/facades/invite-info.service"; +import { CreateProjectUseCase } from "../use-cases/create-project.use-case"; +import { ProjectRepositoryPort } from "@domain/project/ports/project.repository.port"; +import { ProjectCount, ProjectCountLoadState } from "@domain/project/project.model"; +import { ProjectsUIInfoService } from "./ui/projects-ui-info.service"; +import { ProjectsInfoService } from "./projects-info.service"; + +describe("ProjectsInfoService: загрузка активности", () => { + const count: ProjectCount = { + all: 12, + my: 7, + subs: 3, + myLeader: 4, + myInProgram: 2, + mySubmitted: 1, + }; + + function setup(initialUrl: string) { + const events = new Subject(); + const router = { + url: initialUrl, + events, + navigate: vi.fn(() => Promise.resolve(true)), + }; + const repository = { + count$: new BehaviorSubject(count), + countState$: new BehaviorSubject("idle"), + refreshCount: vi.fn(() => of(count)), + }; + const searchForm = new FormGroup({ search: new FormControl("") }); + + TestBed.configureTestingModule({ + providers: [ + ProjectsInfoService, + { provide: Router, useValue: router }, + { provide: ActivatedRoute, useValue: { snapshot: {}, parent: null } }, + { provide: LoggerService, useValue: { debug: vi.fn(), warn: vi.fn() } }, + { provide: NavService, useValue: { setNavTitle: vi.fn() } }, + { provide: InviteInfoService, useValue: { ensureLoaded: vi.fn() } }, + { provide: ProjectsUIInfoService, useValue: { searchForm, myInvites: signal([]) } }, + { provide: CreateProjectUseCase, useValue: { execute: vi.fn() } }, + { provide: ProjectRepositoryPort, useValue: repository }, + ], + }); + + return { service: TestBed.inject(ProjectsInfoService), repository, router, events }; + } + + it.each(["/office/projects/dashboard", "/office/projects/my"])( + "запрашивает count при входе на %s", + url => { + const { service, repository } = setup(url); + service.initializationProjects(); + expect(repository.refreshCount).toHaveBeenCalledOnce(); + }, + ); + + it("не запрашивает count в остальных разделах и загружает при переходе в my", () => { + const { service, repository, router, events } = setup("/office/projects/subscriptions"); + service.initializationProjects(); + expect(repository.refreshCount).not.toHaveBeenCalled(); + + router.url = "/office/projects/my"; + events.next(new NavigationEnd(1, router.url, router.url)); + + expect(repository.refreshCount).toHaveBeenCalledOnce(); + }); +}); diff --git a/projects/social_platform/src/app/api/project/facades/projects-info.service.ts b/projects/social_platform/src/app/api/project/facades/projects-info.service.ts index fd5cc6d3e..1f9142a98 100644 --- a/projects/social_platform/src/app/api/project/facades/projects-info.service.ts +++ b/projects/social_platform/src/app/api/project/facades/projects-info.service.ts @@ -9,7 +9,8 @@ import { ProjectsUIInfoService } from "./ui/projects-ui-info.service"; import { CreateProjectUseCase } from "../use-cases/create-project.use-case"; import { AppRoutes } from "@api/paths/app-routes"; import { InviteInfoService } from "@api/invite/facades/invite-info.service"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop"; +import { ProjectRepositoryPort } from "@domain/project/ports/project.repository.port"; /** Координирует верхний уровень раздела проектов: табы, поиск и создание проекта. */ @Injectable() @@ -24,6 +25,7 @@ export class ProjectsInfoService { private readonly projectsUIInfoService = inject(ProjectsUIInfoService); private readonly createProjectUseCase = inject(CreateProjectUseCase); + private readonly projectRepository = inject(ProjectRepositoryPort); private readonly url = signal(this.router.url); private readonly searchForm = this.projectsUIInfoService.searchForm; @@ -33,11 +35,16 @@ export class ProjectsInfoService { readonly isSubs = computed(() => this.url().includes("/subscriptions")); readonly isInvites = computed(() => this.url().includes("/invites")); readonly isDashboard = computed(() => this.url().includes("/dashboard")); + readonly projectCount = toSignal(this.projectRepository.count$, { requireSync: true }); + readonly projectCountState = toSignal(this.projectRepository.countState$, { + requireSync: true, + }); initializationProjects(): void { this.navService.setNavTitle("Проекты"); this.inviteInfoService.ensureLoaded(); + this.refreshProjectCountIfVisible(); this.searchForm .get("search") @@ -65,7 +72,32 @@ export class ProjectsInfoService { filter(event => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef), ) - .subscribe(() => this.url.set(this.router.url)); + .subscribe(() => { + const previousSection = this.activitySection(this.url()); + this.url.set(this.router.url); + const currentSection = this.activitySection(this.url()); + + if (currentSection && currentSection !== previousSection) { + this.refreshProjectCountIfVisible(); + } + }); + } + + private activitySection(url: string): "dashboard" | "my" | null { + if (url.includes("/dashboard")) return "dashboard"; + if (url.includes("/my")) return "my"; + return null; + } + + private refreshProjectCountIfVisible(): void { + if (!this.isMy() && !this.isDashboard()) return; + + this.projectRepository + .refreshCount() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + error: error => this.logger.warn("Не удалось загрузить статистику проектов", error), + }); } addProject(): void { diff --git a/projects/social_platform/src/app/domain/project/ports/project.repository.port.ts b/projects/social_platform/src/app/domain/project/ports/project.repository.port.ts index 24c9dbdd0..1a61a0073 100644 --- a/projects/social_platform/src/app/domain/project/ports/project.repository.port.ts +++ b/projects/social_platform/src/app/domain/project/ports/project.repository.port.ts @@ -1,7 +1,7 @@ /** @format */ import { BehaviorSubject, Observable } from "rxjs"; -import { Project, ProjectCount } from "../project.model"; +import { Project, ProjectCount, ProjectCountLoadState } from "../project.model"; import { ApiPagination } from "../../other/api-pagination.model"; import { HttpParams } from "@angular/common/http"; import { ProjectCoverReset } from "../project-cover.model"; @@ -9,6 +9,7 @@ import { ProjectCoverReset } from "../project-cover.model"; /** Порт репозитория проектов — контракт CRUD. Реализуется в infrastructure/repository/project. */ export abstract class ProjectRepositoryPort { abstract readonly count$: BehaviorSubject; + abstract readonly countState$: BehaviorSubject; abstract getAll(params?: HttpParams): Observable>; abstract getOne(id: number): Observable; diff --git a/projects/social_platform/src/app/domain/project/project.model.ts b/projects/social_platform/src/app/domain/project/project.model.ts index 6b6e6d669..b08a904eb 100644 --- a/projects/social_platform/src/app/domain/project/project.model.ts +++ b/projects/social_platform/src/app/domain/project/project.model.ts @@ -97,8 +97,13 @@ export class ProjectCount { all!: number; my!: number; subs!: number; + myLeader!: number; + myInProgram!: number; + mySubmitted!: number; } +export type ProjectCountLoadState = "idle" | "loading" | "loaded" | "error"; + export class ProjectStep { id!: number; name!: string; diff --git a/projects/social_platform/src/app/infrastructure/adapters/project/dto/project.dto.ts b/projects/social_platform/src/app/infrastructure/adapters/project/dto/project.dto.ts index 95e5146dc..c3451f820 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/project/dto/project.dto.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/project/dto/project.dto.ts @@ -52,6 +52,9 @@ export interface ProjectCountDto { all: number; my: number; subs: number; + myLeader: number; + myInProgram: number; + mySubmitted: number; } export interface ProjectListDto { diff --git a/projects/social_platform/src/app/infrastructure/adapters/project/project-count-contract.spec.ts b/projects/social_platform/src/app/infrastructure/adapters/project/project-count-contract.spec.ts new file mode 100644 index 000000000..9193c1a0e --- /dev/null +++ b/projects/social_platform/src/app/infrastructure/adapters/project/project-count-contract.spec.ts @@ -0,0 +1,51 @@ +/** @format */ + +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { ProjectHttpAdapter } from "./project-http.adapter"; + +describe("Контракт статистики проектов через общий interceptor", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + ], + }), + ); + + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + it("преобразует snake_case метрик в camelCase Angular domain", () => { + TestBed.inject(ProjectHttpAdapter) + .fetchCount() + .subscribe(value => { + expect(value).toEqual({ + all: 12, + my: 7, + myLeader: 4, + myInProgram: 2, + mySubmitted: 1, + }); + }); + + const request = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/projects/count/", + ); + expect(request.request.method).toBe("GET"); + request.flush({ + all: 12, + my: 7, + my_leader: 4, + my_in_program: 2, + my_submitted: 1, + }); + }); +}); diff --git a/projects/social_platform/src/app/infrastructure/repository/project/project.repository.spec.ts b/projects/social_platform/src/app/infrastructure/repository/project/project.repository.spec.ts index 6edfd5671..80d9dff08 100644 --- a/projects/social_platform/src/app/infrastructure/repository/project/project.repository.spec.ts +++ b/projects/social_platform/src/app/infrastructure/repository/project/project.repository.spec.ts @@ -1,7 +1,7 @@ /** @format */ import { TestBed } from "@angular/core/testing"; -import { of, Subject } from "rxjs"; +import { of, Subject, throwError } from "rxjs"; import { HttpParams } from "@angular/common/http"; import { ProjectRepository } from "./project.repository"; import { ProjectHttpAdapter } from "../../adapters/project/project-http.adapter"; @@ -17,6 +17,7 @@ import { removeProjectCollaborator } from "@domain/project/events/remove-project import { sendVacancyResponse } from "@domain/vacancy/events/send-vacancy-response.event"; import { acceptVacancyResponse } from "@domain/vacancy/events/accept-vacancy-response.event"; import { rejectVacancyResponse } from "@domain/vacancy/events/reject-vacancy-response.event"; +import { acceptInvite } from "@domain/invite/events/accept-invite.event"; describe("ProjectRepository", () => { let repository: ProjectRepository; @@ -39,8 +40,19 @@ describe("ProjectRepository", () => { }); eventBus = TestBed.inject(EventBus); repository = TestBed.inject(ProjectRepository); + adapter.fetchCount.mockReturnValue(of(activityCount())); } + const activityCount = (overrides: Partial = {}): ProjectCount => ({ + all: 0, + my: 0, + subs: 0, + myLeader: 0, + myInProgram: 0, + mySubmitted: 0, + ...overrides, + }); + const page = (): ApiPagination => ({ count: 1, next: "", @@ -97,17 +109,41 @@ describe("ProjectRepository", () => { it("refreshCount мапит в ProjectCount и пушит в count$", () => new Promise(done => { setup(); - adapter.fetchCount.mockReturnValue(of({ my: 1, all: 2, subs: 3 } as ProjectCount)); + adapter.fetchCount.mockReturnValue( + of( + activityCount({ + my: 1, + all: 2, + subs: 3, + myLeader: 4, + myInProgram: 5, + mySubmitted: 6, + }), + ), + ); repository.refreshCount().subscribe(count => { expect(count).toBeInstanceOf(ProjectCount); expect(repository.count$.getValue().my).toBe(1); expect(repository.count$.getValue().all).toBe(2); expect(repository.count$.getValue().subs).toBe(3); + expect(repository.count$.getValue().myLeader).toBe(4); + expect(repository.count$.getValue().myInProgram).toBe(5); + expect(repository.count$.getValue().mySubmitted).toBe(6); + expect(repository.countState$.getValue()).toBe("loaded"); done(); }); })); + it("refreshCount помечает недоступный count API как error", () => { + setup(); + adapter.fetchCount.mockReturnValue(throwError(() => new Error("boom"))); + + repository.refreshCount().subscribe({ error: () => undefined }); + + expect(repository.countState$.getValue()).toBe("error"); + }); + it("update мапит ответ в Project и инвалидирует кеш", () => new Promise(done => { setup(); @@ -150,18 +186,20 @@ describe("ProjectRepository", () => { expect(adapter.deleteOne).toHaveBeenCalledExactlyOnceWith(42); }); - it("ProjectCreated увеличивает count.my", () => { + it("ProjectCreated перечитывает authoritative count", () => { setup(); - repository.count$.next({ my: 1, all: 0, subs: 0 }); + adapter.fetchCount.mockReturnValue(of(activityCount({ my: 2, myLeader: 2 }))); eventBus.emit(projectCreated({ id: 1 } as Project)); expect(repository.count$.getValue().my).toBe(2); + expect(repository.count$.getValue().myLeader).toBe(2); + expect(adapter.fetchCount).toHaveBeenCalledOnce(); }); - it("ProjectDeleted уменьшает count.my и инвалидирует кеш", () => { + it("ProjectDeleted перечитывает count и инвалидирует кеш", () => { setup(); adapter.fetchOne.mockReturnValue(of({ id: 7 } as ProjectDto)); repository.getOne(7).subscribe(); - repository.count$.next({ my: 2, all: 0, subs: 0 }); + adapter.fetchCount.mockReturnValue(of(activityCount({ my: 1 }))); eventBus.emit(projectDeleted(7)); @@ -172,18 +210,28 @@ describe("ProjectRepository", () => { it("ProjectSubscribed увеличивает count.subs", () => { setup(); - repository.count$.next({ my: 0, all: 0, subs: 1 }); + repository.count$.next(activityCount({ subs: 1 })); eventBus.emit(projectSubscribed(7)); expect(repository.count$.getValue().subs).toBe(2); }); it("ProjectUnSubscribed уменьшает count.subs (не ниже 0) и инвалидирует", () => { setup(); - repository.count$.next({ my: 0, all: 0, subs: 0 }); + repository.count$.next(activityCount()); eventBus.emit(projectUnSubscribed(7)); expect(repository.count$.getValue().subs).toBe(0); }); + it("AcceptInvite перечитывает count после успешного доменного события", () => { + setup(); + adapter.fetchCount.mockReturnValue(of(activityCount({ my: 1 }))); + + eventBus.emit(acceptInvite(5, 7, 11, "Участник")); + + expect(adapter.fetchCount).toHaveBeenCalledOnce(); + expect(repository.count$.getValue().my).toBe(1); + }); + it("RemoveProjectCollaborator инвалидирует кеш проекта", () => { setup(); adapter.fetchOne.mockReturnValue(of({ id: 7 } as ProjectDto)); diff --git a/projects/social_platform/src/app/infrastructure/repository/project/project.repository.ts b/projects/social_platform/src/app/infrastructure/repository/project/project.repository.ts index 263188cd5..00c6ae6fd 100644 --- a/projects/social_platform/src/app/infrastructure/repository/project/project.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/project/project.repository.ts @@ -1,8 +1,8 @@ /** @format */ import { inject, Injectable } from "@angular/core"; -import { BehaviorSubject, map, Observable, tap } from "rxjs"; -import { Project, ProjectCount } from "@domain/project/project.model"; +import { BehaviorSubject, catchError, map, Observable, tap, throwError } from "rxjs"; +import { Project, ProjectCount, ProjectCountLoadState } from "@domain/project/project.model"; import { ProjectHttpAdapter } from "../../adapters/project/project-http.adapter"; import { ApiPagination } from "@domain/other/api-pagination.model"; import { HttpParams } from "@angular/common/http"; @@ -28,7 +28,15 @@ import { defer, finalize } from "rxjs"; @Injectable({ providedIn: "root" }) export class ProjectRepository implements ProjectRepositoryPort { private readonly entityCache = new EntityCache(); - readonly count$ = new BehaviorSubject({ my: 0, all: 0, subs: 0 }); + readonly count$ = new BehaviorSubject({ + my: 0, + all: 0, + subs: 0, + myLeader: 0, + myInProgram: 0, + mySubmitted: 0, + }); + readonly countState$ = new BehaviorSubject("idle"); private readonly projectAdapter = inject(ProjectHttpAdapter); private readonly eventBus = inject(EventBus); @@ -38,20 +46,14 @@ export class ProjectRepository implements ProjectRepositoryPort { } private initializeEventListeners(): void { - // События синхронизируют счётчики и кеш деталей без повторной загрузки всех списков. + // Изменения состава проектов требуют повторной authoritative-агрегации на backend. this.eventBus.on("ProjectCreated").subscribe(() => { - this.count$.next({ - ...this.count$.getValue(), - my: this.count$.getValue().my + 1, - }); + this.refreshCountAfterProjectChange(); }); this.eventBus.on("ProjectDeleted").subscribe(event => { this.entityCache.invalidate(event.payload.projectId); - this.count$.next({ - ...this.count$.getValue(), - my: Math.max(0, this.count$.getValue().my - 1), - }); + this.refreshCountAfterProjectChange(); }); this.eventBus.on("ProjectSubscribed").subscribe({ @@ -82,6 +84,7 @@ export class ProjectRepository implements ProjectRepositoryPort { this.eventBus.on("AcceptInvite").subscribe({ next: event => { this.entityCache.invalidate(event.payload.projectId); + this.refreshCountAfterProjectChange(); }, }); @@ -124,10 +127,26 @@ export class ProjectRepository implements ProjectRepositoryPort { } refreshCount(): Observable { - return this.projectAdapter.fetchCount().pipe( - map(dto => plainToInstance(ProjectCount, dto)), - tap(count => this.count$.next(count)), - ); + return defer(() => { + this.countState$.next("loading"); + + return this.projectAdapter.fetchCount().pipe( + map(dto => plainToInstance(ProjectCount, dto)), + tap(count => { + this.count$.next({ ...this.count$.getValue(), ...count }); + this.countState$.next("loaded"); + }), + catchError(error => { + this.countState$.next("error"); + return throwError(() => error); + }), + ); + }); + } + + /** После доменного изменения перечитывает все взаимосвязанные метрики одним запросом. */ + private refreshCountAfterProjectChange(): void { + this.refreshCount().subscribe({ error: () => undefined }); } update(id: number, data: Partial): Observable { diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html new file mode 100644 index 000000000..fb532db80 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.html @@ -0,0 +1,25 @@ + + +

+

моя активность

+ +
+ @for (metric of metrics(); track metric.label) { +
+
+ + {{ metric.label }} +
+
+ @if (isLoaded()) { + {{ metric.value }} + } @else { + — + } +
+
+ } +
+
diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss new file mode 100644 index 000000000..c8a41b967 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.scss @@ -0,0 +1,71 @@ +:host { + display: block; + width: 157px; + height: 141px; +} + +.activity { + box-sizing: border-box; + width: 100%; + height: 100%; + padding: 9px 10px; + background-color: var(--light-white); + border: 0.5px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-lg); + + &__title { + margin: 0 0 5px; + font-size: 11px; + font-weight: 600; + line-height: 14px; + color: var(--accent); + text-transform: uppercase; + } + + &__metrics { + margin: 0; + } + + &__metric { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 26px; + + & + & { + border-top: 0.5px solid var(--medium-grey-for-outline); + } + } + + &__label { + display: flex; + gap: 4px; + align-items: center; + min-width: 0; + font-size: 9px; + font-weight: 400; + line-height: 12px; + color: var(--black); + white-space: nowrap; + } + + &__icon { + display: grid; + flex: 0 0 20px; + place-items: center; + width: 20px; + height: 20px; + color: var(--accent); + background-color: var(--accent-light); + border-radius: 50%; + } + + &__value { + flex: 0 0 auto; + margin: 0 0 0 4px; + font-size: 13px; + font-weight: 600; + line-height: 16px; + color: var(--accent); + } +} diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts new file mode 100644 index 000000000..434569b54 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.spec.ts @@ -0,0 +1,58 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { ProjectCount } from "@domain/project/project.model"; +import { ProjectActivityCardComponent } from "./project-activity-card.component"; + +describe("ProjectActivityCardComponent", () => { + const count = (overrides: Partial = {}): ProjectCount => ({ + all: 0, + my: 3, + subs: 0, + myLeader: 2, + myInProgram: 1, + mySubmitted: 4, + ...overrides, + }); + + function render(value = count(), state: "loaded" | "loading" | "error" = "loaded") { + const fixture = TestBed.createComponent(ProjectActivityCardComponent); + fixture.componentRef.setInput("count", value); + fixture.componentRef.setInput("state", state); + fixture.detectChanges(); + return fixture; + } + + it("показывает четыре значения authoritative count", () => { + const element = render().nativeElement as HTMLElement; + const rows = Array.from(element.querySelectorAll(".activity__metric")); + + expect(rows).toHaveLength(4); + expect(rows.map(row => row.textContent?.replace(/\s+/g, " ").trim())).toEqual([ + "Проектов 3", + "Я лидер 2", + "В программе 1", + "Сдано 4", + ]); + }); + + it("отображает нули как числа и не содержит текст старой заглушки", () => { + const element = render(count({ my: 0, myLeader: 0, myInProgram: 0, mySubmitted: 0 })) + .nativeElement as HTMLElement; + + expect(element.querySelectorAll(".activity__value")).toHaveLength(4); + expect( + Array.from(element.querySelectorAll(".activity__value")).map(x => x.textContent?.trim()), + ).toEqual(["0", "0", "0", "0"]); + expect(element.textContent).not.toContain("скоро"); + expect(element.textContent).not.toContain("пока закрыто"); + expect(element.textContent).not.toContain("Подробнее"); + }); + + it.each(["loading", "error"] as const)("не показывает выдуманные числа в состоянии %s", state => { + const element = render(count(), state).nativeElement as HTMLElement; + expect( + Array.from(element.querySelectorAll(".activity__value")).map(x => x.textContent?.trim()), + ).toEqual(["—", "—", "—", "—"]); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.stories.ts b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.stories.ts new file mode 100644 index 000000000..a22bbc175 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.stories.ts @@ -0,0 +1,256 @@ +/** @format */ + +import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; +import { ProjectCount } from "@domain/project/project.model"; +import { SoonCardComponent } from "@ui/primitives/soon-card/soon-card.component"; +import { ProjectActivityCardComponent } from "./project-activity-card.component"; + +const count: ProjectCount = { + all: 24, + my: 7, + subs: 3, + myLeader: 4, + myInProgram: 2, + mySubmitted: 1, +}; + +const fixtureStyles = ` + +`; + +const projects = ` +
+
+

Цифровая платформа проекта

+
+
+
+
+

Образовательная инициатива

+
+
+
+
+`; + +const meta: Meta = { + title: "Projects/Моя активность", + component: ProjectActivityCardComponent, + decorators: [ + moduleMetadata({ + imports: [ProjectActivityCardComponent, SoonCardComponent], + }), + ], + parameters: { layout: "fullscreen" }, +}; + +export default meta; + +type Story = StoryObj; + +export const Card: Story = { + name: "Карточка", + args: { count, state: "loaded" }, +}; + +export const DashboardBefore: Story = { + name: "Dashboard · до", + render: () => ({ + template: ` + ${fixtureStyles} +
+

Проекты · dashboard

+
+ ${projects} + +
+
+ `, + }), +}; + +export const DashboardAfter: Story = { + name: "Dashboard · после", + render: () => ({ + props: { count }, + template: ` + ${fixtureStyles} +
+

Проекты · dashboard

+
+ ${projects} + +
+
+ `, + }), +}; + +export const MyProjectsBefore: Story = { + name: "Мои проекты · до", + render: () => ({ + template: ` + ${fixtureStyles} +
+

Проекты · мои проекты

+
+ ${projects} + +
+
+ `, + }), +}; + +export const MyProjectsAfter: Story = { + name: "Мои проекты · после", + render: () => ({ + props: { count }, + template: ` + ${fixtureStyles} +
+

Проекты · мои проекты

+
+ ${projects} + +
+
+ `, + }), +}; diff --git a/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.ts b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.ts new file mode 100644 index 000000000..7b0df80a6 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/project-activity-card/project-activity-card.component.ts @@ -0,0 +1,36 @@ +/** @format */ + +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; +import { ProjectCount, ProjectCountLoadState } from "@domain/project/project.model"; +import { IconComponent } from "@ui/primitives/icon/icon.component"; + +interface ProjectActivityMetric { + icon: string; + label: string; + value: number; +} + +/** Компактная информационная карточка проектной активности текущего пользователя. */ +@Component({ + selector: "app-project-activity-card", + templateUrl: "./project-activity-card.component.html", + styleUrl: "./project-activity-card.component.scss", + imports: [IconComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProjectActivityCardComponent { + readonly count = input.required(); + readonly state = input.required(); + + protected readonly metrics = computed(() => [ + { icon: "projects", label: "Проектов", value: this.count().my }, + { icon: "person", label: "Я лидер", value: this.count().myLeader }, + { icon: "program", label: "В программе", value: this.count().myInProgram }, + { icon: "check", label: "Сдано", value: this.count().mySubmitted }, + ]); + + protected readonly isLoaded = computed(() => this.state() === "loaded"); + protected readonly placeholderLabel = computed(() => + this.state() === "error" ? "Данные временно недоступны" : "Загрузка данных", + ); +} diff --git a/projects/social_platform/src/app/ui/pages/projects/projects-activity-placement.spec.ts b/projects/social_platform/src/app/ui/pages/projects/projects-activity-placement.spec.ts new file mode 100644 index 000000000..a2ede1797 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/projects/projects-activity-placement.spec.ts @@ -0,0 +1,28 @@ +/** @format */ + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +describe("ProjectsComponent: размещение проектной активности", () => { + const template = readFileSync( + join( + process.cwd(), + "projects/social_platform/src/app/ui/pages/projects/projects.component.html", + ), + "utf8", + ); + + it("показывает блок только для dashboard и my", () => { + expect(template).toMatch( + /@if \(isMy\(\) \|\| isDashboard\(\)\) \{\s* { + const conditionalBlock = new RegExp( + `@if \\(${condition.replace(/[()]/g, "\\$&")}\\) \\{[\\s\\S]*? } @if (isMy() || isDashboard()) { - - + } @if (isAll()) {
diff --git a/projects/social_platform/src/app/ui/pages/projects/projects.component.ts b/projects/social_platform/src/app/ui/pages/projects/projects.component.ts index 3edf5631e..246002a8d 100644 --- a/projects/social_platform/src/app/ui/pages/projects/projects.component.ts +++ b/projects/social_platform/src/app/ui/pages/projects/projects.component.ts @@ -12,16 +12,17 @@ import { import { RouterOutlet } from "@angular/router"; import { ReactiveFormsModule } from "@angular/forms"; import { SearchComponent } from "@ui/primitives/search/search.component"; -import { ButtonComponent, IconComponent } from "@ui/primitives"; +import { ButtonComponent } from "@ui/primitives/button/button.component"; +import { IconComponent } from "@ui/primitives/icon/icon.component"; import { BarNewComponent } from "./bar-new/bar.component"; import { BackComponent } from "@uilib"; -import { SoonCardComponent } from "@ui/primitives/soon-card/soon-card.component"; import { InfoCardComponent } from "@ui/widgets/info-card/info-card.component"; import { ProjectsUIInfoService } from "@api/project/facades/ui/projects-ui-info.service"; import { ProjectsInfoService } from "@api/project/facades/projects-info.service"; import { SwipeService } from "@api/swipe/swipe.service"; import { ProjectsFilterComponent } from "@ui/widgets/projects-filter/projects-filter.component"; import { OfficeInfoService } from "@api/office/facades/office-info.service"; +import { ProjectActivityCardComponent } from "./project-activity-card/project-activity-card.component"; /** Контейнер модуля проектов с поиском, фильтрацией и навигацией по разделам. */ @Component({ @@ -36,7 +37,7 @@ import { OfficeInfoService } from "@api/office/facades/office-info.service"; RouterOutlet, BarNewComponent, BackComponent, - SoonCardComponent, + ProjectActivityCardComponent, ProjectsFilterComponent, InfoCardComponent, ], @@ -64,6 +65,8 @@ export class ProjectsComponent implements OnInit { protected readonly isSubs = this.projectsInfoService.isSubs; protected readonly isInvites = this.projectsInfoService.isInvites; protected readonly isDashboard = this.projectsInfoService.isDashboard; + protected readonly projectCount = this.projectsInfoService.projectCount; + protected readonly projectCountState = this.projectsInfoService.projectCountState; protected readonly isFilterOpen = this.swipeService.isFilterOpen;