From 880a3da048c9593ef80708e28f5f2fba26b4d885 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 07:35:35 +0700 Subject: [PATCH 1/7] update: add audit and private status to document when create new import request --- src/Api/Controllers/DocumentsController.cs | 1 + .../Payload/Requests/Documents/ImportDocumentRequest.cs | 1 + src/Application/Documents/Commands/ImportDocument.cs | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 5175544f..ef6034bf 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -101,6 +101,7 @@ public async Task>> Import([FromBody] ImportDoc Description = request.Description, DocumentType = request.DocumentType, ImporterId = userId, + IsPrivate = request.IsPrivate, }; var result = await Mediator.Send(command); return Ok(Result.Succeed(result)); diff --git a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs index 7b1b1cd8..063cae22 100644 --- a/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs +++ b/src/Api/Controllers/Payload/Requests/Documents/ImportDocumentRequest.cs @@ -17,4 +17,5 @@ public class ImportDocumentRequest /// Document type of the document to be imported /// public string DocumentType { get; set; } = null!; + public bool IsPrivate { get; set; } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index fb904034..2794555c 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -21,6 +21,7 @@ public record Command : IRequest public string? Description { get; init; } public string DocumentType { get; init; } = null!; public Guid ImporterId { get; init; } + public bool IsPrivate { get; init; } } public class CommandHandler : IRequestHandler @@ -64,6 +65,9 @@ public async Task Handle(Command request, CancellationToken cancell Importer = importer, Department = importer.Department, Status = DocumentStatus.Issued, + Created = LocalDateTime.FromDateTime(DateTime.Now), + CreatedBy = importer.Id, + IsPrivate = request.IsPrivate, }; var log = new DocumentLog() From 01d9be7fcbe8fa645ecea9c28f288e5a1ba6da2f Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 10:10:26 +0700 Subject: [PATCH 2/7] add: get all issued documents --- src/Api/Controllers/DocumentsController.cs | 39 +++++++++- .../GetAllIssuedPaginatedQueryParameters.cs | 6 ++ .../Dtos/ImportDocument/IssuedDocumentDto.cs | 24 ++++++ .../Models/Dtos/ImportDocument/IssuerDto.cs | 17 +++++ .../Documents/Commands/ImportDocument.cs | 9 ++- .../Queries/GetAllIssuedDocumentsPaginated.cs | 74 +++++++++++++++++++ 6 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs create mode 100644 src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs create mode 100644 src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index ef6034bf..2d30d3a9 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,10 +1,13 @@ using Api.Controllers.Payload.Requests.Documents; +using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; using Application.Identity; +using FluentValidation.Results; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -38,6 +41,38 @@ public async Task>> GetById([FromRoute] Guid do return Ok(Result.Succeed(result)); } + /// + /// Get all documents paginated + /// + /// Get all documents query parameters + /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Staff)] + [HttpGet("issued")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllIssuedPaginated( + [FromQuery] GetAllIssuedPaginatedQueryParameters queryParameters) + { + var departmentId = _currentUserService.GetCurrentDepartmentForStaff(); + if (departmentId is null) + { + return Result>.Fail(new Exception("Staff does not have a room")); + } + var query = new GetAllIssuedDocumentsPaginated.Query() + { + DepartmentId = departmentId.Value, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get all documents paginated /// @@ -92,7 +127,7 @@ public async Task>>> GetAllDocumentTypes [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>> Import([FromBody] ImportDocumentRequest request) + public async Task>> Import([FromBody] ImportDocumentRequest request) { var userId = _currentUserService.GetId(); var command = new ImportDocument.Command() @@ -104,7 +139,7 @@ public async Task>> Import([FromBody] ImportDoc IsPrivate = request.IsPrivate, }; var result = await Mediator.Send(command); - return Ok(Result.Succeed(result)); + return Ok(Result.Succeed(result)); } /// diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs new file mode 100644 index 00000000..ed060421 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllIssuedPaginatedQueryParameters.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllIssuedPaginatedQueryParameters : PaginatedQueryParameters +{ + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs new file mode 100644 index 00000000..7d696712 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs @@ -0,0 +1,24 @@ +using Application.Common.Mappings; +using AutoMapper; +using Domain.Entities.Physical; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuedDocumentDto : IMapFrom +{ + public Guid Id { get; set; } + public string Title { get; set; } = null!; + public string? Description { get; set; } + public string DocumentType { get; set; } = null!; + public IssuerDto? Issuer { get; set; } + public string Status { get; set; } = null!; + + public void Mapping(Profile profile) + { + profile.CreateMap() + .ForMember(dest => dest.Status, + opt => opt.MapFrom(src => src.Status.ToString())) + .ForMember(dest => dest.Issuer, + opt => opt.MapFrom(x => x.Importer)); + } +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs new file mode 100644 index 00000000..3818dc77 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuerDto.cs @@ -0,0 +1,17 @@ +using Application.Common.Mappings; +using Domain.Entities; + +namespace Application.Common.Models.Dtos.ImportDocument; + +public class IssuerDto : IMapFrom +{ + public Guid Id { get; set; } + public string Username { get; set; } + public string Email { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Role { get; set; } + public string Position { get; set; } + public bool IsActive { get; set; } + public bool IsActivated { get; set; } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 2794555c..e6a7cf29 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -1,6 +1,7 @@ using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using AutoMapper; using Domain.Entities.Logging; @@ -15,7 +16,7 @@ namespace Application.Documents.Commands; public class ImportDocument { - public record Command : IRequest + public record Command : IRequest { public string Title { get; init; } = null!; public string? Description { get; init; } @@ -24,7 +25,7 @@ public record Command : IRequest public bool IsPrivate { get; init; } } - public class CommandHandler : IRequestHandler + public class CommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; @@ -38,7 +39,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper, ILogger Handle(Command request, CancellationToken cancellationToken) + public async Task Handle(Command request, CancellationToken cancellationToken) { var importer = await _context.Users .Include(x => x.Department) @@ -82,7 +83,7 @@ public async Task Handle(Command request, CancellationToken cancell var result = await _context.Documents.AddAsync(entity, cancellationToken); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); - return _mapper.Map(result.Entity); + return _mapper.Map(result.Entity); } } } \ No newline at end of file diff --git a/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs new file mode 100644 index 00000000..60a1cd78 --- /dev/null +++ b/src/Application/Documents/Queries/GetAllIssuedDocumentsPaginated.cs @@ -0,0 +1,74 @@ +using Application.Common.Extensions; +using Application.Common.Interfaces; +using Application.Common.Mappings; +using Application.Common.Models; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetAllIssuedDocumentsPaginated +{ + public record Query : IRequest> + { + public Guid DepartmentId { get; init; } + public string? SearchTerm { get; init; } + public int? Page { get; init; } + public int? Size { get; init; } + public string? SortBy { get; init; } + public string? SortOrder { get; init; } + } + + public class QueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle(Query request, + CancellationToken cancellationToken) + { + var documents = _context.Documents + .Include(x => x.Importer) + .Where(x => x.Status == DocumentStatus.Issued); + + documents = documents.Where(x => x.Department!.Id == request.DepartmentId); + + if (!(request.SearchTerm is null || request.SearchTerm.Trim().Equals(string.Empty))) + { + documents = documents.Where(x => + x.Title.ToLower().Contains(request.SearchTerm.ToLower())); + } + + var sortBy = request.SortBy; + if (sortBy is null || !sortBy.MatchesPropertyName()) + { + sortBy = nameof(IssuedDocumentDto.Id); + } + var sortOrder = request.SortOrder ?? "asc"; + var pageNumber = request.Page is null or <= 0 ? 1 : request.Page; + var sizeNumber = request.Size is null or <= 0 ? 5 : request.Size; + + var count = await documents.CountAsync(cancellationToken); + var list = await documents + .OrderByCustom(sortBy, sortOrder) + .Paginate(pageNumber.Value, sizeNumber.Value) + .ToListAsync(cancellationToken); + + var result = _mapper.Map>(list); + + return new PaginatedList(result, count, pageNumber.Value, sizeNumber.Value); + } + } +} \ No newline at end of file From 016c23179ba9a4cc47fa66e7dc472babf9f3c7c3 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 11:46:05 +0700 Subject: [PATCH 3/7] add: checkin endpoint --- src/Api/Controllers/DocumentsController.cs | 25 ++++++++ .../Common/Messages/DocumentLogMessages.cs | 1 + .../Documents/Commands/CheckinDocument.cs | 63 +++++++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/Application/Documents/Commands/CheckinDocument.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 4cc90691..776252d5 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -170,6 +170,31 @@ public async Task>> RequestImport([FromBo return Ok(Result.Succeed(result)); } + /// + /// Checkin a document + /// + /// + /// A DocumentDto of the imported document + [RequiresRole(IdentityData.Roles.Employee)] + [HttpPost("{documentId:guid}/checkin")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>> Checkin( + [FromRoute] Guid documentId) + { + var performingUserId = _currentUserService.GetId(); + var command = new CheckinDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + }; + var result = await Mediator.Send(command); + return Ok(Result.Succeed(result)); + } + /// /// Update a document /// diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index b6a62cae..08335797 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -6,5 +6,6 @@ public static class Import { public const string NewImport = "Imported new document"; public const string NewImportRequest = "Created new import request"; + public const string Checkin = "Checked in document"; } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/CheckinDocument.cs b/src/Application/Documents/Commands/CheckinDocument.cs new file mode 100644 index 00000000..58de128a --- /dev/null +++ b/src/Application/Documents/Commands/CheckinDocument.cs @@ -0,0 +1,63 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Entities.Physical; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class CheckinDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await + _context.Documents.FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + + if (document is null) + { + throw new KeyNotFoundException("Document does not exist."); + } + + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + document.Status = DocumentStatus.Available; + document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + document.LastModifiedBy = performingUser!.Id; + var log = new DocumentLog() + { + User = performingUser, + UserId = performingUser.Id, + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + Action = DocumentLogMessages.Import.Checkin, + }; + + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From 81be8e1b6fb789af173b1c1c3d3f706da15916e7 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 11:47:18 +0700 Subject: [PATCH 4/7] add: approve and reject endpoint for document --- src/Api/Controllers/DocumentsController.cs | 43 ++++++++++++ .../Common/Messages/DocumentLogMessages.cs | 2 + .../Documents/Commands/ApproveDocument.cs | 65 +++++++++++++++++++ .../Documents/Commands/RejectDocument.cs | 63 ++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 src/Application/Documents/Commands/ApproveDocument.cs create mode 100644 src/Application/Documents/Commands/RejectDocument.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 776252d5..3d7e9642 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,4 +1,5 @@ using Api.Controllers.Payload.Requests.Documents; +using Application.Borrows.Commands; using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; @@ -238,4 +239,46 @@ public async Task>> Delete([FromRoute] Guid doc var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Approve a document request + /// + /// Id of the document to be approved + /// A DocumentDto of the approved document + [HttpPost("approve/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Approve([FromRoute] Guid documentId) + { + var performingUserId = _currentUserService.GetId(); + var query = new ApproveDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } + + /// + /// Reject a document request + /// + /// Id of the document to be rejected + /// A DocumentDto of the rejected document + [HttpPost("reject/{documentId:guid}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Reject([FromRoute] Guid documentId) + { + var performingUserId = _currentUserService.GetId(); + var query = new RejectDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index 08335797..e0638ec7 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -7,5 +7,7 @@ public static class Import public const string NewImport = "Imported new document"; public const string NewImportRequest = "Created new import request"; public const string Checkin = "Checked in document"; + public const string Approve = "Approved import request"; + public const string Reject = "Rejected import request"; } } \ No newline at end of file diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/Documents/Commands/ApproveDocument.cs new file mode 100644 index 00000000..4b5f8441 --- /dev/null +++ b/src/Application/Documents/Commands/ApproveDocument.cs @@ -0,0 +1,65 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.ImportDocument; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using NodaTime; + +namespace Application.Documents.Commands; + +public class ApproveDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; set; } + public Guid DocumentId { get; set; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents.FirstOrDefaultAsync(x => + x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Issued) + { + throw new ConflictException("Request cannot be approved."); + } + + document.Status = DocumentStatus.Approved; + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new DocumentLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = DocumentLogMessages.Import.Approve, + }; + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/RejectDocument.cs b/src/Application/Documents/Commands/RejectDocument.cs new file mode 100644 index 00000000..e64c5896 --- /dev/null +++ b/src/Application/Documents/Commands/RejectDocument.cs @@ -0,0 +1,63 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class RejectDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; set; } + public Guid DocumentId { get; set; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents.FirstOrDefaultAsync(x => + x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Issued) + { + throw new ConflictException("Request cannot be rejected."); + } + + document.Status = DocumentStatus.Rejected; + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + var log = new DocumentLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = DocumentLogMessages.Import.Reject, + }; + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file From cf8e2d07ed91e47a572038cb8bf1b6b4ba110df5 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 21:03:47 +0700 Subject: [PATCH 5/7] finish: almost everything --- src/Api/Controllers/DocumentsController.cs | 64 ++++++++++++- .../AssignDocumentToFolderRequest.cs | 6 ++ ...cumentsForStaffPaginatedQueryParameters.cs | 21 +++++ .../Common/Messages/DocumentLogMessages.cs | 1 + .../Common/Messages/FolderLogMessage.cs | 1 + .../Dtos/ImportDocument/IssuedDocumentDto.cs | 1 + .../Documents/Commands/ApproveDocument.cs | 4 +- .../Documents/Commands/AssignDocument.cs | 90 +++++++++++++++++++ .../Documents/Commands/CheckinDocument.cs | 16 +++- .../Documents/Commands/ImportDocument.cs | 2 +- .../Documents/Commands/RejectDocument.cs | 4 +- .../Commands/RequestImportDocument.cs | 8 +- .../Documents/Commands/UpdateDocument.cs | 1 + src/Application/Staffs/Commands/AddStaff.cs | 9 +- 14 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs create mode 100644 src/Application/Documents/Commands/AssignDocument.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 3d7e9642..7989a3e5 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -79,12 +79,13 @@ public async Task>>> GetAll /// /// Get all documents query parameters /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Admin)] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task>>> GetAllPaginated( + public async Task>>> GetAllForAdminPaginated( [FromQuery] GetAllDocumentsPaginatedQueryParameters queryParameters) { var query = new GetAllDocumentsPaginated.Query() @@ -102,6 +103,36 @@ public async Task>>> GetAllPagina return Ok(Result>.Succeed(result)); } + /// + /// Get all documents for staff paginated + /// + /// Get all documents for staff query parameters + /// A paginated list of DocumentDto + [RequiresRole(IdentityData.Roles.Staff)] + [HttpGet("staff")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + public async Task>>> GetAllForStaffPaginated( + [FromQuery] GetAllDocumentsForStaffPaginatedQueryParameters queryParameters) + { + var roomId = _currentUserService.GetCurrentRoomForStaff(); + var query = new GetAllDocumentsPaginated.Query() + { + RoomId = roomId, + LockerId = queryParameters.LockerId, + FolderId = queryParameters.FolderId, + SearchTerm = queryParameters.SearchTerm, + Page = queryParameters.Page, + Size = queryParameters.Size, + SortBy = queryParameters.SortBy, + SortOrder = queryParameters.SortOrder, + }; + var result = await Mediator.Send(query); + return Ok(Result>.Succeed(result)); + } + /// /// Get all document types /// @@ -176,7 +207,7 @@ public async Task>> RequestImport([FromBo /// /// /// A DocumentDto of the imported document - [RequiresRole(IdentityData.Roles.Employee)] + [RequiresRole(IdentityData.Roles.Staff)] [HttpPost("{documentId:guid}/checkin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -245,7 +276,7 @@ public async Task>> Delete([FromRoute] Guid doc /// /// Id of the document to be approved /// A DocumentDto of the approved document - [HttpPost("approve/{documentId:guid}")] + [HttpPost("{documentId:guid}/approve")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -266,7 +297,7 @@ public async Task>> Approve([FromRoute] Guid do /// /// Id of the document to be rejected /// A DocumentDto of the rejected document - [HttpPost("reject/{documentId:guid}")] + [HttpPost("{documentId:guid}/reject")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -281,4 +312,29 @@ public async Task>> Reject([FromRoute] Guid doc var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Assign a document to + /// + /// Id of the document to be rejected + /// + /// A DocumentDto of the rejected document + [HttpPost("{documentId:guid}/assign")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Assign( + [FromRoute] Guid documentId, + [FromBody] AssignDocumentToFolderRequest request) + { + var performingUserId = _currentUserService.GetId(); + var query = new AssignDocument.Command() + { + PerformingUserId = performingUserId, + DocumentId = documentId, + FolderId = request.FolderId, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } } \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs new file mode 100644 index 00000000..bcca52f5 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/AssignDocumentToFolderRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class AssignDocumentToFolderRequest +{ + public Guid FolderId { get; set; } +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs new file mode 100644 index 00000000..7860021d --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/GetAllDocumentsForStaffPaginatedQueryParameters.cs @@ -0,0 +1,21 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class GetAllDocumentsForStaffPaginatedQueryParameters : PaginatedQueryParameters +{ + /// + /// Id of the room to find documents in + /// + public Guid? RoomId { get; set; } + /// + /// Id of the locker to find documents in + /// + public Guid? LockerId { get; set; } + /// + /// Id of the folder to find documents in + /// + public Guid? FolderId { get; set; } + /// + /// Search term + /// + public string? SearchTerm { get; set; } +} \ No newline at end of file diff --git a/src/Application/Common/Messages/DocumentLogMessages.cs b/src/Application/Common/Messages/DocumentLogMessages.cs index e0638ec7..5fb37c1f 100644 --- a/src/Application/Common/Messages/DocumentLogMessages.cs +++ b/src/Application/Common/Messages/DocumentLogMessages.cs @@ -9,5 +9,6 @@ public static class Import public const string Checkin = "Checked in document"; public const string Approve = "Approved import request"; public const string Reject = "Rejected import request"; + public const string Assign = "Assigned to a folder"; } } \ No newline at end of file diff --git a/src/Application/Common/Messages/FolderLogMessage.cs b/src/Application/Common/Messages/FolderLogMessage.cs index b6922a83..97f97818 100644 --- a/src/Application/Common/Messages/FolderLogMessage.cs +++ b/src/Application/Common/Messages/FolderLogMessage.cs @@ -4,4 +4,5 @@ public static class FolderLogMessage { public const string Add = "Added folder"; public const string Update = "Updated folder"; + public const string AssignDocument = "Assigned document to folder"; } \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs index 7d696712..1c573d00 100644 --- a/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs +++ b/src/Application/Common/Models/Dtos/ImportDocument/IssuedDocumentDto.cs @@ -12,6 +12,7 @@ public class IssuedDocumentDto : IMapFrom public string DocumentType { get; set; } = null!; public IssuerDto? Issuer { get; set; } public string Status { get; set; } = null!; + public bool IsPrivate { get; set; } public void Mapping(Profile profile) { diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/Documents/Commands/ApproveDocument.cs index 4b5f8441..ec33db31 100644 --- a/src/Application/Documents/Commands/ApproveDocument.cs +++ b/src/Application/Documents/Commands/ApproveDocument.cs @@ -34,7 +34,9 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { - var document = await _context.Documents.FirstOrDefaultAsync(x => + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { diff --git a/src/Application/Documents/Commands/AssignDocument.cs b/src/Application/Documents/Commands/AssignDocument.cs new file mode 100644 index 00000000..80096146 --- /dev/null +++ b/src/Application/Documents/Commands/AssignDocument.cs @@ -0,0 +1,90 @@ +using Application.Common.Exceptions; +using Application.Common.Interfaces; +using Application.Common.Messages; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Entities.Logging; +using Domain.Statuses; +using MediatR; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Application.Documents.Commands; + +public class AssignDocument +{ + public record Command : IRequest + { + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public Guid FolderId { get; init; } + } + + public class CommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public CommandHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var document = await _context.Documents + .Include(x => x.Folder) + .FirstOrDefaultAsync(x => + x.Id == request.DocumentId, cancellationToken); + if (document is null) + { + throw new ConflictException("Document does not exist."); + } + + if (document.Status is not DocumentStatus.Approved) + { + throw new ConflictException("Document cannot be assigned."); + } + + var folder = await _context.Folders + .FirstOrDefaultAsync(x => x.Id == request.FolderId, cancellationToken); + + if (folder is null) + { + throw new ConflictException("Folder does not exist."); + } + + if (folder.NumberOfDocuments >= folder.Capacity) + { + throw new ConflictException("This folder cannot accept more documents."); + } + + var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); + document.Folder = folder; + document.LastModified = LocalDateTime.FromDateTime(DateTime.Now); + document.LastModifiedBy = performingUser!.Id; + var log = new DocumentLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = DocumentLogMessages.Import.Assign, + }; + var folderLog = new FolderLog() + { + Object = folder, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser!, + UserId = performingUser!.Id, + Action = FolderLogMessage.AssignDocument, + }; + var result = _context.Documents.Update(document); + await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.FolderLogs.AddAsync(folderLog, cancellationToken); + await _context.SaveChangesAsync(cancellationToken); + return _mapper.Map(result.Entity); + } + } +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/CheckinDocument.cs b/src/Application/Documents/Commands/CheckinDocument.cs index 58de128a..73351023 100644 --- a/src/Application/Documents/Commands/CheckinDocument.cs +++ b/src/Application/Documents/Commands/CheckinDocument.cs @@ -33,13 +33,25 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { - var document = await - _context.Documents.FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); + var document = await _context.Documents + .Include(x => x.Department) + .Include(x => x.Folder) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { throw new KeyNotFoundException("Document does not exist."); } + + if (document.Status is not DocumentStatus.Approved) + { + throw new ConflictException("Request cannot be checked in."); + } + + if (document.Folder is null) + { + throw new ConflictException("Request cannot be checked in."); + } var performingUser = await _context.Users.FirstOrDefaultAsync(x => x.Id == request.PerformingUserId, cancellationToken); document.Status = DocumentStatus.Available; diff --git a/src/Application/Documents/Commands/ImportDocument.cs b/src/Application/Documents/Commands/ImportDocument.cs index 4036556b..eaf17236 100644 --- a/src/Application/Documents/Commands/ImportDocument.cs +++ b/src/Application/Documents/Commands/ImportDocument.cs @@ -77,7 +77,7 @@ public async Task Handle(Command request, CancellationToken cancell Importer = importer, Department = importer.Department, Folder = folder, - Status = DocumentStatus.Issued, + Status = DocumentStatus.Available, IsPrivate = request.IsPrivate, Created = LocalDateTime.FromDateTime(DateTime.Now), CreatedBy = performingUser!.Id, diff --git a/src/Application/Documents/Commands/RejectDocument.cs b/src/Application/Documents/Commands/RejectDocument.cs index e64c5896..888bceb3 100644 --- a/src/Application/Documents/Commands/RejectDocument.cs +++ b/src/Application/Documents/Commands/RejectDocument.cs @@ -32,7 +32,9 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { - var document = await _context.Documents.FirstOrDefaultAsync(x => + var document = await _context.Documents + .Include(x => x.Department) + .FirstOrDefaultAsync(x => x.Id == request.DocumentId, cancellationToken); if (document is null) { diff --git a/src/Application/Documents/Commands/RequestImportDocument.cs b/src/Application/Documents/Commands/RequestImportDocument.cs index 431a4587..4efe3ca1 100644 --- a/src/Application/Documents/Commands/RequestImportDocument.cs +++ b/src/Application/Documents/Commands/RequestImportDocument.cs @@ -30,13 +30,10 @@ public class CommandHandler : IRequestHandler private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly ILogger _logger; - - public CommandHandler(IApplicationDbContext context, IMapper mapper, ILogger logger) + public CommandHandler(IApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; - _logger = logger; } public async Task Handle(Command request, CancellationToken cancellationToken) @@ -66,6 +63,7 @@ public async Task Handle(Command request, CancellationToken c Importer = issuer, Department = issuer.Department, Status = DocumentStatus.Issued, + IsPrivate = request.IsPrivate, Created = LocalDateTime.FromDateTime(DateTime.Now), CreatedBy = issuer.Id, }; @@ -75,7 +73,7 @@ public async Task Handle(Command request, CancellationToken c Time = LocalDateTime.FromDateTime(DateTime.Now), User = issuer, UserId = issuer.Id, - Action = DocumentLogMessages.Import.NewImportRequest + Action = DocumentLogMessages.Import.NewImportRequest, }; var result = await _context.Documents.AddAsync(entity, cancellationToken); diff --git a/src/Application/Documents/Commands/UpdateDocument.cs b/src/Application/Documents/Commands/UpdateDocument.cs index d670ed86..442da5ce 100644 --- a/src/Application/Documents/Commands/UpdateDocument.cs +++ b/src/Application/Documents/Commands/UpdateDocument.cs @@ -51,6 +51,7 @@ public CommandHandler(IApplicationDbContext context, IMapper mapper) public async Task Handle(Command request, CancellationToken cancellationToken) { var document = await _context.Documents + .Include(x => x.Department) .Include( x => x.Importer) .FirstOrDefaultAsync( x => x.Id.Equals(request.DocumentId), cancellationToken); diff --git a/src/Application/Staffs/Commands/AddStaff.cs b/src/Application/Staffs/Commands/AddStaff.cs index cb381aae..343fb367 100644 --- a/src/Application/Staffs/Commands/AddStaff.cs +++ b/src/Application/Staffs/Commands/AddStaff.cs @@ -39,13 +39,20 @@ public async Task Handle(Command request, CancellationToken cancellati throw new KeyNotFoundException("User does not exist."); } - var room = await _context.Rooms.FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); + var room = await _context.Rooms + .Include(x => x.Staff) + .FirstOrDefaultAsync(x => x.Id == request.RoomId, cancellationToken); if (room is null) { throw new KeyNotFoundException("Room does not exist."); } + if (room.Staff is not null) + { + throw new ConflictException("Room already has a staff."); + } + var existedStaff = await _context.Staffs .Include(x => x.Room) .Include(x => x.User) From e7780a8f1c82a9ec23d3a97cc9c54be65d53ae28 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 21:37:33 +0700 Subject: [PATCH 6/7] reason --- src/Api/Controllers/DocumentsController.cs | 43 +- .../Documents/ApproveImportRequest.cs | 6 + .../Requests/Documents/RejectImportRequest.cs | 6 + .../Common/Messages/RequestLogMessages.cs | 7 + .../Common/Models/Dtos/ReasonDto.cs | 11 + .../Documents/Commands/ApproveDocument.cs | 15 +- .../Documents/Commands/RejectDocument.cs | 14 +- .../Documents/Queries/GetDocumentReason.cs | 43 + src/Domain/Entities/Logging/RequestLog.cs | 1 + ...0230613140433_RequestLogReason.Designer.cs | 1019 +++++++++++++++++ .../20230613140433_RequestLogReason.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 4 + 12 files changed, 1187 insertions(+), 11 deletions(-) create mode 100644 src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs create mode 100644 src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs create mode 100644 src/Application/Common/Messages/RequestLogMessages.cs create mode 100644 src/Application/Common/Models/Dtos/ReasonDto.cs create mode 100644 src/Application/Documents/Queries/GetDocumentReason.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs create mode 100644 src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 7989a3e5..04db12ee 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -1,14 +1,13 @@ using Api.Controllers.Payload.Requests.Documents; -using Application.Borrows.Commands; -using Application.Common.Exceptions; using Application.Common.Interfaces; using Application.Common.Models; +using Application.Common.Models.Dtos; using Application.Common.Models.Dtos.ImportDocument; using Application.Common.Models.Dtos.Physical; using Application.Documents.Commands; using Application.Documents.Queries; using Application.Identity; -using FluentValidation.Results; +using Domain.Enums; using Infrastructure.Identity.Authorization; using Microsoft.AspNetCore.Mvc; @@ -270,48 +269,78 @@ public async Task>> Delete([FromRoute] Guid doc var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - + /// /// Approve a document request /// /// Id of the document to be approved + /// /// A DocumentDto of the approved document [HttpPost("{documentId:guid}/approve")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Approve([FromRoute] Guid documentId) + public async Task>> Approve( + [FromRoute] Guid documentId, + [FromBody] ApproveImportRequest request) { var performingUserId = _currentUserService.GetId(); var query = new ApproveDocument.Command() { PerformingUserId = performingUserId, DocumentId = documentId, + Reason = request.Reason, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } - + /// /// Reject a document request /// /// Id of the document to be rejected + /// /// A DocumentDto of the rejected document [HttpPost("{documentId:guid}/reject")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> Reject([FromRoute] Guid documentId) + public async Task>> Reject( + [FromRoute] Guid documentId, + [FromBody] RejectImportRequest request) { var performingUserId = _currentUserService.GetId(); var query = new RejectDocument.Command() { PerformingUserId = performingUserId, DocumentId = documentId, + Reason = request.Reason, }; var result = await Mediator.Send(query); return Ok(Result.Succeed(result)); } + + /// + /// Get a document request reason + /// + /// Id of the document to be rejected + /// A DocumentDto of the rejected document + [RequiresRole(IdentityData.Roles.Staff)] + [HttpPost("{documentId:guid}/reason")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task>> Reason( + [FromRoute] Guid documentId) + { + var query = new GetDocumentReason.Query() + { + DocumentId = documentId, + Type = RequestType.Import, + }; + var result = await Mediator.Send(query); + return Ok(Result.Succeed(result)); + } /// /// Assign a document to diff --git a/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs new file mode 100644 index 00000000..d617bb8b --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/ApproveImportRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class ApproveImportRequest +{ + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs b/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs new file mode 100644 index 00000000..1e9c4b80 --- /dev/null +++ b/src/Api/Controllers/Payload/Requests/Documents/RejectImportRequest.cs @@ -0,0 +1,6 @@ +namespace Api.Controllers.Payload.Requests.Documents; + +public class RejectImportRequest +{ + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Common/Messages/RequestLogMessages.cs b/src/Application/Common/Messages/RequestLogMessages.cs new file mode 100644 index 00000000..ae9bab77 --- /dev/null +++ b/src/Application/Common/Messages/RequestLogMessages.cs @@ -0,0 +1,7 @@ +namespace Application.Common.Messages; + +public static class RequestLogMessages +{ + public const string ApproveImport = "Approved import request"; + public const string RejectImport = "Rejected import request"; +} \ No newline at end of file diff --git a/src/Application/Common/Models/Dtos/ReasonDto.cs b/src/Application/Common/Models/Dtos/ReasonDto.cs new file mode 100644 index 00000000..8e82c755 --- /dev/null +++ b/src/Application/Common/Models/Dtos/ReasonDto.cs @@ -0,0 +1,11 @@ +using Application.Common.Mappings; +using Domain.Entities.Logging; +using Domain.Enums; + +namespace Application.Common.Models.Dtos; + +public class ReasonDto : IMapFrom +{ + public RequestType Type { get; set; } + public string Reason { get; set; } = null!; +} \ No newline at end of file diff --git a/src/Application/Documents/Commands/ApproveDocument.cs b/src/Application/Documents/Commands/ApproveDocument.cs index ec33db31..15bb65cd 100644 --- a/src/Application/Documents/Commands/ApproveDocument.cs +++ b/src/Application/Documents/Commands/ApproveDocument.cs @@ -17,8 +17,9 @@ public class ApproveDocument { public record Command : IRequest { - public Guid PerformingUserId { get; set; } - public Guid DocumentId { get; set; } + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public string Reason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -58,8 +59,18 @@ public async Task Handle(Command request, CancellationToken cancell UserId = performingUser!.Id, Action = DocumentLogMessages.Import.Approve, }; + var requestLog = new RequestLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser, + UserId = performingUser.Id, + Action = RequestLogMessages.ApproveImport, + Reason = request.Reason, + }; var result = _context.Documents.Update(document); await _context.DocumentLogs.AddAsync(log, cancellationToken); + await _context.RequestLogs.AddAsync(requestLog, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map(result.Entity); } diff --git a/src/Application/Documents/Commands/RejectDocument.cs b/src/Application/Documents/Commands/RejectDocument.cs index 888bceb3..d58bb281 100644 --- a/src/Application/Documents/Commands/RejectDocument.cs +++ b/src/Application/Documents/Commands/RejectDocument.cs @@ -15,8 +15,9 @@ public class RejectDocument { public record Command : IRequest { - public Guid PerformingUserId { get; set; } - public Guid DocumentId { get; set; } + public Guid PerformingUserId { get; init; } + public Guid DocumentId { get; init; } + public string Reason { get; init; } = null!; } public class CommandHandler : IRequestHandler @@ -56,6 +57,15 @@ public async Task Handle(Command request, CancellationToken cancell UserId = performingUser!.Id, Action = DocumentLogMessages.Import.Reject, }; + var requestLog = new RequestLog() + { + Object = document, + Time = LocalDateTime.FromDateTime(DateTime.Now), + User = performingUser, + UserId = performingUser.Id, + Action = RequestLogMessages.RejectImport, + Reason = request.Reason, + }; var result = _context.Documents.Update(document); await _context.DocumentLogs.AddAsync(log, cancellationToken); await _context.SaveChangesAsync(cancellationToken); diff --git a/src/Application/Documents/Queries/GetDocumentReason.cs b/src/Application/Documents/Queries/GetDocumentReason.cs new file mode 100644 index 00000000..9088f785 --- /dev/null +++ b/src/Application/Documents/Queries/GetDocumentReason.cs @@ -0,0 +1,43 @@ +using Application.Common.Interfaces; +using Application.Common.Models.Dtos; +using Application.Common.Models.Dtos.Physical; +using AutoMapper; +using Domain.Enums; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace Application.Documents.Queries; + +public class GetDocumentReason +{ + public record Query : IRequest + { + public Guid DocumentId { get; init; } + public RequestType Type { get; set; } + } + + public class QueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public QueryHandler(IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + public async Task Handle(Query request, CancellationToken cancellationToken) + { + var log = await _context.RequestLogs + .FirstOrDefaultAsync(x => x.Object!.Id == request.DocumentId + && x.Type == request.Type, cancellationToken); + + if (log is null) + { + throw new KeyNotFoundException("Document does not have a request."); + } + + return _mapper.Map(log); + } + } +} \ No newline at end of file diff --git a/src/Domain/Entities/Logging/RequestLog.cs b/src/Domain/Entities/Logging/RequestLog.cs index fbf7d568..e9605ed3 100644 --- a/src/Domain/Entities/Logging/RequestLog.cs +++ b/src/Domain/Entities/Logging/RequestLog.cs @@ -7,4 +7,5 @@ namespace Domain.Entities.Logging; public class RequestLog : BaseLoggingEntity { public RequestType Type { get; set; } + public string Reason { get; set; } = null!; } \ No newline at end of file diff --git a/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs new file mode 100644 index 00000000..710cd220 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.Designer.cs @@ -0,0 +1,1019 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20230613140433_RequestLogReason")] + partial class RequestLogReason + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("Departments"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.ToTable("Entries"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.FileEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.UserGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.ToTable("UserGroups"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("DocumentLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("FolderLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("LockerLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("RoomLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("ObjectId") + .HasColumnType("uuid"); + + b.Property("Time") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ObjectId"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogs"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActualReturnTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowTime") + .HasColumnType("timestamp without time zone"); + + b.Property("BorrowerId") + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("DueTime") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("BorrowerId"); + + b.HasIndex("DocumentId"); + + b.ToTable("Borrows"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DocumentType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("EntryId") + .HasColumnType("uuid"); + + b.Property("FolderId") + .HasColumnType("uuid"); + + b.Property("ImporterId") + .HasColumnType("uuid"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("EntryId") + .IsUnique(); + + b.HasIndex("FolderId"); + + b.HasIndex("ImporterId"); + + b.ToTable("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LockerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfDocuments") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("LockerId"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfFolders") + .HasColumnType("integer"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId"); + + b.ToTable("Lockers"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.Property("DocumentId") + .HasColumnType("uuid"); + + b.Property("EmployeeId") + .HasColumnType("uuid"); + + b.Property("AllowedOperations") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("DocumentId", "EmployeeId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Capacity") + .HasColumnType("integer"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAvailable") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NumberOfLockers") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("DepartmentId") + .IsUnique(); + + b.ToTable("Rooms"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("UserId"); + + b.Property("RoomId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoomId") + .IsUnique(); + + b.ToTable("Staffs"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("ExpiryDateTime") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("JwtId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Token"); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.Property("TokenHash") + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsInvalidated") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("TokenHash"); + + b.HasIndex("UserId"); + + b.ToTable("ResetPasswordTokens"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp without time zone"); + + b.Property("CreatedBy") + .HasColumnType("uuid"); + + b.Property("DepartmentId") + .HasColumnType("uuid"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("FirstName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsActivated") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastModified") + .HasColumnType("timestamp without time zone"); + + b.Property("LastModifiedBy") + .HasColumnType("uuid"); + + b.Property("LastName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Position") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("DepartmentId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserGroupId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "UserGroupId"); + + b.HasIndex("UserGroupId"); + + b.ToTable("Memberships"); + }); + + modelBuilder.Entity("Domain.Entities.Digital.Entry", b => + { + b.HasOne("Domain.Entities.Digital.FileEntity", "File") + .WithOne() + .HasForeignKey("Domain.Entities.Digital.Entry", "FileId"); + + b.Navigation("File"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.DocumentLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.FolderLog", b => + { + b.HasOne("Domain.Entities.Physical.Folder", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.LockerLog", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RequestLog", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.RoomLog", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Object") + .WithMany() + .HasForeignKey("ObjectId"); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Logging.UserLog", b => + { + b.HasOne("Domain.Entities.User", "Object") + .WithMany() + .HasForeignKey("ObjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Object"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Borrow", b => + { + b.HasOne("Domain.Entities.User", "Borrower") + .WithMany() + .HasForeignKey("BorrowerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Borrower"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Document", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.HasOne("Domain.Entities.Digital.Entry", "Entry") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Document", "EntryId"); + + b.HasOne("Domain.Entities.Physical.Folder", "Folder") + .WithMany("Documents") + .HasForeignKey("FolderId"); + + b.HasOne("Domain.Entities.User", "Importer") + .WithMany() + .HasForeignKey("ImporterId"); + + b.Navigation("Department"); + + b.Navigation("Entry"); + + b.Navigation("Folder"); + + b.Navigation("Importer"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.HasOne("Domain.Entities.Physical.Locker", "Locker") + .WithMany("Folders") + .HasForeignKey("LockerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Locker"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithMany("Lockers") + .HasForeignKey("RoomId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Permission", b => + { + b.HasOne("Domain.Entities.Physical.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Document"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithOne("Room") + .HasForeignKey("Domain.Entities.Physical.Room", "DepartmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Staff", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("Domain.Entities.Physical.Staff", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.Physical.Room", "Room") + .WithOne("Staff") + .HasForeignKey("Domain.Entities.Physical.Staff", "RoomId"); + + b.Navigation("Room"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.ResetPasswordToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId"); + + b.Navigation("Department"); + }); + + modelBuilder.Entity("Memberships", b => + { + b.HasOne("Domain.Entities.Digital.UserGroup", null) + .WithMany() + .HasForeignKey("UserGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Domain.Entities.Department", b => + { + b.Navigation("Room"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Folder", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Locker", b => + { + b.Navigation("Folders"); + }); + + modelBuilder.Entity("Domain.Entities.Physical.Room", b => + { + b.Navigation("Lockers"); + + b.Navigation("Staff"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs new file mode 100644 index 00000000..5ca28aa1 --- /dev/null +++ b/src/Infrastructure/Persistence/Migrations/20230613140433_RequestLogReason.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Persistence.Migrations +{ + /// + public partial class RequestLogReason : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Reason", + table: "RequestLogs", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Reason", + table: "RequestLogs"); + } + } +} diff --git a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs index 679625fe..ac299d19 100644 --- a/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Infrastructure/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -201,6 +201,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ObjectId") .HasColumnType("uuid"); + b.Property("Reason") + .IsRequired() + .HasColumnType("text"); + b.Property("Time") .HasColumnType("timestamp without time zone"); From 4f45f17cf2907ea5e396b81f3bad59d030033fb7 Mon Sep 17 00:00:00 2001 From: Nguyen Quang Chien Date: Tue, 13 Jun 2023 21:52:36 +0700 Subject: [PATCH 7/7] rename endpoint --- src/Api/Controllers/DocumentsController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Api/Controllers/DocumentsController.cs b/src/Api/Controllers/DocumentsController.cs index 04db12ee..2a6b6036 100644 --- a/src/Api/Controllers/DocumentsController.cs +++ b/src/Api/Controllers/DocumentsController.cs @@ -207,7 +207,7 @@ public async Task>> RequestImport([FromBo /// /// A DocumentDto of the imported document [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("{documentId:guid}/checkin")] + [HttpPost("checkin{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] @@ -276,7 +276,7 @@ public async Task>> Delete([FromRoute] Guid doc /// Id of the document to be approved /// /// A DocumentDto of the approved document - [HttpPost("{documentId:guid}/approve")] + [HttpPost("approve/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -301,7 +301,7 @@ public async Task>> Approve( /// Id of the document to be rejected /// /// A DocumentDto of the rejected document - [HttpPost("{documentId:guid}/reject")] + [HttpPost("reject/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -326,7 +326,7 @@ public async Task>> Reject( /// Id of the document to be rejected /// A DocumentDto of the rejected document [RequiresRole(IdentityData.Roles.Staff)] - [HttpPost("{documentId:guid}/reason")] + [HttpPost("reason/{documentId:guid}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)]