diff --git a/projects/social_platform/src/app/office/office.component.scss b/projects/social_platform/src/app/office/office.component.scss
index 9738a8301..808ce9de9 100644
--- a/projects/social_platform/src/app/office/office.component.scss
+++ b/projects/social_platform/src/app/office/office.component.scss
@@ -56,6 +56,14 @@
}
}
+ &__programs {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ max-width: 135px;
+ margin-top: 12px;
+ }
+
&__inner {
display: flex;
width: 100%;
@@ -89,7 +97,7 @@
display: flex;
flex-direction: column;
gap: 2px;
- width: 70%;
+ width: 80%;
margin-top: 30px;
color: var(--dark-grey);
}
diff --git a/projects/social_platform/src/app/office/office.component.ts b/projects/social_platform/src/app/office/office.component.ts
index 3c82830dd..ad397fc50 100644
--- a/projects/social_platform/src/app/office/office.component.ts
+++ b/projects/social_platform/src/app/office/office.component.ts
@@ -1,12 +1,11 @@
/** @format */
-import { Component, OnDestroy, OnInit, Signal } from "@angular/core";
+import { Component, OnDestroy, OnInit, signal, Signal } from "@angular/core";
import { IndustryService } from "@services/industry.service";
import { forkJoin, map, noop, Subscription } from "rxjs";
-import { ActivatedRoute, Router, RouterOutlet } from "@angular/router";
+import { ActivatedRoute, Router, RouterOutlet, RouterLink } from "@angular/router";
import { Invite } from "@models/invite.model";
import { AuthService } from "@auth/services";
-import { ProjectService } from "@services/project.service";
import { User } from "@auth/models/user.model";
import { ChatService } from "@services/chat.service";
import { SnackbarComponent } from "@ui/components/snackbar/snackbar.component";
@@ -14,10 +13,13 @@ import { DeleteConfirmComponent } from "@ui/components/delete-confirm/delete-con
import { ButtonComponent } from "@ui/components";
import { ModalComponent } from "@ui/components/modal/modal.component";
import { NavComponent } from "./features/nav/nav.component";
-import { IconComponent, ProfileControlPanelComponent, SidebarComponent } from "@uilib";
+import { ProfileControlPanelComponent, SidebarComponent } from "@uilib";
import { AsyncPipe } from "@angular/common";
import { InviteService } from "@services/invite.service";
import { toSignal } from "@angular/core/rxjs-interop";
+import { ProgramSidebarCardComponent } from "./features/program-sidebar-card/program-sidebar-card.component";
+import { ProgramService } from "./program/services/program.service";
+import { Program } from "./program/models/program.model";
/**
* Главный компонент офиса - корневой компонент рабочего пространства
@@ -45,8 +47,9 @@ import { toSignal } from "@angular/core/rxjs-interop";
DeleteConfirmComponent,
SnackbarComponent,
AsyncPipe,
+ RouterLink,
ProfileControlPanelComponent,
- IconComponent,
+ ProgramSidebarCardComponent,
],
})
export class OfficeComponent implements OnInit, OnDestroy {
@@ -56,7 +59,8 @@ export class OfficeComponent implements OnInit, OnDestroy {
public readonly authService: AuthService,
private readonly inviteService: InviteService,
private readonly router: Router,
- public readonly chatService: ChatService
+ public readonly chatService: ChatService,
+ private readonly programService: ProgramService
) {}
invites: Signal
= toSignal(
@@ -72,6 +76,7 @@ export class OfficeComponent implements OnInit, OnDestroy {
waitVerificationAccepted = false;
inviteErrorModal = false;
+ protected readonly programs = signal([]);
navItems: {
name: string;
@@ -121,6 +126,17 @@ export class OfficeComponent implements OnInit, OnDestroy {
if (localStorage.getItem("waitVerificationAccepted") === "true") {
this.waitVerificationAccepted = true;
}
+
+ const programsSub$ = this.programService.getActualPrograms().subscribe({
+ next: ({ results: programs }) => {
+ const resultPrograms = programs.filter(
+ (program: Program) => Date.now() < Date.parse(program.datetimeRegistrationEnds)
+ );
+ this.programs.set(resultPrograms.slice(0, 3));
+ },
+ });
+
+ this.subscriptions$.push(programsSub$);
}
ngOnDestroy(): void {
diff --git a/projects/social_platform/src/app/office/program/detail/list/list-all.resolver.ts b/projects/social_platform/src/app/office/program/detail/list/list-all.resolver.ts
index 5a7b30438..7b6db7737 100644
--- a/projects/social_platform/src/app/office/program/detail/list/list-all.resolver.ts
+++ b/projects/social_platform/src/app/office/program/detail/list/list-all.resolver.ts
@@ -1,5 +1,6 @@
/** @format */
+import { HttpParams } from "@angular/common/http";
import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, ResolveFn, Router } from "@angular/router";
import { ApiPagination } from "@office/models/api-pagination.model";
@@ -48,17 +49,22 @@ export const ListAllResolver: ResolveFn> = (
const projectRatingService = inject(ProjectRatingService);
const router = inject(Router);
- return projectRatingService.getAll(route.parent?.params["programId"], 0, 8).pipe(
- catchError(error => {
- if (error.status === 403) {
- router.navigate([], {
- queryParams: { access: "accessDenied" },
- queryParamsHandling: "merge",
- replaceUrl: true,
- });
- }
+ return projectRatingService
+ .getAll(
+ route.parent?.params["programId"],
+ new HttpParams({ fromObject: { offset: 0, limit: 8 } })
+ )
+ .pipe(
+ catchError(error => {
+ if (error.status === 403) {
+ router.navigate([], {
+ queryParams: { access: "accessDenied" },
+ queryParamsHandling: "merge",
+ replaceUrl: true,
+ });
+ }
- return EMPTY;
- })
- );
+ return EMPTY;
+ })
+ );
};
diff --git a/projects/social_platform/src/app/office/program/models/program.model.ts b/projects/social_platform/src/app/office/program/models/program.model.ts
index 8ace1b6f6..2ba3be164 100644
--- a/projects/social_platform/src/app/office/program/models/program.model.ts
+++ b/projects/social_platform/src/app/office/program/models/program.model.ts
@@ -50,11 +50,14 @@ export class Program {
datetimeRegistrationEnds!: string;
datetimeStarted!: string;
datetimeFinished!: string;
+ datetimeProjectSubmissionEnds!: string;
+ datetimeEvaluationEnds!: string;
viewsCount!: number;
likesCount!: number;
isUserLiked!: boolean;
isUserManager!: boolean;
isUserMember!: boolean;
+ publishProjectsAfterFinish!: boolean;
static default(): Program {
return {
@@ -73,6 +76,8 @@ export class Program {
datetimeRegistrationEnds: "",
datetimeStarted: "",
datetimeFinished: "",
+ datetimeProjectSubmissionEnds: "",
+ datetimeEvaluationEnds: "",
viewsCount: 1,
tag: "",
likesCount: 1,
@@ -80,6 +85,7 @@ export class Program {
isUserLiked: false,
isUserMember: false,
isUserManager: false,
+ publishProjectsAfterFinish: false,
};
}
}
diff --git a/projects/social_platform/src/app/office/program/services/program.service.ts b/projects/social_platform/src/app/office/program/services/program.service.ts
index f5e79ed12..f9e261853 100644
--- a/projects/social_platform/src/app/office/program/services/program.service.ts
+++ b/projects/social_platform/src/app/office/program/services/program.service.ts
@@ -10,6 +10,7 @@ import { Project } from "@models/project.model";
import { ApiPagination } from "@models/api-pagination.model";
import { User } from "@auth/models/user.model";
import { PartnerProgramFields } from "@office/models/partner-program-fields.model";
+import { ProjectAdditionalFields } from "@office/projects/models/project-additional-fields.model";
/**
* Сервис для работы с программами
@@ -67,6 +68,10 @@ export class ProgramService {
return this.apiService.get(`${this.PROGRAMS_URL}/`, httpParams);
}
+ getActualPrograms(): Observable> {
+ return this.apiService.get(`${this.PROGRAMS_URL}/`);
+ }
+
getOne(programId: number): Observable {
return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/`);
}
@@ -103,6 +108,15 @@ export class ProgramService {
return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/filters/`);
}
+ getProgramProjectAdditionalFields(programId: number): Observable {
+ return this.apiService.get(`${this.PROGRAMS_URL}/${programId}/projects/apply/`);
+ }
+
+ // body - это форма проекта который подается + programFieldValues
+ applyProjectToProgram(programId: number, body: any): Observable {
+ return this.apiService.post(`${this.PROGRAMS_URL}/${programId}/projects/apply/`, body);
+ }
+
createProgramFilters(
programId: number,
filters: Record,
diff --git a/projects/social_platform/src/app/office/program/shared/program-card/program-card.component.html b/projects/social_platform/src/app/office/program/shared/program-card/program-card.component.html
index 7f8e20098..4c6a029f2 100644
--- a/projects/social_platform/src/app/office/program/shared/program-card/program-card.component.html
+++ b/projects/social_platform/src/app/office/program/shared/program-card/program-card.component.html
@@ -20,7 +20,7 @@
{{
!registerDateExpired && (!program.isUserMember || !program.isUserManager)
- ? "Подача проектов до:" + " " + (program.datetimeRegistrationEnds | date: "dd MMMM")
+ ? "Регистрация до:" + " " + (program.datetimeRegistrationEnds | date: "dd MMMM")
: registerDateExpired
? "регистрация завершена"
: "ты уже участвуешь!"
diff --git a/projects/social_platform/src/app/office/projects/edit/edit.component.ts b/projects/social_platform/src/app/office/projects/edit/edit.component.ts
index c59b20aa5..af4f3bf91 100644
--- a/projects/social_platform/src/app/office/projects/edit/edit.component.ts
+++ b/projects/social_platform/src/app/office/projects/edit/edit.component.ts
@@ -47,15 +47,13 @@ import { ProjectTeamService } from "./services/project-team.service";
import { ProjectAdditionalStepComponent } from "./shared/project-additional-step/project-additional-step.component";
import { ProjectAdditionalService } from "./services/project-additional.service";
import { ProjectAchievementsService } from "./services/project-achievements.service";
-import { Goal, GoalPostForm } from "@office/models/goals.model";
+import { Goal } from "@office/models/goals.model";
import { ProjectGoalService } from "./services/project-goals.service";
import { SnackbarService } from "@ui/services/snackbar.service";
-import { Resource, ResourcePostForm } from "@office/models/resource.model";
+import { Resource } from "@office/models/resource.model";
import { Partner } from "@office/models/partner.model";
import { ProjectPartnerService } from "./services/project-partner.service";
import { ProjectResourceService } from "./services/project-resources.service";
-import { HttpErrorResponse } from "@angular/common/http";
-import { ProjectAssign } from "../models/project-assign.model";
/**
* Компонент редактирования проекта
@@ -120,6 +118,7 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy {
private readonly snackBarService: SnackbarService,
private readonly skillsService: SkillsService,
private readonly projectAdditionalService: ProjectAdditionalService,
+ private readonly programService: ProgramService,
private readonly projectGoalService: ProjectGoalService
) {}
@@ -175,9 +174,6 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy {
this.projectAdditionalService.clearAssignProjectToProgramError();
}
- // Сигналы для работы с модальными окнами с текстом
- assignProjectToProgramModalMessage = signal(null);
-
// Геттеры для работы с целями
get goals(): FormArray {
return this.projectGoalsService.goals;
@@ -286,32 +282,32 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy {
this.projectStepService.navigateToStep(step);
}
- /**
- * Привязка проекта к программе выбранной
- * Перенаправление её на редактирование "нового" проекта
- */
- assignProjectToProgram(): void {
- this.projectService
- .assignProjectToProgram(
- Number(this.route.snapshot.paramMap.get("projectId")),
- this.projectForm.get("partnerProgramId")?.value
- )
- .subscribe({
- next: r => {
- this.assignProjectToProgramModalMessage.set(r);
- this.isAssignProjectToProgramModalOpen.set(true);
- this.router.navigateByUrl(`/office/projects/${r.newProjectId}/edit?editingStep=main`);
- },
-
- error: err => {
- if (err instanceof HttpErrorResponse) {
- if (err.status === 400) {
- this.setAssignProjectToProgramError(err.error);
- }
- }
- },
- });
- }
+ // /**
+ // * Привязка проекта к программе выбранной
+ // * Перенаправление её на редактирование "нового" проекта
+ // */
+ // assignProjectToProgram(): void {
+ // this.projectService
+ // .assignProjectToProgram(
+ // Number(this.route.snapshot.paramMap.get("projectId")),
+ // this.projectForm.get("partnerProgramId")?.value
+ // )
+ // .subscribe({
+ // next: r => {
+ // this.assignProjectToProgramModalMessage.set(r);
+ // this.isAssignProjectToProgramModalOpen.set(true);
+ // this.router.navigateByUrl(`/office/projects/${r.newProjectId}/edit?editingStep=main`);
+ // },
+
+ // error: err => {
+ // if (err instanceof HttpErrorResponse) {
+ // if (err.status === 400) {
+ // this.setAssignProjectToProgramError(err.error);
+ // }
+ // }
+ // },
+ // });
+ // }
// Методы для управления состоянием отправки форм
setIsSubmittingAsPublished(status: boolean): void {
@@ -352,9 +348,15 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
+ const programId = this.projectForm.get("partnerProgramId")?.value;
+
this.projectService.remove(Number(this.route.snapshot.paramMap.get("projectId"))).subscribe({
next: () => {
- this.router.navigateByUrl(`/office/projects/my`);
+ if (this.fromProgram) {
+ this.router.navigateByUrl(`/office/program/${programId}`);
+ } else {
+ this.router.navigateByUrl(`/office/projects/my`);
+ }
},
});
}
@@ -566,7 +568,6 @@ export class ProjectEditComponent implements OnInit, AfterViewInit, OnDestroy {
*/
private sendAdditionalFields(projectId: number, relationId: number): void {
const isDraft = this.projectForm.get("draft")?.value === true;
-
this.projectAdditionalService.sendAdditionalFieldsValues(projectId).subscribe({
next: () => {
if (!isDraft) {
diff --git a/projects/social_platform/src/app/office/projects/edit/services/project-additional.service.ts b/projects/social_platform/src/app/office/projects/edit/services/project-additional.service.ts
index 5a83bfb54..8b27ae100 100644
--- a/projects/social_platform/src/app/office/projects/edit/services/project-additional.service.ts
+++ b/projects/social_platform/src/app/office/projects/edit/services/project-additional.service.ts
@@ -242,7 +242,7 @@ export class ProjectAdditionalService {
control.addValidators([Validators.maxLength(500)]);
break;
case "textarea":
- control.addValidators([Validators.maxLength(300)]);
+ control.addValidators([Validators.maxLength(500)]);
break;
}
}
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 43183b3d1..ceaeae0ad 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,6 +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 { optionalUrlOrMentionValidator } from "@utils/optionalUrl.validator";
import { stripNullish } from "@utils/stripNull";
import { concatMap, filter } from "rxjs";
/**
@@ -33,6 +34,29 @@ export class ProjectFormService {
this.initializeForm();
}
+ formModel = (this.projectForm = this.fb.group({
+ imageAddress: [""],
+ name: ["", [Validators.required, Validators.maxLength(256)]],
+ region: ["", [Validators.required, Validators.maxLength(256)]],
+ implementationDeadline: [null],
+ trl: [null],
+ links: this.fb.array([]),
+ link: ["", optionalUrlOrMentionValidator],
+ industryId: [undefined],
+ description: ["", [Validators.maxLength(800)]],
+ presentationAddress: [""],
+ coverImageAddress: [""],
+ actuality: ["", [Validators.maxLength(1000)]],
+ targetAudience: ["", [Validators.maxLength(500)]],
+ problem: ["", [Validators.maxLength(1000)]],
+ partnerProgramId: [null],
+ achievements: this.fb.array([]),
+ title: [""],
+ status: [""],
+
+ draft: [null],
+ }));
+
/**
* Создает и настраивает основную форму проекта с набором контролов и валидаторов.
* Подписывается на изменения полей 'presentationAddress' и 'coverImageAddress' для автосохранения при очищении.
@@ -40,19 +64,19 @@ export class ProjectFormService {
private initializeForm(): void {
this.projectForm = this.fb.group({
imageAddress: [""],
- name: ["", [Validators.required]],
- region: ["", [Validators.required]],
+ name: ["", [Validators.required, Validators.maxLength(256)]],
+ region: ["", [Validators.required, Validators.maxLength(256)]],
implementationDeadline: [null],
trl: [null],
links: this.fb.array([]),
- link: ["", Validators.pattern(/^(https?:\/\/)/)],
- industryId: [undefined, [Validators.required]],
- description: ["", [Validators.required, Validators.minLength(0), Validators.maxLength(800)]],
+ link: ["", optionalUrlOrMentionValidator],
+ industryId: [undefined],
+ description: ["", [Validators.maxLength(800)]],
presentationAddress: [""],
- coverImageAddress: ["", [Validators.required]],
- actuality: ["", [Validators.maxLength(400)]],
- targetAudience: ["", [Validators.required, Validators.maxLength(400)]],
- problem: ["", [Validators.required, Validators.maxLength(400)]],
+ coverImageAddress: [""],
+ actuality: ["", [Validators.maxLength(1000)]],
+ targetAudience: ["", [Validators.maxLength(500)]],
+ problem: ["", [Validators.maxLength(1000)]],
partnerProgramId: [null],
achievements: this.fb.array([]),
title: [""],
@@ -130,7 +154,7 @@ export class ProjectFormService {
}
links.forEach(link => {
- linksFormArray.push(this.fb.control(link, [Validators.required]));
+ linksFormArray.push(this.fb.control(link, optionalUrlOrMentionValidator));
});
}
@@ -193,7 +217,13 @@ export class ProjectFormService {
* @returns объект значений формы без nullish
*/
public getFormValue(): any {
- return stripNullish(this.projectForm.value);
+ const value = stripNullish(this.projectForm.value);
+
+ if (Array.isArray(value["links"])) {
+ value["links"] = value["links"].map((v: string) => v?.trim()).filter((v: string) => !!v);
+ }
+
+ return value;
}
// Геттеры для быстрого доступа к контролам основной формы
diff --git a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html
index 7a7b83302..293f58894 100644
--- a/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html
+++ b/projects/social_platform/src/app/office/projects/edit/shared/project-main-step/project-main-step.component.html
@@ -415,7 +415,7 @@
@if (hasLinks) {
- @for (control of linksArray.controls; track trackByIndex($index)) {
+ @for (control of links.controls; track trackByIndex($index)) {
-