Skip to content

Commit 929e44c

Browse files
authored
Merge pull request #249 from PROCOLLAB-github/fixes
Export file of projects, rates of program
2 parents 91f69ec + 7f401c3 commit 929e44c

16 files changed

Lines changed: 307 additions & 85 deletions

File tree

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"class-transformer": "^0.5.1",
4747
"core-js": "^3.23.4",
4848
"dayjs": "^1.11.3",
49+
"file-saver": "^2.0.5",
4950
"fuse.js": "^6.6.2",
5051
"js-base64": "^3.7.7",
5152
"js-cookie": "^3.0.5",

projects/core/src/lib/interceptors/bearer-token.interceptor.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ export class BearerTokenInterceptor implements HttpInterceptor {
4646
*/
4747
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
4848
// Базовые заголовки для всех запросов
49-
const headers: Record<string, string> = {
50-
Accept: "application/json",
51-
};
49+
const headers: Record<string, string> = {};
5250

5351
const tokens = this.tokenService.getTokens();
5452

@@ -57,14 +55,24 @@ export class BearerTokenInterceptor implements HttpInterceptor {
5755
headers["Authorization"] = `Bearer ${tokens.access}`;
5856
}
5957

58+
// Для blob запросов (файлы) не устанавливаем Accept, чтобы не парсить blob как JSON
59+
const isBlobRequest =
60+
request.url.includes("/export") ||
61+
request.url.includes("/download") ||
62+
(request.headers.has("X-Request-Type") && request.headers.get("X-Request-Type") === "blob");
63+
64+
const hasAcceptHeader = request.headers.has("Accept");
65+
66+
if (!isBlobRequest && !hasAcceptHeader) {
67+
headers["Accept"] = "application/json";
68+
}
69+
6070
const req = request.clone({ setHeaders: headers });
6171

62-
// Если токены есть, обрабатываем запрос с возможностью обновления токенов
6372
if (tokens !== null) {
6473
return this.handleRequestWithTokens(req, next);
6574
} else {
66-
// Если токенов нет, просто выполняем запрос
67-
return next.handle(request);
75+
return next.handle(req);
6876
}
6977
}
7078

@@ -107,10 +115,9 @@ export class BearerTokenInterceptor implements HttpInterceptor {
107115
request: HttpRequest<unknown>,
108116
next: HttpHandler
109117
): Observable<HttpEvent<unknown>> {
110-
// Если токен еще не обновляется, начинаем процесс обновления
111118
if (!this.isRefreshing) {
112119
this.isRefreshing = true;
113-
this.refreshTokenSubject.next(null); // Сбрасываем subject
120+
this.refreshTokenSubject.next(null);
114121

115122
return this.tokenService.refreshTokens().pipe(
116123
catchError(err => {
@@ -119,22 +126,29 @@ export class BearerTokenInterceptor implements HttpInterceptor {
119126
}),
120127
switchMap(res => {
121128
this.isRefreshing = false;
122-
this.refreshTokenSubject.next(res.access); // Уведомляем о новом токене
129+
this.refreshTokenSubject.next(res.access);
123130

124-
// Сохраняем новые токены в хранилище
125131
this.tokenService.memTokens(res);
126132

127-
// Подготавливаем заголовки с новым токеном
128-
const headers: Record<string, string> = {
129-
Accept: "application/json",
130-
};
133+
const headers: Record<string, string> = {};
131134

132135
const tokens = this.tokenService.getTokens();
133136
if (tokens) {
134137
headers["Authorization"] = `Bearer ${tokens.access}`;
135138
}
136139

137-
// Повторяем исходный запрос с новым токеном
140+
const isBlobRequest =
141+
request.url.includes("/export") ||
142+
request.url.includes("/download") ||
143+
(request.headers.has("X-Request-Type") &&
144+
request.headers.get("X-Request-Type") === "blob");
145+
146+
const hasAcceptHeader = request.headers.has("Accept");
147+
148+
if (!isBlobRequest && !hasAcceptHeader) {
149+
headers["Accept"] = "application/json";
150+
}
151+
138152
return next.handle(
139153
request.clone({
140154
setHeaders: headers,
@@ -144,17 +158,32 @@ export class BearerTokenInterceptor implements HttpInterceptor {
144158
);
145159
}
146160

147-
// Если токен уже обновляется, ждем завершения процесса
148161
return this.refreshTokenSubject.pipe(
149-
filter(token => token !== null), // Ждем получения нового токена
150-
take(1), // Берем только первое значение
151-
switchMap(token =>
152-
next.handle(
162+
filter(token => token !== null),
163+
take(1),
164+
switchMap(token => {
165+
const headers: Record<string, string> = {
166+
Authorization: `Bearer ${token}`,
167+
};
168+
169+
const isBlobRequest =
170+
request.url.includes("/export") ||
171+
request.url.includes("/download") ||
172+
(request.headers.has("X-Request-Type") &&
173+
request.headers.get("X-Request-Type") === "blob");
174+
175+
const hasAcceptHeader = request.headers.has("Accept");
176+
177+
if (!isBlobRequest && !hasAcceptHeader) {
178+
headers["Accept"] = "application/json";
179+
}
180+
181+
return next.handle(
153182
request.clone({
154-
setHeaders: { Authorization: `Bearer ${token}` },
183+
setHeaders: headers,
155184
})
156-
)
157-
)
185+
);
186+
})
158187
);
159188
}
160189
}

projects/core/src/lib/interceptors/camelcase.interceptor.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,34 @@ export class CamelcaseInterceptor implements HttpInterceptor {
5454
}),
5555
});
5656
} else {
57-
// Если тела нет, просто клонируем запрос
5857
req = request.clone();
5958
}
6059

6160
// Выполняем запрос и обрабатываем ответ
6261
return next.handle(req).pipe(
6362
map((event: HttpEvent<any>) => {
64-
// Обрабатываем только HTTP ответы (не события загрузки и т.д.)
6563
if (event instanceof HttpResponse) {
64+
if (event.body instanceof Blob) {
65+
return event;
66+
}
67+
68+
if (typeof event.body !== "object" || event.body === null) {
69+
return event;
70+
}
71+
6672
// Клонируем ответ с преобразованным телом (snake_case → camelCase)
67-
return event.clone({
68-
body: camelcaseKeys(event.body, {
69-
deep: true, // Рекурсивное преобразование вложенных объектов
70-
}),
71-
});
73+
try {
74+
return event.clone({
75+
body: camelcaseKeys(event.body, {
76+
deep: true, // Рекурсивное преобразование вложенных объектов
77+
}),
78+
});
79+
} catch (error) {
80+
console.warn("CamelcaseInterceptor: Failed to transform response body", error);
81+
return event;
82+
}
7283
}
7384

74-
// Для других типов событий возвращаем как есть
7585
return event;
7686
})
7787
);

projects/core/src/lib/services/api.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ export class ApiService {
4444
return this.http.get(this.apiUrl + path, { params, ...options }).pipe(first()) as Observable<T>;
4545
}
4646

47+
getFile(path: string, params?: HttpParams): Observable<Blob> {
48+
return this.http
49+
.get(this.apiUrl + path, {
50+
params,
51+
responseType: "blob",
52+
})
53+
.pipe(first()) as Observable<Blob>;
54+
}
55+
4756
/**
4857
* Выполняет PUT запрос к API (полное обновление ресурса)
4958
* @param path - Относительный путь к ресурсу

projects/social_platform/src/app/auth/services/auth.service.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,8 @@ export class AuthService {
7676
.pipe(map(json => plainToInstance(RegisterResponse, json)));
7777
}
7878

79-
/**
80-
* Отправить резюме по email
81-
* @returns Observable завершения операции
82-
*/
83-
sendCV(): Observable<any> {
84-
return this.apiService.get(`${this.AUTH_USERS_URL}/send_mail_cv/`);
79+
downloadCV() {
80+
return this.apiService.getFile(`${this.AUTH_USERS_URL}/download_cv/`);
8581
}
8682

8783
/** Поток данных профиля пользователя */

projects/social_platform/src/app/office/features/detail/detail.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -545,10 +545,9 @@ <h3 class="text-body-14 cancel__title" style="flex-wrap: wrap; width: 90%">
545545

546546
@if (+profile.id === +info().id) {
547547
<app-button
548-
(click)="sendCVEmail()"
548+
(click)="downloadCV()"
549549
size="medium"
550-
style="opacity: 0, 5"
551-
[disabled]="true"
550+
[loader]="isSended"
552551
appearance="outline"
553552
customTypographyClass="text-body-12"
554553
>cкачать CV</app-button
@@ -645,18 +644,19 @@ <h3 class="text-body-12 lists__title">подтвердить владение н
645644
<i
646645
(click)="isDelayModalOpen = false"
647646
appIcon
647+
appSquare="8"
648648
icon="cross"
649649
class="cancel__cross"
650650
></i>
651651
<p class="cancel__title text-bold-body-16">Повторите загрузку позже</p>
652652
</div>
653653
<p class="text-body-14 cancel__text">
654-
Скачивание будет доступно через {{ errorMessageModal() }} секунд.
654+
Скачивание будет доступно через несколько секунд.
655655
</p>
656656
</div>
657657
</app-modal>
658658

659-
<app-modal [open]="isSended" (openChange)="isSended = !isSended">
659+
<!-- <app-modal [open]="isSended" (openChange)="isSended = !isSended">
660660
<div class="cancel">
661661
<div class="cancel__top">
662662
<i
@@ -673,7 +673,7 @@ <h3 class="text-body-12 lists__title">подтвердить владение н
673673
Технические сложности? Мы всегда на связи в Telegram — {{ "@procollab_support" }}
674674
</p>
675675
</div>
676-
</app-modal>
676+
</app-modal> -->
677677
</ng-template>
678678

679679
<router-outlet></router-outlet>

projects/social_platform/src/app/office/features/detail/detail.component.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import { CommonModule, Location } from "@angular/common";
44
import { ChangeDetectorRef, Component, inject, OnDestroy, OnInit, signal } from "@angular/core";
5-
import { ButtonComponent, InputComponent } from "@ui/components";
6-
import { BackComponent, IconComponent } from "@uilib";
5+
import { ButtonComponent } from "@ui/components";
6+
import { IconComponent } from "@uilib";
77
import { ModalComponent } from "@ui/components/modal/modal.component";
88
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
99
import { AuthService } from "@auth/services";
@@ -23,15 +23,13 @@ import { ProfileDataService } from "@office/profile/detail/services/profile-date
2323
import { ProfileService } from "projects/skills/src/app/profile/services/profile.service";
2424
import { SnackbarService } from "@ui/services/snackbar.service";
2525
import { ApproveSkillComponent } from "../approve-skill/approve-skill.component";
26-
import { ProjectsService } from "@office/projects/services/projects.service";
27-
import { TruncatePipe } from "projects/core/src/lib/pipes/truncate.pipe";
2826
import { ProgramService } from "@office/program/services/program.service";
2927
import { ProjectFormService } from "@office/projects/edit/services/project-form.service";
3028
import {
3129
PartnerProgramFields,
3230
projectNewAdditionalProgramVields,
3331
} from "@office/models/partner-program-fields.model";
34-
import { HttpRequest, HttpResponse } from "@angular/common/http";
32+
import { saveFile } from "@utils/helpers/export-file";
3533

3634
@Component({
3735
selector: "app-detail",
@@ -42,13 +40,10 @@ import { HttpRequest, HttpResponse } from "@angular/common/http";
4240
RouterModule,
4341
IconComponent,
4442
ButtonComponent,
45-
BackComponent,
4643
ModalComponent,
4744
AvatarComponent,
4845
TooltipComponent,
49-
InputComponent,
5046
ApproveSkillComponent,
51-
TruncatePipe,
5247
],
5348
standalone: true,
5449
})
@@ -64,7 +59,6 @@ export class DeatilComponent implements OnInit, OnDestroy {
6459
private readonly location = inject(Location);
6560
private readonly profileDataService = inject(ProfileDataService);
6661
public readonly skillsProfileService = inject(ProfileService);
67-
private readonly projectsService = inject(ProjectsService);
6862
public readonly chatService = inject(ChatService);
6963
private readonly cdRef = inject(ChangeDetectorRef);
7064
private readonly programService = inject(ProgramService);
@@ -388,15 +382,17 @@ export class DeatilComponent implements OnInit, OnDestroy {
388382
* Отправка CV пользователя на email
389383
* Проверяет ограничения по времени и отправляет CV на почту пользователя
390384
*/
391-
sendCVEmail() {
392-
this.authService.sendCV().subscribe({
393-
next: () => {
394-
this.isSended = true;
385+
downloadCV() {
386+
this.isSended = true;
387+
this.authService.downloadCV().subscribe({
388+
next: blob => {
389+
saveFile(blob, "cv", this.profile?.firstName + " " + this.profile?.lastName);
390+
this.isSended = false;
395391
},
396392
error: err => {
393+
this.isSended = false;
397394
if (err.status === 400) {
398395
this.isDelayModalOpen = true;
399-
this.errorMessageModal.set(err.error.seconds_after_retry);
400396
}
401397
},
402398
});

0 commit comments

Comments
 (0)