From 62890df28db402dad467bf475cf6c85fcb39ce05 Mon Sep 17 00:00:00 2001 From: Peter <81633644+TThanos3000@users.noreply.github.com> Date: Thu, 29 Oct 2026 12:41:49 +0300 Subject: [PATCH 1/2] fix(programs): clarify setup validation and restore program heading --- .../management/management.component.html | 5 +++ .../management/management.component.spec.ts | 43 +++++++++++++++++++ .../management/management.component.ts | 20 ++++++++- .../solution-form.component.html | 5 ++- .../solution-form.component.spec.ts | 26 +++++++++++ .../solution-form/solution-form.component.ts | 24 +++++++++++ .../public-registration.component.spec.ts | 8 +++- .../public-registration.component.ts | 6 ++- .../detail/detail-cleanup.component.spec.ts | 16 +++++++ .../ui/widgets/detail/detail.component.html | 4 ++ .../ui/widgets/detail/detail.component.scss | 6 +++ 11 files changed, 158 insertions(+), 5 deletions(-) diff --git a/projects/social_platform/src/app/ui/pages/program/management/management.component.html b/projects/social_platform/src/app/ui/pages/program/management/management.component.html index 83c393718..caafe8926 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/management.component.html +++ b/projects/social_platform/src/app/ui/pages/program/management/management.component.html @@ -820,7 +820,12 @@

Календарь программы

type="datetime-local" [id]="field.key" [formControlName]="field.key" + [attr.aria-invalid]="dateError(field.key) ? 'true' : null" + [attr.aria-describedby]="dateError(field.key) ? field.key + '-error' : null" /> + @if (dateError(field.key); as error) { +

{{ error }}

+ } }

Если отдельный срок сдачи не задан, действует срок окончания регистрации. diff --git a/projects/social_platform/src/app/ui/pages/program/management/management.component.spec.ts b/projects/social_platform/src/app/ui/pages/program/management/management.component.spec.ts index 5b007eadc..3aeaff9b1 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/management.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/program/management/management.component.spec.ts @@ -113,6 +113,49 @@ describe("Сохранение настроек организатора", () => expect(component.basicForm.dirty).toBe(true); expect(component.formError()).toContain("Данные изменились"); }); + it.each(["2026-09-30T18:00", "2026-10-01T15:00", "2026-10-01T15:00:00"])( + "не отправляет несогласованные сроки и сохраняет введенное окончание %s", + finish => { + const { component, save } = setup(); + component.settingsTab.set("dates"); + component.datesForm.controls.datetimeFinished.setValue(finish); + component.datesForm.controls.datetimeFinished.markAsDirty(); + component.save(); + expect(save).not.toHaveBeenCalled(); + expect(component.dateError("datetimeFinished")).toContain("позже начала"); + expect(component.dateError("datetimeRegistrationEnds")).toContain("не позже окончания"); + expect(component.datesForm.controls.datetimeFinished.value).toBe(finish); + expect(component.datesForm.dirty).toBe(true); + }, + ); + it("разрешает регистрацию после старта вплоть до окончания программы", () => { + const { component, save } = setup(); + component.settingsTab.set("dates"); + const registration = component.datesForm.controls.datetimeRegistrationEnds; + registration.setValue(component.datesForm.controls.datetimeFinished.value); + registration.markAsDirty(); + component.save(); + expect(component.dateError("datetimeRegistrationEnds")).toBeNull(); + expect(save).toHaveBeenCalledWith(1, "dates", { + expectedUpdatedAt: program.updatedAt, + confirmReopen: false, + datetimeRegistrationEnds: new Date(program.datetimeFinished!).toISOString(), + }); + }); + it("не требует полного календаря черновика", () => { + const { component, state, save } = setup(); + state.program.set({ ...program, draft: true }); + component.resetForms(); + component.settingsTab.set("dates"); + component.datesForm.controls.datetimeFinished.setValue(""); + component.datesForm.controls.datetimeFinished.markAsDirty(); + component.save(); + expect(save).toHaveBeenCalledWith(1, "dates", { + expectedUpdatedAt: program.updatedAt, + confirmReopen: false, + datetimeFinished: null, + }); + }); it("сохраняет mobile URL и полный framing; 409 оставляет кадр для повторной попытки", () => { const { component, save } = setup(); component.settingsTab.set("basic"); diff --git a/projects/social_platform/src/app/ui/pages/program/management/management.component.ts b/projects/social_platform/src/app/ui/pages/program/management/management.component.ts index 4171a8935..4fd70c930 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/management.component.ts +++ b/projects/social_platform/src/app/ui/pages/program/management/management.component.ts @@ -1197,6 +1197,18 @@ export class ProgramManagementComponent implements OnInit { control.updateValueAndValidity({ emitEvent: false }); } } + dateError(field: string): string | null { + const values = this.datesForm.getRawValue(); + const timestamp = (value: string | null) => (value ? Date.parse(`${value}+03:00`) : Number.NaN); + const start = timestamp(values.datetimeStarted), + finish = timestamp(values.datetimeFinished), + registration = timestamp(values.datetimeRegistrationEnds); + if (field === "datetimeFinished" && finish <= start) + return "Окончание программы должно быть позже начала."; + if (field === "datetimeRegistrationEnds" && registration > finish) + return "Регистрация должна завершиться не позже окончания программы."; + return null; + } save(confirmReopen = false): void { const p = this.state.program(), section = this.settingsTab(); @@ -1212,9 +1224,13 @@ export class ProgramManagementComponent implements OnInit { ) return; const form = section === "basic" ? this.basicForm : this.datesForm; - if (form.invalid) { + const dateError = + section === "dates" + ? this.dateFields.map(field => this.dateError(field.key)).find(Boolean) + : null; + if (form.invalid || dateError) { form.markAllAsTouched(); - this.formError.set("Заполните обязательные поля."); + this.formError.set(dateError || "Заполните обязательные поля."); return; } const body: ManagementSettingsWrite = { expectedUpdatedAt: p.updatedAt, confirmReopen }; diff --git a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html index fdf453c32..551dba054 100644 --- a/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html +++ b/projects/social_platform/src/app/ui/pages/program/management/solution-form/solution-form.component.html @@ -106,6 +106,9 @@

{{ field.label }}{{ field.required ? " *" : "" }}

добавьте его заново.

} + @if (optionsProblem(field); as problem) { +

{{ problem }}

+ }
+ @if (appWidth >= 1000 && listType() === "program") { +

{{ info().name }}

+ } + @if (appWidth >= 1000 && listType() === "project") {

{{ info().name }}

} diff --git a/projects/social_platform/src/app/ui/widgets/detail/detail.component.scss b/projects/social_platform/src/app/ui/widgets/detail/detail.component.scss index 2005f167f..c495df4c0 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/detail.component.scss +++ b/projects/social_platform/src/app/ui/widgets/detail/detail.component.scss @@ -181,6 +181,12 @@ $detail-bar-mb: 12px; overflow-wrap: anywhere; } + &__program-heading { + display: block; + margin-bottom: 20px; + overflow-wrap: anywhere; + } + &__cover-frame { position: absolute; inset: 0; From a0b5b4aae7753a9281d3fb7da00b96da013bc884 Mon Sep 17 00:00:00 2001 From: Peter <81633644+TThanos3000@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:48:33 +0300 Subject: [PATCH 2/2] fix(programs): preserve one heading on public solution pages --- .../detail/detail-cleanup.component.spec.ts | 22 ++++++++++++++++++- .../ui/widgets/detail/detail.component.html | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/projects/social_platform/src/app/ui/widgets/detail/detail-cleanup.component.spec.ts b/projects/social_platform/src/app/ui/widgets/detail/detail-cleanup.component.spec.ts index 47f22d3a6..3ba6c6dcd 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/detail-cleanup.component.spec.ts +++ b/projects/social_platform/src/app/ui/widgets/detail/detail-cleanup.component.spec.ts @@ -10,6 +10,8 @@ import { ProjectFormService } from "@api/project/project-form.service"; import { ProjectAdditionalService } from "@api/project/facades/edit/project-additional.service"; import { ChatStateService } from "@domain/shared/chat-state.service"; import { Program } from "@domain/program/program.model"; +import { ProgramDetailMainUIInfoService } from "@api/program/facades/detail/ui/program-detail-main-ui-info.service"; +import { PublicProgramSolutionsComponent } from "@ui/pages/program/published-solutions/public-program-solutions.component"; import { DeatilComponent } from "./detail.component"; import { DetailInfoService } from "./services/detail-info.service"; import { DetailProgramInfoService } from "./services/program/detail-program-info.service"; @@ -49,9 +51,10 @@ describe("Detail cleanup actions", () => { }, ); await TestBed.configureTestingModule({ - imports: [DeatilComponent], + imports: [DeatilComponent, PublicProgramSolutionsComponent], providers: [ provideRouter([]), + { provide: ProgramDetailMainUIInfoService, useValue: { program: info } }, { provide: ProjectFormService, useValue: { getForm: () => new FormGroup({}) } }, { provide: ChatStateService, useValue: { userOnlineStatusCache: of({}) } }, ], @@ -149,6 +152,23 @@ describe("Detail cleanup actions", () => { } }, ); + it.each(["", "https://example.test/cover.png"])( + "keeps one desktop heading in the public solutions page with cover %s", + coverImageAddress => { + const previousWidth = window.innerWidth; + Object.defineProperty(window, "innerWidth", { configurable: true, value: 1440 }); + try { + info.set({ ...Program.default(), name: "Публичная программа", coverImageAddress }); + const fixture = TestBed.createComponent(PublicProgramSolutionsComponent); + fixture.detectChanges(); + const headings = fixture.nativeElement.querySelectorAll("h1"); + expect(headings).toHaveLength(1); + expect(headings[0].textContent).toContain("Публичная программа"); + } finally { + Object.defineProperty(window, "innerWidth", { configurable: true, value: previousWidth }); + } + }, + ); it("organizer has management without a duplicate public-solutions action", () => { manager.set(true); info.set({ ...Program.default(), solutionStorageMode: "solution" }); diff --git a/projects/social_platform/src/app/ui/widgets/detail/detail.component.html b/projects/social_platform/src/app/ui/widgets/detail/detail.component.html index 907a40979..d23ce79e7 100644 --- a/projects/social_platform/src/app/ui/widgets/detail/detail.component.html +++ b/projects/social_platform/src/app/ui/widgets/detail/detail.component.html @@ -91,7 +91,7 @@ > - @if (appWidth >= 1000 && listType() === "program") { + @if (appWidth >= 1000 && listType() === "program" && !publicationsOnly()) {

{{ info().name }}

}