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 }}
}