diff --git a/packages/backend/src/_common/model/app-log/LogAction.type.ts b/packages/backend/src/_common/model/app-log/LogAction.type.ts index f503934cdc..ada19650b5 100644 --- a/packages/backend/src/_common/model/app-log/LogAction.type.ts +++ b/packages/backend/src/_common/model/app-log/LogAction.type.ts @@ -1,7 +1,12 @@ export type LogAction = + // Docs | "USAGERS_DOCS_UPLOAD" | "USAGERS_DOCS_DOWNLOAD" | "USAGERS_DOCS_DELETE" + | "USAGERS_DOCS_RENAME" + | "USAGERS_DOCS_SHARED" + + /// | "EXPORT_USAGERS" | "GET_STATS" | "EXPORT_STATS" diff --git a/packages/backend/src/_common/model/usager/UsagerDoc.type.ts b/packages/backend/src/_common/model/usager/UsagerDoc.type.ts deleted file mode 100644 index 9a54cf186b..0000000000 --- a/packages/backend/src/_common/model/usager/UsagerDoc.type.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AppEntity } from "../_core"; - -export type UsagerDoc = AppEntity & { - createdAt?: Date; - createdBy: string; - label: string; - filetype: string; - usagerUUID: string; - path: string; - structureId: number; - usagerRef: number; - encryptionContext?: string; - encryptionVersion?: number; -}; diff --git a/packages/backend/src/_common/model/usager/index.ts b/packages/backend/src/_common/model/usager/index.ts index c2966d8610..70568a1033 100644 --- a/packages/backend/src/_common/model/usager/index.ts +++ b/packages/backend/src/_common/model/usager/index.ts @@ -1,6 +1,5 @@ //@index('./*', f => `export * from '${f.path}'`) export * from "./cerfa"; export * from "./history"; -export * from "./UsagerDoc.type"; export * from "./UsagerImport.type"; export * from "./UsagerLight.type"; diff --git a/packages/backend/src/_migrations/1730216743164-auto-migration.ts b/packages/backend/src/_migrations/1730216743164-auto-migration.ts new file mode 100644 index 0000000000..b200db2840 --- /dev/null +++ b/packages/backend/src/_migrations/1730216743164-auto-migration.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { domifaConfig } from "../config"; + +export class AutoMigration1730216743164 implements MigrationInterface { + name = "AutoMigration1730216743164"; + + public async up(queryRunner: QueryRunner): Promise { + if ( + domifaConfig().envId === "prod" || + domifaConfig().envId === "preprod" || + domifaConfig().envId === "local" + ) { + await queryRunner.query( + `ALTER TABLE "usager_docs" ADD "shared" boolean NOT NULL DEFAULT false` + ); + + await queryRunner.query( + `UPDATE "structure_doc" SET label = 'Attestation postale' where label='attestation_postale'` + ); + await queryRunner.query( + `UPDATE "structure_doc" SET label = 'Courrier de radiation' where label='courrier_radiation'` + ); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "usager_docs" DROP COLUMN "shared"`); + } +} diff --git a/packages/backend/src/auth/decorators/current-usager-doc.decorator.ts b/packages/backend/src/auth/decorators/current-usager-doc.decorator.ts new file mode 100644 index 0000000000..4fd5903172 --- /dev/null +++ b/packages/backend/src/auth/decorators/current-usager-doc.decorator.ts @@ -0,0 +1,9 @@ +import { UsagerDoc } from "@domifa/common"; +import { createParamDecorator, ExecutionContext } from "@nestjs/common"; + +export const CurrentUsagerDoc = createParamDecorator( + (_data: unknown, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return request.usagerDoc as UsagerDoc; + } +); diff --git a/packages/backend/src/auth/decorators/index.ts b/packages/backend/src/auth/decorators/index.ts index ed81022c54..3637e6df40 100644 --- a/packages/backend/src/auth/decorators/index.ts +++ b/packages/backend/src/auth/decorators/index.ts @@ -4,6 +4,7 @@ export * from "./AllowUserStructureRoles.decorator"; export * from "./current-chosen-user-structure.decorator"; export * from "./current-interaction.decorator"; export * from "./current-structure-information.decorator"; +export * from "./current-usager-doc.decorator"; export * from "./current-usager-note.decorator"; export * from "./current-usager.decorator"; export * from "./current-user.decorator"; diff --git a/packages/backend/src/auth/guards/CanGetUserStructure.guard.ts b/packages/backend/src/auth/guards/CanGetUserStructure.guard.ts index 0577edf721..c18a7c64b2 100644 --- a/packages/backend/src/auth/guards/CanGetUserStructure.guard.ts +++ b/packages/backend/src/auth/guards/CanGetUserStructure.guard.ts @@ -24,7 +24,7 @@ export class CanGetUserStructureGuard implements CanActivate { !isUUID(userUuid) ) { appLogger.error( - `[CanGetUserStructureGuard] invalid user.Uuid or structureId`, + "[CanGetUserStructureGuard] invalid user.Uuid or structureId", { sentry: true, context: { "user.Uuid": userUuid, structureId, user: r.user._id }, @@ -39,7 +39,7 @@ export class CanGetUserStructureGuard implements CanActivate { if (!chosenUserStructure) { appLogger.error( - `[CanGetUserStructureGuard] chosenUserStructure not found`, + "[CanGetUserStructureGuard] chosenUserStructure not found", { sentry: true, context: { diff --git a/packages/backend/src/auth/guards/index.ts b/packages/backend/src/auth/guards/index.ts index c4cfeb6b65..5df1171ec7 100644 --- a/packages/backend/src/auth/guards/index.ts +++ b/packages/backend/src/auth/guards/index.ts @@ -4,4 +4,5 @@ export * from "./CanGetUserStructure.guard"; export * from "./interactions.guard"; export * from "./structure-information-access.guard"; export * from "./usager-access.guard"; +export * from "./usager-doc-access.guard"; export * from "./usager-note-access.guard"; diff --git a/packages/backend/src/auth/guards/usager-access.guard.ts b/packages/backend/src/auth/guards/usager-access.guard.ts index 73d37f2421..af1bef178a 100644 --- a/packages/backend/src/auth/guards/usager-access.guard.ts +++ b/packages/backend/src/auth/guards/usager-access.guard.ts @@ -1,4 +1,3 @@ -import { usagerRepository } from "./../../database/services/usager/usagerRepository.service"; import { CanActivate, ExecutionContext, @@ -8,6 +7,7 @@ import { } from "@nestjs/common"; import { appLogger } from "../../util"; +import { usagerRepository } from "../../database"; @Injectable() export class UsagerAccessGuard implements CanActivate { @@ -18,7 +18,7 @@ export class UsagerAccessGuard implements CanActivate { typeof r.params.usagerRef === "undefined" || typeof r.user.structureId === "undefined" ) { - appLogger.error(`[UsagerAccessGuard] invalid usagerRef or structureId`, { + appLogger.error("[UsagerAccessGuard] invalid usagerRef or structureId", { sentry: true, context: { usagerRef: r.params.usagerRef, diff --git a/packages/backend/src/auth/guards/usager-doc-access.guard.ts b/packages/backend/src/auth/guards/usager-doc-access.guard.ts new file mode 100644 index 0000000000..be8cda8da1 --- /dev/null +++ b/packages/backend/src/auth/guards/usager-doc-access.guard.ts @@ -0,0 +1,62 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, +} from "@nestjs/common"; + +import { appLogger } from "../../util"; +import { isNumber, isUUID } from "class-validator"; +import { usagerDocsRepository } from "../../database"; +import { UserStructureAuthenticated } from "../../_common/model"; + +@Injectable() +export class UsagerDocAccessGuard implements CanActivate { + public async canActivate(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + const user = request?.user as UserStructureAuthenticated; + + if ( + user?.role === "simple" || + !isUUID(request.params.docUuid) || + !isNumber(user?.structureId) + ) { + appLogger.error("[UsagerDocAccessGuard] invalid docUuid or structureId", { + sentry: true, + context: { + docUuid: request?.params?.docUuid, + structureId: user.structureId, + user: user.id, + role: user.role, + }, + }); + throw new HttpException("USAGER_DOC_NOT_FOUND", HttpStatus.BAD_REQUEST); + } + + const docUuid = request.params.docUuid; + + try { + // Todo: optimize this request, generate one request with a join + const usagerDoc = await usagerDocsRepository.findOneOrFail({ + where: { + structureId: user.structureId, + uuid: docUuid, + }, + }); + request.usagerDoc = usagerDoc; + return request; + } catch (e) { + appLogger.error("[UsagerDocAccessGuard] usager not found", { + sentry: true, + context: { + docUuid: request.params.docUuid, + structureId: user.structureId, + user: user.id, + role: user.role, + }, + }); + throw new HttpException("USAGER_DOC_NOT_FOUND", HttpStatus.BAD_REQUEST); + } + } +} diff --git a/packages/backend/src/database/entities/usager/UsagerDocsTable.typeorm.ts b/packages/backend/src/database/entities/usager/UsagerDocsTable.typeorm.ts index ce64f7dc7a..de6f1a5ec8 100644 --- a/packages/backend/src/database/entities/usager/UsagerDocsTable.typeorm.ts +++ b/packages/backend/src/database/entities/usager/UsagerDocsTable.typeorm.ts @@ -1,11 +1,9 @@ -import { UsagerDoc } from "../../../_common/model/usager/UsagerDoc.type"; import { Column, Entity, Index, JoinColumn, ManyToOne } from "typeorm"; - import { AppTypeormTable } from "../_core/AppTypeormTable.typeorm"; import { StructureTable } from "../structure"; import { UsagerTable } from "./UsagerTable.typeorm"; +import { UsagerDoc } from "@domifa/common"; -// https://typeorm.io/#/entities/column-types-for-postgres @Entity({ name: "usager_docs" }) export class UsagerDocsTable extends AppTypeormTable @@ -48,6 +46,9 @@ export class UsagerDocsTable @Column({ type: "integer", nullable: true }) public encryptionVersion: number; + @Column({ type: "boolean", default: false }) + public shared: boolean; + public constructor(entity?: Partial) { super(entity); Object.assign(this, entity); diff --git a/packages/backend/src/database/services/usager/usagerDocsRepository.service.ts b/packages/backend/src/database/services/usager/usagerDocsRepository.service.ts index c280876104..18bc1b2459 100644 --- a/packages/backend/src/database/services/usager/usagerDocsRepository.service.ts +++ b/packages/backend/src/database/services/usager/usagerDocsRepository.service.ts @@ -1,6 +1,15 @@ -import { myDataSource } from "./../_postgres/appTypeormManager.service"; -import { UsagerDoc } from "./../../../_common/model/usager/UsagerDoc.type"; -import { UsagerDocsTable } from "./../../entities/usager/UsagerDocsTable.typeorm"; +import { UsagerDoc } from "@domifa/common"; +import { UsagerDocsTable } from "../../entities"; +import { myDataSource } from "../_postgres"; + +export const USAGER_DOCS_FIELDS_TO_SELECT = { + filetype: true, + label: true, + uuid: true, + createdAt: true, + createdBy: true, + shared: true, +}; export const usagerDocsRepository = myDataSource .getRepository(UsagerDocsTable) @@ -11,13 +20,7 @@ export const usagerDocsRepository = myDataSource usagerRef, structureId, }, - select: { - filetype: true, - label: true, - uuid: true, - createdAt: true, - createdBy: true, - }, + select: USAGER_DOCS_FIELDS_TO_SELECT, order: { createdAt: "DESC", }, diff --git a/packages/backend/src/structures/controllers/structure-doc.controller.ts b/packages/backend/src/structures/controllers/structure-doc.controller.ts index 2995334b4d..231e26e6c6 100644 --- a/packages/backend/src/structures/controllers/structure-doc.controller.ts +++ b/packages/backend/src/structures/controllers/structure-doc.controller.ts @@ -131,10 +131,8 @@ export class StructureDocController { customDocType: structureDocDto.customDocType, }); - // Ajout du document try { await structureDocRepository.insert(newDoc); - const docs = await structureDocRepository.findBy({ structureId: user.structureId, }); diff --git a/packages/backend/src/structures/dto/structure-doc.dto.ts b/packages/backend/src/structures/dto/structure-doc.dto.ts index ad41180943..52d88f5143 100644 --- a/packages/backend/src/structures/dto/structure-doc.dto.ts +++ b/packages/backend/src/structures/dto/structure-doc.dto.ts @@ -4,10 +4,12 @@ import { IsNotEmpty, IsString, MaxLength, + MinLength, ValidateIf, } from "class-validator"; import { ApiProperty } from "@nestjs/swagger"; import { StructureCustomDocType } from "@domifa/common"; +import { StripTagsTransform } from "../../_common/decorators"; export class StructureDocDto { @ApiProperty({ @@ -17,7 +19,9 @@ export class StructureDocDto { @ValidateIf((o) => o.custom === true) @IsNotEmpty() @IsString() - @MaxLength(200) + @MinLength(2) + @MaxLength(100) + @StripTagsTransform() public label: string; @ApiProperty({ diff --git a/packages/backend/src/usagers/controllers/usager-docs.controller.ts b/packages/backend/src/usagers/controllers/usager-docs.controller.ts index 2bbaa22928..b92b2ba93c 100644 --- a/packages/backend/src/usagers/controllers/usager-docs.controller.ts +++ b/packages/backend/src/usagers/controllers/usager-docs.controller.ts @@ -1,5 +1,3 @@ -import { UsagerDocsTable } from "./../../database/entities/usager/UsagerDocsTable.typeorm"; -import { usagerDocsRepository } from "./../../database/services/usager/usagerDocsRepository.service"; import { Body, Controller, @@ -9,6 +7,7 @@ import { Param, ParseIntPipe, ParseUUIDPipe, + Patch, Post, Res, UploadedFile, @@ -20,10 +19,13 @@ import { FileInterceptor } from "@nestjs/platform-express"; import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import { Response } from "express"; -import { AllowUserStructureRoles } from "../../auth/decorators"; +import { + AllowUserStructureRoles, + CurrentUsagerDoc, +} from "../../auth/decorators"; import { CurrentUsager } from "../../auth/decorators/current-usager.decorator"; import { CurrentUser } from "../../auth/decorators/current-user.decorator"; -import { AppUserGuard } from "../../auth/guards"; +import { AppUserGuard, UsagerDocAccessGuard } from "../../auth/guards"; import { UsagerAccessGuard } from "../../auth/guards/usager-access.guard"; import { domifaConfig } from "../../config"; @@ -33,9 +35,9 @@ import { randomName, validateUpload, } from "../../util/file-manager/FileManager"; -import { UsagerDoc, UserStructureAuthenticated } from "../../_common/model"; +import { UserStructureAuthenticated } from "../../_common/model"; import { AppLogsService } from "../../modules/app-logs/app-logs.service"; -import { UploadUsagerDocDto } from "../dto"; +import { PatchUsagerDocDto, PostUsagerDocDto } from "../dto"; import crypto from "node:crypto"; import { ExpressRequest } from "../../util/express"; @@ -48,7 +50,12 @@ import { FILES_SIZE_LIMIT } from "../../util/file-manager"; import { PassThrough, Readable } from "node:stream"; import { join } from "node:path"; import { FileManagerService } from "../../util/file-manager/file-manager.service"; -import { Usager } from "@domifa/common"; +import { Usager, UsagerDoc } from "@domifa/common"; +import { + USAGER_DOCS_FIELDS_TO_SELECT, + usagerDocsRepository, + UsagerDocsTable, +} from "../../database"; @UseGuards(AuthGuard("jwt"), AppUserGuard, UsagerAccessGuard) @ApiTags("docs") @@ -85,7 +92,7 @@ export class UsagerDocsController { @UploadedFile() file: Express.Multer.File, @CurrentUser() user: UserStructureAuthenticated, @CurrentUsager() currentUsager: Usager, - @Body() postData: UploadUsagerDocDto, + @Body() postData: PostUsagerDocDto, @Res() res: Response ) { const encryptionContext = crypto.randomUUID(); @@ -93,7 +100,7 @@ export class UsagerDocsController { const path = randomName(file); - const newDoc: UsagerDocsTable = { + const newDoc: UsagerDocsTable = new UsagerDocsTable({ createdAt: new Date(), createdBy: userName, filetype: file.mimetype, @@ -104,7 +111,7 @@ export class UsagerDocsController { usagerUUID: currentUsager.uuid, encryptionContext, encryptionVersion: 0, - }; + }); try { const filePath = join( @@ -138,30 +145,72 @@ export class UsagerDocsController { return res.status(HttpStatus.OK).json(docs); } - @Delete(":usagerRef/:docUuid") + @Patch(":usagerRef/:docUuid") + @UseGuards(UsagerDocAccessGuard) @AllowUserStructureRoles("simple", "responsable", "admin") - public async deleteDocument( - @Param("usagerRef", new ParseIntPipe()) usagerRef: number, + public async patchDocument( + @Param("usagerRef", new ParseIntPipe()) _usagerRef: number, @Param("docUuid", new ParseUUIDPipe()) docUuid: string, + @Body() updatedDoc: PatchUsagerDocDto, @CurrentUser() user: UserStructureAuthenticated, + @CurrentUsagerDoc() usagerDoc: UsagerDoc, @CurrentUsager() currentUsager: Usager, @Res() res: Response ) { - const doc = await usagerDocsRepository.findOneBy({ - uuid: docUuid, - structureId: currentUsager.structureId, - }); + if (updatedDoc.shared && usagerDoc.shared !== updatedDoc.shared) { + await this.appLogsService.create({ + userId: user.id, + usagerRef: currentUsager.ref, + structureId: user.structureId, + action: "USAGERS_DOCS_SHARED", + }); + } - if (!doc) { - return res - .status(HttpStatus.BAD_REQUEST) - .json({ message: "DOC_NOT_FOUND" }); + if (usagerDoc.label !== updatedDoc.label) { + await this.appLogsService.create({ + userId: user.id, + usagerRef: currentUsager.ref, + structureId: user.structureId, + action: "USAGERS_DOCS_RENAME", + }); } - await this.fileManagerService.deleteFile(doc.path); + await usagerDocsRepository.update( + { + uuid: docUuid, + }, + { + label: updatedDoc.label, + shared: updatedDoc.shared, + } + ); + + const doc = await usagerDocsRepository.findOne({ + where: { + uuid: docUuid, + }, + select: USAGER_DOCS_FIELDS_TO_SELECT, + }); + + return res.status(HttpStatus.OK).json(doc); + } + + @Delete(":usagerRef/:docUuid") + @UseGuards(UsagerDocAccessGuard) + @AllowUserStructureRoles("simple", "responsable", "admin") + public async deleteDocument( + @Param("usagerRef", new ParseIntPipe()) usagerRef: number, + @Param("docUuid", new ParseUUIDPipe()) _docUuid: string, + @CurrentUser() user: UserStructureAuthenticated, + @CurrentUsager() currentUsager: Usager, + @CurrentUsagerDoc() usagerDoc: UsagerDoc, + + @Res() res: Response + ) { + await this.fileManagerService.deleteFile(usagerDoc.path); await usagerDocsRepository.delete({ - uuid: doc.uuid, + uuid: usagerDoc.uuid, }); await this.appLogsService.create({ @@ -185,7 +234,7 @@ export class UsagerDocsController { @Param("usagerRef", new ParseIntPipe()) usagerRef: number, @CurrentUsager() currentUsager: Usager ): Promise { - return usagerDocsRepository.getUsagerDocs( + return await usagerDocsRepository.getUsagerDocs( usagerRef, currentUsager.structureId ); @@ -200,13 +249,6 @@ export class UsagerDocsController { @CurrentUser() user: UserStructureAuthenticated, @CurrentUsager() currentUsager: Usager ) { - await this.appLogsService.create({ - userId: user.id, - usagerRef, - structureId: user.structureId, - action: "USAGERS_DOCS_DOWNLOAD", - }); - const doc = await usagerDocsRepository.findOneBy({ uuid: docUuid, usagerRef, @@ -229,6 +271,13 @@ export class UsagerDocsController { const mainSecret = domifaConfig().security.mainSecret; const body = await this.fileManagerService.getFileBody(filePath); + await this.appLogsService.create({ + userId: user.id, + usagerRef, + structureId: user.structureId, + action: "USAGERS_DOCS_DOWNLOAD", + }); + try { return pipeline( // note: encryptedFilePath should end with .sfe, not .encrypted, to prepare for phase 3. diff --git a/packages/backend/src/usagers/dto/index.ts b/packages/backend/src/usagers/dto/index.ts index 2c36b70a17..a108364c55 100644 --- a/packages/backend/src/usagers/dto/index.ts +++ b/packages/backend/src/usagers/dto/index.ts @@ -11,5 +11,5 @@ export * from "./procuration.dto"; export * from "./rdv.dto"; export * from "./search-usager.dto"; export * from "./transfert.dto"; -export * from "./UploadUsagerDoc.dto"; +export * from "./usager-doc"; export * from "./UsagerAyantDroitDto"; diff --git a/packages/backend/src/usagers/dto/usager-doc/index.ts b/packages/backend/src/usagers/dto/usager-doc/index.ts new file mode 100644 index 0000000000..8b5ca19c0b --- /dev/null +++ b/packages/backend/src/usagers/dto/usager-doc/index.ts @@ -0,0 +1,3 @@ +// @index('./*', f => `export * from '${f.path}'`) +export * from "./patch-usager-doc.dto"; +export * from "./post-usager-doc.dto"; diff --git a/packages/backend/src/usagers/dto/usager-doc/patch-usager-doc.dto.ts b/packages/backend/src/usagers/dto/usager-doc/patch-usager-doc.dto.ts new file mode 100644 index 0000000000..4349e9b82a --- /dev/null +++ b/packages/backend/src/usagers/dto/usager-doc/patch-usager-doc.dto.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { + IsBoolean, + IsNotEmpty, + IsString, + MaxLength, + MinLength, +} from "class-validator"; +import { StripTagsTransform, Trim } from "../../../_common/decorators"; + +export class PatchUsagerDocDto { + @ApiProperty({ + type: String, + required: true, + maxLength: 100, + }) + @IsNotEmpty() + @MaxLength(100) + @MinLength(2) + @IsString() + @Trim() + @StripTagsTransform() + public label!: string; + + @ApiProperty({ + type: Boolean, + required: true, + }) + @IsNotEmpty() + @IsBoolean() + public shared!: boolean; +} diff --git a/packages/backend/src/usagers/dto/UploadUsagerDoc.dto.ts b/packages/backend/src/usagers/dto/usager-doc/post-usager-doc.dto.ts similarity index 61% rename from packages/backend/src/usagers/dto/UploadUsagerDoc.dto.ts rename to packages/backend/src/usagers/dto/usager-doc/post-usager-doc.dto.ts index bdb460b3e7..35f6bd0cd1 100644 --- a/packages/backend/src/usagers/dto/UploadUsagerDoc.dto.ts +++ b/packages/backend/src/usagers/dto/usager-doc/post-usager-doc.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString, MaxLength } from "class-validator"; -import { StripTagsTransform, Trim } from "../../_common/decorators"; +import { IsNotEmpty, IsString, MaxLength, MinLength } from "class-validator"; +import { StripTagsTransform, Trim } from "../../../_common/decorators"; -export class UploadUsagerDocDto { +export class PostUsagerDocDto { @ApiProperty({ type: String, required: true, @@ -10,6 +10,7 @@ export class UploadUsagerDocDto { }) @IsNotEmpty() @MaxLength(100) + @MinLength(2) @IsString() @Trim() @StripTagsTransform() diff --git a/packages/backend/src/util/file-manager/FileManager.ts b/packages/backend/src/util/file-manager/FileManager.ts index 9e37d26539..bdba1af2ed 100644 --- a/packages/backend/src/util/file-manager/FileManager.ts +++ b/packages/backend/src/util/file-manager/FileManager.ts @@ -5,7 +5,7 @@ import { FILES_EXTENSIONS } from "./FILES_EXTENSIONS.const"; import { randomBytes } from "crypto"; import sanitizeFilename from "sanitize-filename"; import sharp from "sharp"; -import { UsagerDoc } from "../../_common/model"; +import { UsagerDoc } from "@domifa/common"; export const compressAndResizeImage = ( usagerDoc: Pick< diff --git a/packages/common/src/structure-doc/constants/STRUCTURE_CUSTOM_DOC_LABELS.const.ts b/packages/common/src/structure-doc/constants/STRUCTURE_CUSTOM_DOC_LABELS.const.ts new file mode 100644 index 0000000000..ef4c7fdb4e --- /dev/null +++ b/packages/common/src/structure-doc/constants/STRUCTURE_CUSTOM_DOC_LABELS.const.ts @@ -0,0 +1,10 @@ +import { StructureCustomDocType } from ".."; + +export const STRUCTURE_CUSTOM_DOC_LABELS: { + [key in StructureCustomDocType]: string; +} = { + attestation_postale: "Attestation postale", + courrier_radiation: "Courrier de radiation", + acces_espace_domicilie: "Accès à Mon DomiFa", + autre: "Autre document", +}; diff --git a/packages/common/src/structure-doc/constants/index.ts b/packages/common/src/structure-doc/constants/index.ts index 65af98d5e6..591b6328fb 100644 --- a/packages/common/src/structure-doc/constants/index.ts +++ b/packages/common/src/structure-doc/constants/index.ts @@ -1,2 +1,3 @@ //@index('./*', f => `export * from '${f.path}'`) export * from "./DOCUMENT_EXTENSION_LABELS.const"; +export * from "./STRUCTURE_CUSTOM_DOC_LABELS.const"; diff --git a/packages/frontend/src/app/modules/shared/components/button/button.component.css b/packages/frontend/src/app/modules/shared/components/button/button.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/frontend/src/app/modules/shared/components/button/button.component.html b/packages/frontend/src/app/modules/shared/components/button/button.component.html new file mode 100644 index 0000000000..4b7ced8c98 --- /dev/null +++ b/packages/frontend/src/app/modules/shared/components/button/button.component.html @@ -0,0 +1,49 @@ + diff --git a/packages/frontend/src/app/modules/shared/components/button/button.component.spec.ts b/packages/frontend/src/app/modules/shared/components/button/button.component.spec.ts new file mode 100644 index 0000000000..79fae6447e --- /dev/null +++ b/packages/frontend/src/app/modules/shared/components/button/button.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { ButtonComponent } from "./button.component"; + +describe("ButtonComponent", () => { + let component: ButtonComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ButtonComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/frontend/src/app/modules/shared/components/button/button.component.ts b/packages/frontend/src/app/modules/shared/components/button/button.component.ts new file mode 100644 index 0000000000..d3c47887b1 --- /dev/null +++ b/packages/frontend/src/app/modules/shared/components/button/button.component.ts @@ -0,0 +1,21 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { IconName, IconPrefix } from "@fortawesome/fontawesome-svg-core"; + +@Component({ + selector: "app-button", + templateUrl: "./button.component.html", + styleUrls: ["./button.component.css"], +}) +export class ButtonComponent { + @Input() loading = false; + @Input() color = "primary"; + + @Input() icon?: IconName; + @Input() prefix: IconPrefix = "fas"; + @Input() loadingText = "Patientez..."; + @Input() loadingAriaLabel = "Chargement en cours"; + @Input() content = ""; + @Input() ariaLabel = ""; + + @Output() readonly action = new EventEmitter(); +} diff --git a/packages/frontend/src/app/modules/shared/shared.module.ts b/packages/frontend/src/app/modules/shared/shared.module.ts index e5268d5642..711f2a84ca 100644 --- a/packages/frontend/src/app/modules/shared/shared.module.ts +++ b/packages/frontend/src/app/modules/shared/shared.module.ts @@ -10,6 +10,7 @@ import { CustomToastrComponent } from "./components/custom-toastr/custom-toastr. import { ReplaceLineBreaks } from "./pipes"; import { DateFrDirective, CleanStrDirective } from "./directives"; +import { ButtonComponent } from "./components/button/button.component"; @NgModule({ declarations: [ @@ -17,6 +18,7 @@ import { DateFrDirective, CleanStrDirective } from "./directives"; CleanStrDirective, CustomToastrComponent, ReplaceLineBreaks, + ButtonComponent, ], exports: [ ReplaceLineBreaks, @@ -24,6 +26,7 @@ import { DateFrDirective, CleanStrDirective } from "./directives"; CleanStrDirective, CustomToastrComponent, FontAwesomeModule, + ButtonComponent, ], imports: [CommonModule, FontAwesomeModule], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.html b/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.html index 1ed07dffc9..e340fcc4d9 100644 --- a/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.html +++ b/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.html @@ -60,20 +60,8 @@ [document]="document" > - - {{ document.label }} - - - Attestation postale - Courrier de radiation - {{ - document.label - }} - + {{ document.label }} -- + {{ document.createdBy.nom }} {{ document.createdBy.prenom }} @@ -81,54 +69,25 @@ {{ document.createdAt | date : "dd MMMM yyyy" }} - + - + diff --git a/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.ts b/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.ts index 09c965e89b..c84681344e 100644 --- a/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.ts +++ b/packages/frontend/src/app/modules/structures/components/structures-custom-docs-table/structures-custom-docs-table.component.ts @@ -13,13 +13,14 @@ import { StructureDocService } from "../../services/structure-doc.service"; import { Subscription } from "rxjs"; import { StructureDoc, UserStructure } from "@domifa/common"; import { UsagersFilterCriteriaSortValues } from "../../../manage-usagers/components/usager-filter"; +import { WithLoading } from "../../../../shared"; @Component({ selector: "app-structures-custom-docs-table", templateUrl: "./structures-custom-docs-table.component.html", }) export class StructuresCustomDocsTableComponent implements OnDestroy { - @Input() public structureDocs!: StructureDoc[]; + @Input() public structureDocs!: WithLoading[]; @Input() public me!: UserStructure; @Input() public title!: string; @@ -30,70 +31,48 @@ export class StructuresCustomDocsTableComponent implements OnDestroy { public currentKey: keyof StructureDoc = "createdAt"; private subscription = new Subscription(); - // Frontend variables - public loadings: { - download: string[]; - delete: string[]; - }; constructor( private readonly structureDocService: StructureDocService, private readonly toastService: CustomToastService - ) { - this.loadings = { - download: [], - delete: [], - }; - } + ) {} public ngOnDestroy(): void { this.subscription.unsubscribe(); } - public getStructureDoc(structureDoc: StructureDoc): void { - this.loadings.download.push(structureDoc.uuid); - + public getStructureDoc(structureDoc: WithLoading): void { + structureDoc.loading = true; this.subscription.add( this.structureDocService.getStructureDoc(structureDoc.uuid).subscribe({ next: (blob: Blob) => { const extension = structureDoc.path.split(".")[1]; const newBlob = new Blob([blob], { type: structureDoc.filetype }); saveAs(newBlob, `${structureDoc.label}.${extension}`); - this.stopLoading("download", structureDoc.uuid); + structureDoc.loading = false; }, error: () => { this.toastService.error("Impossible de télécharger le fichier"); - this.stopLoading("download", structureDoc.uuid); + structureDoc.loading = false; }, }) ); } - public deleteStructureDoc(structureDoc: StructureDoc): void { - this.loadings.delete.push(structureDoc.uuid); - + public deleteStructureDoc(structureDoc: WithLoading): void { + structureDoc.loading = true; this.subscription.add( this.structureDocService.deleteStructureDoc(structureDoc.uuid).subscribe({ next: () => { - this.stopLoading("delete", structureDoc.uuid); - this.toastService.success("Suppression réussie"); - + structureDoc.loading = false; + this.toastService.success("Suppression du document réussie"); this.getAllStructureDocs.emit(); }, error: () => { - this.stopLoading("delete", structureDoc.uuid); + structureDoc.loading = false; this.toastService.error("Impossible de télécharger le fichier"); }, }) ); } - - private stopLoading(loadingType: "delete" | "download", loadingRef: string) { - setTimeout(() => { - const index = this.loadings[loadingType].indexOf(loadingRef); - if (index !== -1) { - this.loadings[loadingType].splice(index, 1); - } - }, 500); - } } diff --git a/packages/frontend/src/app/modules/structures/components/structures-custom-docs/structures-custom-docs.component.ts b/packages/frontend/src/app/modules/structures/components/structures-custom-docs/structures-custom-docs.component.ts index 4b57b891fa..69b3c3a13e 100644 --- a/packages/frontend/src/app/modules/structures/components/structures-custom-docs/structures-custom-docs.component.ts +++ b/packages/frontend/src/app/modules/structures/components/structures-custom-docs/structures-custom-docs.component.ts @@ -14,19 +14,16 @@ import { DEFAULT_MODAL_OPTIONS } from "../../../../../_common/model"; import { AuthService } from "../../../shared/services/auth.service"; import { StructureDocService } from "../../services/structure-doc.service"; import { StructureDoc, UserStructure } from "@domifa/common"; -import { DOMIFA_CUSTOM_DOCS } from "../../constants/DOMIFA_CUSTOM_DOCS.const"; +import { initializeLoadingState, WithLoading } from "../../../../shared"; @Component({ selector: "app-structures-custom-docs", templateUrl: "./structures-custom-docs.component.html", }) export class StructuresCustomDocsComponent implements OnInit, OnDestroy { - // Documents simples - public structureDocs: StructureDoc[]; - // Documents pré-remplis - public customStructureDocs: StructureDoc[]; + public structureDocs: WithLoading[]; + public customStructureDocs: WithLoading[]; - public defaultStructureDocs: StructureDoc[]; public me!: UserStructure | null; public isCustomDoc: boolean; @@ -45,7 +42,6 @@ export class StructuresCustomDocsComponent implements OnInit, OnDestroy { this.structureDocs = []; this.customStructureDocs = []; this.isCustomDoc = false; - this.defaultStructureDocs = DOMIFA_CUSTOM_DOCS; } public ngOnInit(): void { @@ -60,12 +56,12 @@ export class StructuresCustomDocsComponent implements OnInit, OnDestroy { this.subscription.add( this.structureDocService.getAllStructureDocs().subscribe({ next: (structureDocs: StructureDoc[]) => { - this.structureDocs = structureDocs.filter( + this.structureDocs = initializeLoadingState(structureDocs).filter( (structureDoc) => !structureDoc.custom ); - this.customStructureDocs = structureDocs.filter( - (structureDoc) => structureDoc.custom - ); + this.customStructureDocs = initializeLoadingState( + structureDocs + ).filter((structureDoc) => structureDoc.custom); }, error: () => { this.toastService.error("Impossible d'afficher les documents"); diff --git a/packages/frontend/src/app/modules/structures/components/structures-upload-docs/structures-upload-docs.component.html b/packages/frontend/src/app/modules/structures/components/structures-upload-docs/structures-upload-docs.component.html index ef45d22aac..43fc308011 100644 --- a/packages/frontend/src/app/modules/structures/components/structures-upload-docs/structures-upload-docs.component.html +++ b/packages/frontend/src/app/modules/structures/components/structures-upload-docs/structures-upload-docs.component.html @@ -46,10 +46,7 @@
{ - this.uploadForm.get("label")?.setValue(value === "autre" ? "" : value); + this.uploadForm + .get("label") + ?.setValue( + value === "autre" ? "" : STRUCTURE_CUSTOM_DOC_LABELS[value] + ); this.uploadForm.get("label")?.updateValueAndValidity(); }) ); diff --git a/packages/frontend/src/app/modules/structures/structures.module.ts b/packages/frontend/src/app/modules/structures/structures.module.ts index 9c1a6c08d4..46bf5f5874 100644 --- a/packages/frontend/src/app/modules/structures/structures.module.ts +++ b/packages/frontend/src/app/modules/structures/structures.module.ts @@ -21,6 +21,7 @@ import { StructuresRoutingModule } from "./structures-routing.module"; import { GeneralModule } from "../general/general.module"; import { SortArrayPipe } from "../shared/pipes"; import { DisplayTableImageComponent } from "../shared/components/display-table-image/display-table-image.component"; +import { TableHeadSortComponent } from "../shared/components/table-head-sort/table-head-sort.component"; @NgModule({ declarations: [ @@ -48,6 +49,7 @@ import { DisplayTableImageComponent } from "../shared/components/display-table-i StructuresRoutingModule, SortArrayPipe, DisplayTableImageComponent, + TableHeadSortComponent, ], providers: [], schemas: [CUSTOM_ELEMENTS_SCHEMA], diff --git a/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.html b/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.html index 32aac22944..67e4f37460 100644 --- a/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.html +++ b/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.html @@ -29,108 +29,10 @@ - - - - - Attestation postale - - {{ defaultStructureDocs.attestation_postale.createdBy.nom }} - {{ defaultStructureDocs.attestation_postale.createdBy.prenom }} - - - {{ - defaultStructureDocs.attestation_postale.createdAt - | date : "dd MMMM yyyy" - }} - - - - - - - - - - Courrier de radiation - - {{ defaultStructureDocs.courrier_radiation.createdBy.nom }} - {{ defaultStructureDocs.courrier_radiation.createdBy.prenom }} - - - - {{ - defaultStructureDocs.courrier_radiation.createdAt - | date : "dd MMMM yyyy" - }} - - - - - - - - {{ document.label }} + + {{ document.label }} + {{ document.createdBy.nom }} {{ document.createdBy.prenom }} {{ document.createdAt | date : "dd MMMM yyyy" }} - + diff --git a/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.ts b/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.ts index 633fed4355..a83da12b72 100644 --- a/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.ts +++ b/packages/frontend/src/app/modules/usager-profil/components/_documents/profil-structure-documents/profil-structure-docs.component.ts @@ -6,11 +6,14 @@ import { DocumentService } from "../../../../usager-shared/services/document.ser import { CustomToastService } from "../../../../shared/services/custom-toast.service"; import { Subscription } from "rxjs"; import { + STRUCTURE_CUSTOM_DOC_LABELS, + StructureCustomDocType, StructureDoc, StructureDocTypesAvailable, UserStructure, } from "@domifa/common"; import { UsagersFilterCriteriaSortValues } from "../../../../manage-usagers/components/usager-filter"; +import { initializeLoadingState, WithLoading } from "../../../../../shared"; @Component({ selector: "app-profil-structure-docs", @@ -20,16 +23,8 @@ export class ProfilStructureDocsComponent implements OnInit, OnDestroy { @Input() public usager!: UsagerFormModel; @Input() public me!: UserStructure; - public defaultStructureDocs: { - attestation_postale: StructureDoc; - courrier_radiation: StructureDoc; - }; - private subscription = new Subscription(); - public customStructureDocs: StructureDoc[]; - - // Frontend variables - public loadings: string[]; + public docs: WithLoading[] = []; public sortValue: UsagersFilterCriteriaSortValues = "desc"; public currentKey: keyof StructureDoc = "createdAt"; @@ -37,80 +32,46 @@ export class ProfilStructureDocsComponent implements OnInit, OnDestroy { constructor( private readonly documentService: DocumentService, private readonly toastService: CustomToastService - ) { - this.defaultStructureDocs = { - attestation_postale: { - id: 0, - createdBy: { - id: 0, - nom: "Domifa", - prenom: "", - }, - filetype: - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - custom: true, - uuid: "xxx", - createdAt: this.me?.structure?.createdAt, - label: "Attestation postale", - customDocType: "attestation_postale", - path: "", - }, - courrier_radiation: { - id: 1, - createdBy: { - id: 0, - nom: "Domifa", - prenom: "", - }, - filetype: - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - custom: true, - uuid: "xxx", - createdAt: this.me?.structure?.createdAt, - label: "Courrier de radiation", - customDocType: "courrier_radiation", - path: "", - }, - }; - - this.customStructureDocs = []; - this.loadings = []; - } + ) {} public ngOnInit(): void { this.getAllStructureDocs(); } // Documents définis par Domifa - public getDomifaCustomDoc(docType: StructureDocTypesAvailable): void { - this.loadings.push(docType); - + public getDomifaCustomDoc(structureDoc: WithLoading): void { this.subscription.add( this.documentService .getDomifaCustomDoc({ usagerId: this.usager.ref, - docType, + docType: structureDoc.customDocType as StructureDocTypesAvailable, }) .subscribe({ next: (blob: Blob) => { const newBlob = new Blob([blob], { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }); - saveAs(newBlob, `${docType}.docx`); - this.stopLoading(docType); + saveAs(newBlob, `${structureDoc.customDocType}.docx`); + structureDoc.loading = false; }, error: () => { this.toastService.error( "Impossible de télécharger le fichier pour l'instant" ); - this.stopLoading(docType); + structureDoc.loading = false; }, }) ); } - // Documents personnalisables de la structure - public getStructureCustomDoc(structureDoc: StructureDoc): void { + public getStructureCustomDoc(structureDoc: WithLoading): void { + structureDoc.loading = true; + // id= 0 => documents par défaut de DomiFa + if (structureDoc.id === 0) { + this.getDomifaCustomDoc(structureDoc); + return; + } + this.subscription.add( this.documentService .getStructureCustomDoc(this.usager.ref, structureDoc.uuid) @@ -120,13 +81,13 @@ export class ProfilStructureDocsComponent implements OnInit, OnDestroy { const newBlob = new Blob([blob], { type: structureDoc.filetype }); saveAs(newBlob, structureDoc.label + extension); - this.stopLoading(structureDoc.uuid); + structureDoc.loading = false; }, error: () => { this.toastService.error( "Impossible de télécharger le fichier pour l'instant" ); - this.stopLoading(structureDoc.uuid); + structureDoc.loading = false; }, }) ); @@ -136,31 +97,46 @@ export class ProfilStructureDocsComponent implements OnInit, OnDestroy { this.subscription.add( this.documentService.getAllStructureDocs().subscribe({ next: (structureDocs: StructureDoc[]) => { - structureDocs.forEach((structureDoc: StructureDoc) => { - if (structureDoc.customDocType === "attestation_postale") { - this.defaultStructureDocs.attestation_postale.createdBy = - structureDoc.createdBy; - this.defaultStructureDocs.attestation_postale.createdAt = - structureDoc.createdAt; - } else if (structureDoc.customDocType === "courrier_radiation") { - this.defaultStructureDocs.courrier_radiation.createdBy = - structureDoc.createdBy; - this.defaultStructureDocs.courrier_radiation.createdAt = - structureDoc.createdAt; - } else { - this.customStructureDocs.push(structureDoc); - } - }); + this.docs = initializeLoadingState(structureDocs); + if ( + !this.docs.some( + (structure) => structure.customDocType === "attestation_postale" + ) + ) { + this.docs.push(this.getDefaultCustomDoc("attestation_postale")); + } + if ( + !this.docs.some( + (structure) => structure.customDocType === "courrier_radiation" + ) + ) { + this.docs.push(this.getDefaultCustomDoc("courrier_radiation")); + } }, }) ); } - private stopLoading(loadingRef: string): void { - const index = this.loadings.indexOf(loadingRef); - if (index !== -1) { - this.loadings.splice(index, 1); - } + private getDefaultCustomDoc( + customDocType: StructureCustomDocType + ): WithLoading { + return { + id: 0, + createdBy: { + id: 0, + nom: "Domifa", + prenom: "", + }, + filetype: + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + custom: true, + uuid: "xxx", + createdAt: this.me?.structure?.createdAt, + label: STRUCTURE_CUSTOM_DOC_LABELS[customDocType], + customDocType, + path: "", + loading: false, + }; } public ngOnDestroy(): void { diff --git a/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.html b/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.html index b42a773811..c652647e99 100644 --- a/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.html +++ b/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.html @@ -51,66 +51,50 @@ - {{ document.label }} + + {{ document.label }} + ⚠️ Document partagé sur Mon DomiFa + {{ document.createdBy }} {{ document.createdAt | date : "dd MMMM yyyy" }} - + - + diff --git a/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.ts b/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.ts index 918084e9ae..5e885bb9e8 100644 --- a/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.ts +++ b/packages/frontend/src/app/modules/usager-shared/components/display-usager-docs/display-usager-docs.component.ts @@ -9,6 +9,7 @@ import { Subscription } from "rxjs"; import { UsagerDoc, UserStructure } from "@domifa/common"; import { UsagersFilterCriteriaSortValues } from "../../../manage-usagers/components/usager-filter"; import slug from "slug"; +import { initializeLoadingState, WithLoading } from "../../../../shared"; @Component({ selector: "app-display-usager-docs", @@ -20,12 +21,7 @@ export class DisplayUsagerDocsComponent implements OnInit, OnDestroy { @Input() public editPJ!: boolean; private subscription = new Subscription(); - public docs: UsagerDoc[]; - - public loadings: { - download: number[]; - delete: number[]; - }; + public docs: WithLoading[]; public sortValue: UsagersFilterCriteriaSortValues = "desc"; public currentKey: keyof UsagerDoc = "createdAt"; @@ -34,10 +30,6 @@ export class DisplayUsagerDocsComponent implements OnInit, OnDestroy { private readonly documentService: DocumentService, private readonly toastService: CustomToastService ) { - this.loadings = { - download: [], - delete: [], - }; this.docs = []; } @@ -49,7 +41,7 @@ export class DisplayUsagerDocsComponent implements OnInit, OnDestroy { this.subscription.add( this.documentService.getUsagerDocs(this.usager.ref).subscribe({ next: (docs: UsagerDoc[]) => { - this.docs = docs; + this.docs = initializeLoadingState(docs); }, error: () => { this.toastService.error("Impossible de d'afficher les documents"); @@ -58,49 +50,54 @@ export class DisplayUsagerDocsComponent implements OnInit, OnDestroy { ); } - public getDocument(docIndex: number) { - this.startLoading("download", docIndex); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateDocument(updatedDoc: any, index: number) { + this.docs = [ + ...this.docs.slice(0, index), + updatedDoc, + ...this.docs.slice(index + 1), + ]; + } + + public getDocument(doc: WithLoading) { + doc.loading = true; this.subscription.add( - this.documentService - .getDocument(this.usager.ref, this.docs[docIndex].uuid) - .subscribe({ - next: (blob: Blob) => { - const doc = this.docs[docIndex]; - const extension = STRUCTURE_DOC_EXTENSIONS[doc.filetype]; - const newBlob = new Blob([blob], { type: doc.filetype }); + this.documentService.getDocument(this.usager.ref, doc.uuid).subscribe({ + next: (blob: Blob) => { + const extension = STRUCTURE_DOC_EXTENSIONS[doc.filetype]; + const newBlob = new Blob([blob], { type: doc.filetype }); - const name = - this.slugLabel(doc.label) + - "_" + - this.slugLabel(this.usager.nom + " " + this.usager.prenom) + - extension; - saveAs(newBlob, name); - this.stopLoading("download", docIndex); - }, - error: () => { - this.toastService.error("Impossible de télécharger le fichier"); - this.stopLoading("download", docIndex); - }, - }) + const label = this.slugLabel(doc.label); + const slugName = this.slugLabel( + `${this.usager.nom} ${this.usager.prenom}` + ); + const name = `${label}_${slugName}${extension}`; + + saveAs(newBlob, name); + doc.loading = false; + }, + error: () => { + this.toastService.error("Impossible de télécharger le fichier"); + doc.loading = false; + }, + }) ); } - public deleteDocument(docIndex: number): void { - this.startLoading("delete", docIndex); + public deleteDocument(doc: WithLoading): void { + doc.loading = true; this.subscription.add( - this.documentService - .deleteDocument(this.usager.ref, this.docs[docIndex].uuid) - .subscribe({ - next: (docs: UsagerDoc[]) => { - this.docs = docs; - this.stopLoading("delete", docIndex); - this.toastService.success("Document supprimé avec succès"); - }, - error: () => { - this.stopLoading("delete", docIndex); - this.toastService.error("Impossible de supprimer le document"); - }, - }) + this.documentService.deleteDocument(this.usager.ref, doc.uuid).subscribe({ + next: (docs: UsagerDoc[]) => { + this.docs = initializeLoadingState(docs); + this.toastService.success("Document supprimé avec succès"); + }, + error: () => { + doc.loading = true; + + this.toastService.error("Impossible de supprimer le document"); + }, + }) ); } @@ -115,25 +112,6 @@ export class DisplayUsagerDocsComponent implements OnInit, OnDestroy { }); } - private startLoading( - loadingType: "delete" | "download", - loadingRef: number - ): void { - this.loadings[loadingType].push(loadingRef); - } - - private stopLoading( - loadingType: "delete" | "download", - loadingRef: number - ): void { - setTimeout(() => { - const index = this.loadings[loadingType].indexOf(loadingRef); - if (index !== -1) { - this.loadings[loadingType].splice(index, 1); - } - }, 500); - } - public ngOnDestroy(): void { this.subscription.unsubscribe(); } diff --git a/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.html b/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.html index f03ee038c6..2d87d08d89 100644 --- a/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.html +++ b/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.html @@ -1,12 +1,24 @@ -
-
-
+ + + + + + -
+
-
-
- Partager le document avec le domicilié sur "Mon DomiFa". -
- - + +
+ + Partager le document avec le domicilié sur "Mon DomiFa". + + +
+ + +
+
+ + +
+
+ +
+ Attention: le domicilié pourra télécharger le document depuis un + ordinateur ou un téléphone
-
- - -
-
-
- -
+ +
+ - + diff --git a/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.spec.ts b/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.spec.ts index cef2483bf4..5fb3f714a2 100644 --- a/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.spec.ts +++ b/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.spec.ts @@ -1,6 +1,8 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { EditUsagerDocComponent } from "./edit-usager-doc.component"; +import { HttpClientModule } from "@angular/common/http"; +import { SharedModule } from "../../../shared/shared.module"; describe("EditUsagerDocComponent", () => { let component: EditUsagerDocComponent; @@ -9,10 +11,24 @@ describe("EditUsagerDocComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [EditUsagerDocComponent], + imports: [HttpClientModule, SharedModule], }).compileComponents(); fixture = TestBed.createComponent(EditUsagerDocComponent); component = fixture.componentInstance; + component.doc = { + uuid: "82b484ba-1335-4344-85b1-68ac5660f4f6", + createdAt: new Date("2024-10-29T21:35:12.143Z"), + label: "LABEL", + filetype: "image/jpeg", + createdBy: "Team", + shared: false, + loading: true, + usagerUUID: "", + path: "", + structureId: 0, + usagerRef: 1, + }; fixture.detectChanges(); }); diff --git a/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.ts b/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.ts index 35f0fc211d..a580650705 100644 --- a/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.ts +++ b/packages/frontend/src/app/modules/usager-shared/components/edit-usager-doc/edit-usager-doc.component.ts @@ -1,4 +1,13 @@ -import { Component, Input, OnInit } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + TemplateRef, + ViewChild, +} from "@angular/core"; import { UntypedFormGroup, AbstractControl, @@ -9,29 +18,47 @@ import { Usager, UsagerDoc } from "@domifa/common"; import { Subscription } from "rxjs"; import { CustomToastService } from "../../../shared/services"; import { DocumentService } from "../../services"; -import { NoWhiteSpaceValidator } from "../../../../shared"; +import { NoWhiteSpaceValidator, WithLoading } from "../../../../shared"; +import { NgbModal, NgbModalRef } from "@ng-bootstrap/ng-bootstrap"; +import { DEFAULT_MODAL_OPTIONS } from "../../../../../_common/model"; export type DocumentPatchForm = Pick; + @Component({ selector: "app-edit-usager-doc", templateUrl: "./edit-usager-doc.component.html", styleUrls: ["./edit-usager-doc.component.css"], }) -export class EditUsagerDocComponent implements OnInit { +export class EditUsagerDocComponent implements OnInit, OnDestroy { public submitted = false; public loading = false; public documentForm!: UntypedFormGroup; private subscription = new Subscription(); - @Input() public document: UsagerDoc; - @Input() public usager: Pick; + @Input() public usager: Pick; + @Input() public doc: WithLoading; + @Output() public readonly docChange = new EventEmitter< + WithLoading + >(); + + @ViewChild("editDocumentModal", { static: true }) + public editDocumentModal!: TemplateRef; constructor( private readonly formBuilder: UntypedFormBuilder, private readonly documentService: DocumentService, - private readonly toastService: CustomToastService + private readonly toastService: CustomToastService, + private readonly modalService: NgbModal ) {} + public openModal(): void { + this.modalService.open(this.editDocumentModal, DEFAULT_MODAL_OPTIONS); + } + + public closeModals(): void { + this.modalService.dismissAll(); + } + public get u(): { [key: string]: AbstractControl } { return this.documentForm.controls; } @@ -39,10 +66,15 @@ export class EditUsagerDocComponent implements OnInit { public ngOnInit(): void { this.documentForm = this.formBuilder.group({ label: [ - this.document.label, - [Validators.required, NoWhiteSpaceValidator, Validators.minLength(2)], + this.doc.label, + [ + Validators.required, + NoWhiteSpaceValidator, + Validators.minLength(2), + Validators.maxLength(100), + ], ], - shared: [this.document.shared, Validators.required], + shared: [this.doc.shared, Validators.required], }); } @@ -52,16 +84,17 @@ export class EditUsagerDocComponent implements OnInit { return; } + this.loading = true; + this.subscription.add( this.documentService - .patchDocument( - this.documentForm.value, - this.usager.ref, - this.document.uuid - ) + .patchDocument(this.documentForm.value, this.usager.ref, this.doc.uuid) .subscribe({ - next: () => { + next: (doc: UsagerDoc) => { + this.docChange.emit({ ...doc, loading: false }); this.toastService.success("Fichier modifié avec succès"); + this.loading = false; + this.closeModals(); }, error: () => { this.loading = false; @@ -70,4 +103,8 @@ export class EditUsagerDocComponent implements OnInit { }) ); } + + public ngOnDestroy(): void { + this.subscription.unsubscribe(); + } } diff --git a/packages/frontend/src/app/modules/usager-shared/services/document.service.ts b/packages/frontend/src/app/modules/usager-shared/services/document.service.ts index 7748aed843..d615592f60 100644 --- a/packages/frontend/src/app/modules/usager-shared/services/document.service.ts +++ b/packages/frontend/src/app/modules/usager-shared/services/document.service.ts @@ -139,9 +139,9 @@ export class DocumentService { doc: DocumentPatchForm, usagerRef: number, uuid: string - ): Observable { - return this.http.patch( - `${this.endPoint}/${usagerRef}/${uuid}`, + ): Observable { + return this.http.patch( + `${this.endPoint}${usagerRef}/${uuid}`, doc ); } diff --git a/packages/frontend/src/app/shared/index.ts b/packages/frontend/src/app/shared/index.ts index b544cc6910..230e99f2f1 100644 --- a/packages/frontend/src/app/shared/index.ts +++ b/packages/frontend/src/app/shared/index.ts @@ -4,4 +4,5 @@ export * from "./bootstrap-util"; export * from "./constants"; export * from "./store"; export * from "./string-cleaner.service"; +export * from "./utils"; export * from "./validators"; diff --git a/packages/frontend/src/app/shared/utils/index.ts b/packages/frontend/src/app/shared/utils/index.ts new file mode 100644 index 0000000000..48efd8d5ee --- /dev/null +++ b/packages/frontend/src/app/shared/utils/index.ts @@ -0,0 +1,2 @@ +// @index('./*', f => `export * from '${f.path}'`) +export * from "./init-loading-state"; diff --git a/packages/frontend/src/app/shared/utils/init-loading-state.ts b/packages/frontend/src/app/shared/utils/init-loading-state.ts new file mode 100644 index 0000000000..3ca8b2d72c --- /dev/null +++ b/packages/frontend/src/app/shared/utils/init-loading-state.ts @@ -0,0 +1,23 @@ +type LoadingState = { + loading: boolean; +}; + +export type WithLoading = T & LoadingState; + +export function initializeLoadingState( + items: T[], + initialState = false +): WithLoading[] { + return items.map((item) => ({ + ...item, + loading: initialState, + })); +} + +export function updateLoadingState( + items: WithLoading[], + identifier: (item: T) => boolean, + loading: boolean +): WithLoading[] { + return items.map((item) => (identifier(item) ? { ...item, loading } : item)); +}