105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
import { HttpEventType } from '@angular/common/http';
|
||
import { computed, Injectable, inject, signal } from '@angular/core';
|
||
import { finalize } from 'rxjs';
|
||
import { FileDownloadService } from './file-download.service';
|
||
import { TenantAttachment, TenantService } from './tenant.service';
|
||
|
||
@Injectable({ providedIn: 'root' })
|
||
export class TenantAttachmentTransferService {
|
||
private readonly tenantService = inject(TenantService);
|
||
private readonly downloads = inject(FileDownloadService);
|
||
readonly errors = signal<Record<string, string>>({});
|
||
readonly uploadingTenantIds = signal<ReadonlySet<string>>(new Set());
|
||
readonly uploadProgress = signal<Record<string, number | null>>({});
|
||
readonly downloadingIds = computed<ReadonlySet<string>>(() => {
|
||
const prefix = 'attachment:';
|
||
return new Set(
|
||
[...this.downloads.pendingKeys()]
|
||
.filter((key) => key.startsWith(prefix))
|
||
.map((key) => key.slice(prefix.length)),
|
||
);
|
||
});
|
||
readonly downloadErrors = computed<Record<string, string>>(() => {
|
||
const prefix = 'attachment:';
|
||
return Object.fromEntries(
|
||
Object.entries(this.downloads.errors())
|
||
.filter(([key]) => key.startsWith(prefix))
|
||
.map(([key, error]) => [key.slice(prefix.length), error]),
|
||
);
|
||
});
|
||
|
||
upload(tenantId: string, file: File, onUploaded: (attachment: TenantAttachment) => void): void {
|
||
if (this.uploadingTenantIds().has(tenantId)) return;
|
||
this.errors.update((errors) => this.withoutKey(errors, tenantId));
|
||
this.uploadingTenantIds.update((ids) => new Set(ids).add(tenantId));
|
||
this.uploadProgress.update((progress) => ({ ...progress, [tenantId]: null }));
|
||
this.tenantService
|
||
.uploadAttachment(tenantId, file)
|
||
.pipe(
|
||
finalize(() => {
|
||
this.uploadingTenantIds.update((ids) => this.without(ids, tenantId));
|
||
this.uploadProgress.update((progress) => this.withoutKey(progress, tenantId));
|
||
}),
|
||
)
|
||
.subscribe({
|
||
next: (event) => {
|
||
if (event.type === HttpEventType.UploadProgress) {
|
||
const percent = event.total ? Math.round((event.loaded / event.total) * 100) : null;
|
||
this.uploadProgress.update((progress) => ({ ...progress, [tenantId]: percent }));
|
||
} else if (event.type === HttpEventType.Response && event.body) {
|
||
onUploaded(event.body);
|
||
}
|
||
},
|
||
error: (error: unknown) => {
|
||
const message = this.errorMessage(error, 'Impossible d’envoyer cette pièce jointe.');
|
||
this.errors.update((errors) => ({ ...errors, [tenantId]: message }));
|
||
},
|
||
});
|
||
}
|
||
|
||
download(tenantId: string, attachment: { id: string; fileName: string }): void {
|
||
this.downloads.download(
|
||
this.downloadKey(attachment.id),
|
||
this.tenantService.downloadAttachment(tenantId, attachment.id),
|
||
attachment.fileName,
|
||
'Impossible de télécharger cette pièce jointe.',
|
||
);
|
||
}
|
||
|
||
downloading(attachmentId: string): boolean {
|
||
return this.downloads.isPending(this.downloadKey(attachmentId));
|
||
}
|
||
|
||
downloadError(attachmentId: string): string {
|
||
return this.downloads.error(this.downloadKey(attachmentId));
|
||
}
|
||
|
||
private downloadKey(attachmentId: string): string {
|
||
return `attachment:${attachmentId}`;
|
||
}
|
||
|
||
private without(values: ReadonlySet<string>, value: string): ReadonlySet<string> {
|
||
const next = new Set(values);
|
||
next.delete(value);
|
||
return next;
|
||
}
|
||
|
||
private withoutKey<T>(record: Record<string, T>, key: string): Record<string, T> {
|
||
const next = { ...record };
|
||
delete next[key];
|
||
return next;
|
||
}
|
||
|
||
private errorMessage(error: unknown, fallback: string): string {
|
||
return typeof error === 'object' &&
|
||
error !== null &&
|
||
'error' in error &&
|
||
typeof error.error === 'object' &&
|
||
error.error !== null &&
|
||
'message' in error.error &&
|
||
typeof error.error.message === 'string'
|
||
? error.error.message
|
||
: fallback;
|
||
}
|
||
}
|