diff --git a/FixIt-backend/src/FixIt.Api/Controllers/TicketNotesController.cs b/FixIt-backend/src/FixIt.Api/Controllers/TicketNotesController.cs new file mode 100644 index 0000000..2cf7efc --- /dev/null +++ b/FixIt-backend/src/FixIt.Api/Controllers/TicketNotesController.cs @@ -0,0 +1,50 @@ +using FixIt.Api.Services; +using FixIt.Domain.Entities; +using FixIt.Infrastructure.Persistence; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Security.Claims; + +namespace FixIt.Api.Controllers +{ + [ApiController] + [Route("api/tickets/{ticketId}/notes")] + [Authorize] + public class TicketNotesController : ControllerBase + { + private readonly ApplicationDbContext _context; + private readonly HistoryLogService _historyLogService; + + public TicketNotesController(ApplicationDbContext context, HistoryLogService historyLogService) + { + _context = context; + _historyLogService = historyLogService; + } + + [HttpPost] + public async Task AddNote(Guid ticketId, [FromBody] string content) + { + var ticket = await _context.Tickets.AnyAsync(t => t.Id == ticketId); + if (!ticket) return NotFound(); + + var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); + + var note = new TicketNote + { + Id = Guid.NewGuid(), + TicketId = ticketId, + AuthorId = userId, + Content = content, + CreatedAt = DateTime.UtcNow, + }; + + _context.TicketNotes.Add(note); + + await _historyLogService.AddHistoryLog(ticketId, userId, "Użytkownik dodaÅ‚ nowÄ… notatkÄ™."); + await _context.SaveChangesAsync(); + + return Ok(); + } + } +} diff --git a/FixIt-backend/src/FixIt.Api/Controllers/TicketsController.cs b/FixIt-backend/src/FixIt.Api/Controllers/TicketsController.cs index 1fa2844..84d5ce7 100644 --- a/FixIt-backend/src/FixIt.Api/Controllers/TicketsController.cs +++ b/FixIt-backend/src/FixIt.Api/Controllers/TicketsController.cs @@ -6,6 +6,7 @@ using FixIt.Domain.Enums; using FixIt.Api.Dtos; using FixIt.Domain.Entities; +using FixIt.Api.Services; namespace FixIt.Api.Controllers { @@ -15,10 +16,12 @@ namespace FixIt.Api.Controllers public class TicketsController : ControllerBase { private readonly ApplicationDbContext _context; + private readonly HistoryLogService _historyLogService; - public TicketsController(ApplicationDbContext context) + public TicketsController(ApplicationDbContext context, HistoryLogService historyLogService) { _context = context; + _historyLogService = historyLogService; } [HttpGet] @@ -69,7 +72,11 @@ public async Task GetAllTickets([FromQuery] TicketQueryParamsDto t.Status, t.CreatedAt, t.ClientId, - t.TechnicianId + $"{t.Client.FirstName} {t.Client.LastName}", + t.TechnicianId, + t.TechnicianId != null + ? $"{t.Technician!.FirstName} {t.Technician.LastName}" + : "Nieprzypisany" )) .ToListAsync(); @@ -82,6 +89,53 @@ public async Task GetAllTickets([FromQuery] TicketQueryParamsDto }); } + [HttpGet("{id}")] + public async Task> GetTicketDetails(Guid id) + { + var ticket = await _context.Tickets + .Include(t => t.Client) + .Include(t => t.Technician) + .Include(t => t.Notes).ThenInclude(n => n.Author) + .Include(t => t.HistoryLogs).ThenInclude(h => h.ChangedByUser) + .FirstOrDefaultAsync(t => t.Id == id); + + if (ticket == null) return NotFound("Nie znaleziono takiego zgÅ‚oszenia"); + + var detailsDto = new TicketDetailsDto( + ticket.Id, + ticket.Title, + ticket.Description, + ticket.Status, + ticket.CreatedAt, + ticket.ClientId, + $"{ticket.Client.FirstName} {ticket.Client.LastName}", + ticket.TechnicianId, + ticket.Technician != null + ? $"{ticket.Technician.FirstName} {ticket.Technician.LastName}" + : "Nieprzypisany", + ticket.Notes + .OrderByDescending(n => n.CreatedAt) + .Select(n => new NoteDto( + n.Id, + n.Content, + n.CreatedAt, + n.AuthorId, + $"{n.Author.FirstName} {n.Author.LastName}")) + .ToList(), + ticket.HistoryLogs + .OrderByDescending(h => h.CreatedAt) + .Select(h => new HistoryLogDto( + h.Id, + h.Description, + h.CreatedAt, + h.ChangedByUserId, + $"{h.ChangedByUser.FirstName} {h.ChangedByUser.LastName}")) + .ToList() + ); + + return Ok(detailsDto); + } + [HttpPost] [Authorize(Roles = "User")] public async Task CreateTicket(CreateTicketDto dto) @@ -108,7 +162,8 @@ public async Task CreateTicket(CreateTicketDto dto) [Authorize(Roles = "Technician,Admin")] public async Task UpdateStatus(Guid id, [FromBody] TicketStatus newStatus) { - var ticket = await _context.Tickets.FindAsync(id); + var ticket = await _context.Tickets + .FirstOrDefaultAsync(t => t.Id == id); if (ticket == null) return NotFound(); var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); @@ -116,7 +171,11 @@ public async Task UpdateStatus(Guid id, [FromBody] TicketStatus n if (!User.IsInRole("Admin") && ticket.TechnicianId != userId) return Forbid(); + string description = $"Zmiana statusu z {ticket.Status} na {newStatus}"; + ticket.Status = newStatus; + + await _historyLogService.AddHistoryLog(id, userId, description); await _context.SaveChangesAsync(); return NoContent(); @@ -162,9 +221,10 @@ public async Task ClaimTicket(Guid id) ticket.TechnicianId = technicianId; ticket.Status = TicketStatus.Assigned; - await _context.SaveChangesAsync(); + await _historyLogService.AddHistoryLog(id, technicianId, "Technik przypisaÅ‚ zgÅ‚oszenie do siebie."); - return Ok("ZgÅ‚oszenie zostaÅ‚o przypisane do Ciebie."); + await _context.SaveChangesAsync(); + return Ok("ZgÅ‚oszenie zostaÅ‚o przypisane."); } } } \ No newline at end of file diff --git a/FixIt-backend/src/FixIt.Api/Dtos/HistoryLogDto.cs b/FixIt-backend/src/FixIt.Api/Dtos/HistoryLogDto.cs new file mode 100644 index 0000000..0d8e1be --- /dev/null +++ b/FixIt-backend/src/FixIt.Api/Dtos/HistoryLogDto.cs @@ -0,0 +1,7 @@ +public record HistoryLogDto( + Guid Id, + string Description, + DateTime CreatedAt, + Guid ChangedByUserId, + string UserFullName +); \ No newline at end of file diff --git a/FixIt-backend/src/FixIt.Api/Dtos/NoteDto.cs b/FixIt-backend/src/FixIt.Api/Dtos/NoteDto.cs new file mode 100644 index 0000000..bc540f5 --- /dev/null +++ b/FixIt-backend/src/FixIt.Api/Dtos/NoteDto.cs @@ -0,0 +1,7 @@ +public record NoteDto( + Guid Id, + string Content, + DateTime CreatedAt, + Guid AuthorId, + string AuthorFullName +); \ No newline at end of file diff --git a/FixIt-backend/src/FixIt.Api/Dtos/TicketDto.cs b/FixIt-backend/src/FixIt.Api/Dtos/TicketDto.cs index 4489697..7f936de 100644 --- a/FixIt-backend/src/FixIt.Api/Dtos/TicketDto.cs +++ b/FixIt-backend/src/FixIt.Api/Dtos/TicketDto.cs @@ -3,11 +3,35 @@ namespace FixIt.Api.Dtos; public record CreateTicketDto(string Title, string Description); -public record TicketDto(Guid Id, string Title, string Description, TicketStatus Status, DateTime CreatedAt, Guid ClientId, Guid? TechnicianId); +public record TicketDto( + Guid Id, + string Title, + string Description, + TicketStatus Status, + DateTime CreatedAt, + Guid ClientId, + string ClientName, + Guid? TechnicianId, + string? TechnicianName +); public record TicketQueryParamsDto( int Page = 1, int PageSize = 10, string? Search = null, TicketStatus? Status = null, SortDirection Sort = SortDirection.Desc +); + +public record TicketDetailsDto( + Guid Id, + string Title, + string Description, + TicketStatus Status, + DateTime CreatedAt, + Guid ClientId, + string ClientName, + Guid? TechnicianId, + string? TechnicianName, + List Notes, + List HistoryLogs ); \ No newline at end of file diff --git a/FixIt-backend/src/FixIt.Api/FixIt.Api.http b/FixIt-backend/src/FixIt.Api/FixIt.Api.http index 97e67a8..a04fcd7 100644 --- a/FixIt-backend/src/FixIt.Api/FixIt.Api.http +++ b/FixIt-backend/src/FixIt.Api/FixIt.Api.http @@ -1,7 +1,9 @@ +# Zmienne globalne + @FixIt.Api_HostAddress = http://localhost:5013 -### Test rejestracji -POST http://localhost:5013/api/auth/register +# Test rejestracji +POST {{FixIt.Api_HostAddress}}/api/auth/register Content-Type: application/json { @@ -10,8 +12,11 @@ Content-Type: application/json "firstName": "Tester", "lastName": "Manualny" } -### Test logowania -POST http://localhost:5013/api/auth/login + +### + +# Test logowania i zapisanie tokena +POST {{FixIt.Api_HostAddress}}/api/auth/login Content-Type: application/json { @@ -19,23 +24,30 @@ Content-Type: application/json "password": "Password123!" } -@token = eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6ImMxZDExODI3LTJlNTctNDkwNC1hYzQwLTIwZWViN2ZiYjk4MCIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2VtYWlsYWRkcmVzcyI6InRlc3RAdXNlci5wbCIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6IlVzZXIiLCJmaXJzdE5hbWUiOiJUZXN0ZXIiLCJleHAiOjE3NzA2ODQzOTUsImlzcyI6IkZpeEl0QXBpIiwiYXVkIjoiRml4SXRGcm9udCJ9.F_I6Y0_rv8Gx5k_9Xu1STANcHu65QOiv5e1NLX6HRheVFmyaNMmrtoB_DbKZC5CbNL6Yndb1TNqlg9buzKa8KQ +### -### Pobieranie wszystkich ticketow +> {% client.global.set("token", response.body.token) %} +### + +# Pobieranie wszystkich ticketów GET {{FixIt.Api_HostAddress}}/api/tickets Authorization: Bearer {{token}} Accept: application/json -### Tworzenie nowego zgloszenia +### + +# Tworzenie nowego zg³oszenia POST {{FixIt.Api_HostAddress}}/api/tickets Authorization: Bearer {{token}} Content-Type: application/json { "title": "Zepsuta drukarka", - "description": "Drukarka w biurze nr 5 nie chce drukowac na czarno." + "description": "Drukarka w biurze nr 5 nie chce drukowaæ na czarno." } -### Przyjmowanie zgloszenia (Claim) -POST {{FixIt.Api_HostAddress}}/api/tickets/TUTAJ-GUID-Z-BAZY/claim +### + +# Przyjmowanie zg³oszenia (Claim) +POST {{FixIt.Api_HostAddress}}/api/tickets/38c5b462-9708-4faa-89af-977716a5a14d/claim Authorization: Bearer {{token}} \ No newline at end of file diff --git a/FixIt-backend/src/FixIt.Api/Program.cs b/FixIt-backend/src/FixIt.Api/Program.cs index d6b9c33..4255a16 100644 --- a/FixIt-backend/src/FixIt.Api/Program.cs +++ b/FixIt-backend/src/FixIt.Api/Program.cs @@ -82,6 +82,7 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddOpenApi(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/FixIt-backend/src/FixIt.Api/Services/HistoryLogService.cs b/FixIt-backend/src/FixIt.Api/Services/HistoryLogService.cs new file mode 100644 index 0000000..592d177 --- /dev/null +++ b/FixIt-backend/src/FixIt.Api/Services/HistoryLogService.cs @@ -0,0 +1,31 @@ +using FixIt.Domain.Entities; +using FixIt.Infrastructure.Persistence; + +namespace FixIt.Api.Services +{ + public class HistoryLogService + { + private readonly ApplicationDbContext _context; + + public HistoryLogService(ApplicationDbContext context) + { + _context = context; + } + + public async Task AddHistoryLog(Guid ticketId, Guid userId, string description) + { + var log = new TicketHistoryLog + { + Id = Guid.NewGuid(), + TicketId = ticketId, + ChangedByUserId = userId, + Description = description, + CreatedAt = DateTime.UtcNow, + }; + + _context.TicketHistoryLogs.Add(log); + + await _context.SaveChangesAsync(); + } + } +} diff --git a/FixIt-backend/src/FixIt.Domain/Entities/Ticket.cs b/FixIt-backend/src/FixIt.Domain/Entities/Ticket.cs index 40bebad..7123f06 100644 --- a/FixIt-backend/src/FixIt.Domain/Entities/Ticket.cs +++ b/FixIt-backend/src/FixIt.Domain/Entities/Ticket.cs @@ -13,10 +13,13 @@ public class Ticket public Guid ClientId { get; set; } [ForeignKey("ClientId")] - public User Client { get; set; } = null!; + public virtual User Client { get; set; } = null!; public Guid? TechnicianId { get; set; } [ForeignKey("TechnicianId")] - public User? Technician { get; set; } + public virtual User? Technician { get; set; } + + public virtual ICollection Notes { get; set; } = new List(); + public virtual ICollection HistoryLogs { get; set; } = new List(); } } diff --git a/FixIt-backend/src/FixIt.Domain/Entities/TicketHistoryLog.cs b/FixIt-backend/src/FixIt.Domain/Entities/TicketHistoryLog.cs new file mode 100644 index 0000000..f9e29d2 --- /dev/null +++ b/FixIt-backend/src/FixIt.Domain/Entities/TicketHistoryLog.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace FixIt.Domain.Entities +{ + public class TicketHistoryLog + { + public Guid Id { get; set; } + public Guid TicketId { get; set; } + public Guid ChangedByUserId { get; set; } + public string Description { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [ForeignKey("TicketId")] + public virtual Ticket Ticket { get; set; } = null!; + + [ForeignKey("ChangedByUserId")] + public virtual User ChangedByUser { get; set; } = null!; + } +} diff --git a/FixIt-backend/src/FixIt.Domain/Entities/TicketNote.cs b/FixIt-backend/src/FixIt.Domain/Entities/TicketNote.cs new file mode 100644 index 0000000..ba26dda --- /dev/null +++ b/FixIt-backend/src/FixIt.Domain/Entities/TicketNote.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace FixIt.Domain.Entities +{ + public class TicketNote + { + public Guid Id { get; set; } + public string Content { get; set; } = string.Empty; + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + public Guid TicketId { get; set; } + public Guid AuthorId { get; set; } + + [ForeignKey("TicketId")] + public virtual Ticket Ticket { get; set; } = null!; + + [ForeignKey("AuthorId")] + public virtual User Author { get; set; } = null!; + } +} diff --git a/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232228_AddTicketRelationsAndAttributes.Designer.cs b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232228_AddTicketRelationsAndAttributes.Designer.cs new file mode 100644 index 0000000..2c407ce --- /dev/null +++ b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232228_AddTicketRelationsAndAttributes.Designer.cs @@ -0,0 +1,219 @@ +// +using System; +using FixIt.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FixIt.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260212232228_AddTicketRelationsAndAttributes")] + partial class AddTicketRelationsAndAttributes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TechnicianId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("TechnicianId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketHistoryLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChangedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("TicketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketHistoryLog"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TicketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketNote"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.HasOne("FixIt.Domain.Entities.User", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.User", "Technician") + .WithMany() + .HasForeignKey("TechnicianId"); + + b.Navigation("Client"); + + b.Navigation("Technician"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketHistoryLog", b => + { + b.HasOne("FixIt.Domain.Entities.User", "ChangedByUser") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.Ticket", "Ticket") + .WithMany("HistoryLogs") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedByUser"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketNote", b => + { + b.HasOne("FixIt.Domain.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.Ticket", "Ticket") + .WithMany("Notes") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.Navigation("HistoryLogs"); + + b.Navigation("Notes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232228_AddTicketRelationsAndAttributes.cs b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232228_AddTicketRelationsAndAttributes.cs new file mode 100644 index 0000000..f07b7ee --- /dev/null +++ b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232228_AddTicketRelationsAndAttributes.cs @@ -0,0 +1,99 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FixIt.Infrastructure.Migrations +{ + /// + public partial class AddTicketRelationsAndAttributes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TicketHistoryLog", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TicketId = table.Column(type: "uuid", nullable: false), + ChangedByUserId = table.Column(type: "uuid", nullable: false), + Description = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TicketHistoryLog", x => x.Id); + table.ForeignKey( + name: "FK_TicketHistoryLog_Tickets_TicketId", + column: x => x.TicketId, + principalTable: "Tickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TicketHistoryLog_Users_ChangedByUserId", + column: x => x.ChangedByUserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TicketNote", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Content = table.Column(type: "text", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + TicketId = table.Column(type: "uuid", nullable: false), + AuthorId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TicketNote", x => x.Id); + table.ForeignKey( + name: "FK_TicketNote_Tickets_TicketId", + column: x => x.TicketId, + principalTable: "Tickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TicketNote_Users_AuthorId", + column: x => x.AuthorId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TicketHistoryLog_ChangedByUserId", + table: "TicketHistoryLog", + column: "ChangedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_TicketHistoryLog_TicketId", + table: "TicketHistoryLog", + column: "TicketId"); + + migrationBuilder.CreateIndex( + name: "IX_TicketNote_AuthorId", + table: "TicketNote", + column: "AuthorId"); + + migrationBuilder.CreateIndex( + name: "IX_TicketNote_TicketId", + table: "TicketNote", + column: "TicketId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TicketHistoryLog"); + + migrationBuilder.DropTable( + name: "TicketNote"); + } + } +} diff --git a/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232917_FixMissingTicketTables.Designer.cs b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232917_FixMissingTicketTables.Designer.cs new file mode 100644 index 0000000..0bd53a6 --- /dev/null +++ b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232917_FixMissingTicketTables.Designer.cs @@ -0,0 +1,219 @@ +// +using System; +using FixIt.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace FixIt.Infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260212232917_FixMissingTicketTables")] + partial class FixMissingTicketTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TechnicianId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("TechnicianId"); + + b.ToTable("Tickets"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketHistoryLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChangedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("TicketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketHistoryLogs"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TicketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketNotes"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.HasOne("FixIt.Domain.Entities.User", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.User", "Technician") + .WithMany() + .HasForeignKey("TechnicianId"); + + b.Navigation("Client"); + + b.Navigation("Technician"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketHistoryLog", b => + { + b.HasOne("FixIt.Domain.Entities.User", "ChangedByUser") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.Ticket", "Ticket") + .WithMany("HistoryLogs") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedByUser"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketNote", b => + { + b.HasOne("FixIt.Domain.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.Ticket", "Ticket") + .WithMany("Notes") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.Navigation("HistoryLogs"); + + b.Navigation("Notes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232917_FixMissingTicketTables.cs b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232917_FixMissingTicketTables.cs new file mode 100644 index 0000000..470872e --- /dev/null +++ b/FixIt-backend/src/FixIt.Infrastructure/Migrations/20260212232917_FixMissingTicketTables.cs @@ -0,0 +1,206 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FixIt.Infrastructure.Migrations +{ + /// + public partial class FixMissingTicketTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TicketHistoryLog_Tickets_TicketId", + table: "TicketHistoryLog"); + + migrationBuilder.DropForeignKey( + name: "FK_TicketHistoryLog_Users_ChangedByUserId", + table: "TicketHistoryLog"); + + migrationBuilder.DropForeignKey( + name: "FK_TicketNote_Tickets_TicketId", + table: "TicketNote"); + + migrationBuilder.DropForeignKey( + name: "FK_TicketNote_Users_AuthorId", + table: "TicketNote"); + + migrationBuilder.DropPrimaryKey( + name: "PK_TicketNote", + table: "TicketNote"); + + migrationBuilder.DropPrimaryKey( + name: "PK_TicketHistoryLog", + table: "TicketHistoryLog"); + + migrationBuilder.RenameTable( + name: "TicketNote", + newName: "TicketNotes"); + + migrationBuilder.RenameTable( + name: "TicketHistoryLog", + newName: "TicketHistoryLogs"); + + migrationBuilder.RenameIndex( + name: "IX_TicketNote_TicketId", + table: "TicketNotes", + newName: "IX_TicketNotes_TicketId"); + + migrationBuilder.RenameIndex( + name: "IX_TicketNote_AuthorId", + table: "TicketNotes", + newName: "IX_TicketNotes_AuthorId"); + + migrationBuilder.RenameIndex( + name: "IX_TicketHistoryLog_TicketId", + table: "TicketHistoryLogs", + newName: "IX_TicketHistoryLogs_TicketId"); + + migrationBuilder.RenameIndex( + name: "IX_TicketHistoryLog_ChangedByUserId", + table: "TicketHistoryLogs", + newName: "IX_TicketHistoryLogs_ChangedByUserId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_TicketNotes", + table: "TicketNotes", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_TicketHistoryLogs", + table: "TicketHistoryLogs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_TicketHistoryLogs_Tickets_TicketId", + table: "TicketHistoryLogs", + column: "TicketId", + principalTable: "Tickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TicketHistoryLogs_Users_ChangedByUserId", + table: "TicketHistoryLogs", + column: "ChangedByUserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TicketNotes_Tickets_TicketId", + table: "TicketNotes", + column: "TicketId", + principalTable: "Tickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TicketNotes_Users_AuthorId", + table: "TicketNotes", + column: "AuthorId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TicketHistoryLogs_Tickets_TicketId", + table: "TicketHistoryLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_TicketHistoryLogs_Users_ChangedByUserId", + table: "TicketHistoryLogs"); + + migrationBuilder.DropForeignKey( + name: "FK_TicketNotes_Tickets_TicketId", + table: "TicketNotes"); + + migrationBuilder.DropForeignKey( + name: "FK_TicketNotes_Users_AuthorId", + table: "TicketNotes"); + + migrationBuilder.DropPrimaryKey( + name: "PK_TicketNotes", + table: "TicketNotes"); + + migrationBuilder.DropPrimaryKey( + name: "PK_TicketHistoryLogs", + table: "TicketHistoryLogs"); + + migrationBuilder.RenameTable( + name: "TicketNotes", + newName: "TicketNote"); + + migrationBuilder.RenameTable( + name: "TicketHistoryLogs", + newName: "TicketHistoryLog"); + + migrationBuilder.RenameIndex( + name: "IX_TicketNotes_TicketId", + table: "TicketNote", + newName: "IX_TicketNote_TicketId"); + + migrationBuilder.RenameIndex( + name: "IX_TicketNotes_AuthorId", + table: "TicketNote", + newName: "IX_TicketNote_AuthorId"); + + migrationBuilder.RenameIndex( + name: "IX_TicketHistoryLogs_TicketId", + table: "TicketHistoryLog", + newName: "IX_TicketHistoryLog_TicketId"); + + migrationBuilder.RenameIndex( + name: "IX_TicketHistoryLogs_ChangedByUserId", + table: "TicketHistoryLog", + newName: "IX_TicketHistoryLog_ChangedByUserId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_TicketNote", + table: "TicketNote", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_TicketHistoryLog", + table: "TicketHistoryLog", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_TicketHistoryLog_Tickets_TicketId", + table: "TicketHistoryLog", + column: "TicketId", + principalTable: "Tickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TicketHistoryLog_Users_ChangedByUserId", + table: "TicketHistoryLog", + column: "ChangedByUserId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TicketNote_Tickets_TicketId", + table: "TicketNote", + column: "TicketId", + principalTable: "Tickets", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TicketNote_Users_AuthorId", + table: "TicketNote", + column: "AuthorId", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/FixIt-backend/src/FixIt.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/FixIt-backend/src/FixIt.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 5a5df7a..fea0417 100644 --- a/FixIt-backend/src/FixIt.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/FixIt-backend/src/FixIt.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -57,6 +57,62 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Tickets"); }); + modelBuilder.Entity("FixIt.Domain.Entities.TicketHistoryLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChangedByUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("TicketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ChangedByUserId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketHistoryLogs"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorId") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TicketId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TicketId"); + + b.ToTable("TicketNotes"); + }); + modelBuilder.Entity("FixIt.Domain.Entities.User", b => { b.Property("Id") @@ -109,6 +165,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Technician"); }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketHistoryLog", b => + { + b.HasOne("FixIt.Domain.Entities.User", "ChangedByUser") + .WithMany() + .HasForeignKey("ChangedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.Ticket", "Ticket") + .WithMany("HistoryLogs") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChangedByUser"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.TicketNote", b => + { + b.HasOne("FixIt.Domain.Entities.User", "Author") + .WithMany() + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("FixIt.Domain.Entities.Ticket", "Ticket") + .WithMany("Notes") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("FixIt.Domain.Entities.Ticket", b => + { + b.Navigation("HistoryLogs"); + + b.Navigation("Notes"); + }); #pragma warning restore 612, 618 } } diff --git a/FixIt-backend/src/FixIt.Infrastructure/Persistence/ApplicationDbContext.cs b/FixIt-backend/src/FixIt.Infrastructure/Persistence/ApplicationDbContext.cs index 674a254..190c878 100644 --- a/FixIt-backend/src/FixIt.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/FixIt-backend/src/FixIt.Infrastructure/Persistence/ApplicationDbContext.cs @@ -12,5 +12,7 @@ public ApplicationDbContext(DbContextOptions options) : ba public DbSet Users { get; set; } public DbSet Tickets { get; set; } + public DbSet TicketNotes { get; set; } + public DbSet TicketHistoryLogs { get; set; } } } diff --git a/FixIt-frontend/src/common/components/InitialsAvatar/InitialsAvatar.tsx b/FixIt-frontend/src/common/components/InitialsAvatar/InitialsAvatar.tsx new file mode 100644 index 0000000..a708d0e --- /dev/null +++ b/FixIt-frontend/src/common/components/InitialsAvatar/InitialsAvatar.tsx @@ -0,0 +1,17 @@ +import { Avatar } from "@mantine/core"; + +import { getColorFromName, getInitials } from "../../utils/avatar"; + +interface InitialsAvatarProps { + fullName: string; +} + +const InitialsAvatar = ({ fullName }: InitialsAvatarProps) => { + return ( + + {getInitials(fullName)} + + ); +}; + +export default InitialsAvatar; diff --git a/FixIt-frontend/src/common/components/Modal/Modal.tsx b/FixIt-frontend/src/common/components/Modal/Modal.tsx index 75bafad..08493cc 100644 --- a/FixIt-frontend/src/common/components/Modal/Modal.tsx +++ b/FixIt-frontend/src/common/components/Modal/Modal.tsx @@ -6,6 +6,7 @@ interface ModalProps { maxWidth?: number | string; children: React.ReactNode; closeOnBackdropClick?: boolean; + isButtonClose?: boolean; onClose: () => void; } @@ -15,6 +16,7 @@ const Modal = ({ maxWidth = 500, children, closeOnBackdropClick = true, + isButtonClose = false, onClose, }: ModalProps) => { return ( @@ -22,7 +24,7 @@ const Modal = ({ opened={isOpen} onClose={onClose} centered - withCloseButton={false} + withCloseButton={isButtonClose} closeOnEscape closeOnClickOutside={closeOnBackdropClick} keepMounted={keepMounted} diff --git a/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/DetailsTab/DetailsTab.tsx b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/DetailsTab/DetailsTab.tsx new file mode 100644 index 0000000..38a0a95 --- /dev/null +++ b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/DetailsTab/DetailsTab.tsx @@ -0,0 +1,65 @@ +import { + Card, + Text, + Group, + Badge, + Stack, + Divider, + SimpleGrid, +} from "@mantine/core"; + +import InfoItem from "./InfoItem/InfoItem"; + +import { + statusLabelMap, + statusColorMap, + type TicketDetails, +} from "../../../../types/tickets"; + +type TicketDetailsData = Omit; + +interface DetailsTabProps { + ticket: TicketDetailsData; +} + +const DetailsTab = ({ ticket }: DetailsTabProps) => { + return ( + + + + + + {ticket.title} + + + {statusLabelMap[ticket.status]} + + + + {ticket.description || "Brak opisu zgÅ‚oszenia."} + + + + + + Szczegóły zgÅ‚oszenia + + + + + + + + + + ); +}; + +export default DetailsTab; diff --git a/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/DetailsTab/InfoItem/InfoItem.tsx b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/DetailsTab/InfoItem/InfoItem.tsx new file mode 100644 index 0000000..5e6410e --- /dev/null +++ b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/DetailsTab/InfoItem/InfoItem.tsx @@ -0,0 +1,19 @@ +import { Box, Text } from "@mantine/core"; + +interface InfoItemProps { + label: string; + value?: string; +} + +const InfoItem = ({ label, value }: InfoItemProps) => ( + + + {label} + + + {value || "---"} + + +); + +export default InfoItem; diff --git a/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/HistoryTab/HistoryTab.tsx b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/HistoryTab/HistoryTab.tsx new file mode 100644 index 0000000..54861b2 --- /dev/null +++ b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/HistoryTab/HistoryTab.tsx @@ -0,0 +1,38 @@ +import { Stack, Group, Text, Card, Box } from "@mantine/core"; + +import InitialsAvatar from "../../../InitialsAvatar/InitialsAvatar"; + +import type { HistoryLog } from "../../../../types/tickets"; + +interface HistoryTabProps { + history: HistoryLog[]; +} + +const HistoryTab = ({ history }: HistoryTabProps) => { + return ( + + + {history?.map((log) => ( + + + + + + {log.userFullName} + + {new Date(log.createdAt).toLocaleString()} + + + + {log.description} + + + + + ))} + + + ); +}; + +export default HistoryTab; diff --git a/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/NotesTab/NotesTab.tsx b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/NotesTab/NotesTab.tsx new file mode 100644 index 0000000..3dc8753 --- /dev/null +++ b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/NotesTab/NotesTab.tsx @@ -0,0 +1,5 @@ +const ActivityTab = () => { + return
ActivityTab
; +}; + +export default ActivityTab; diff --git a/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/TicketDetailsModal.tsx b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/TicketDetailsModal.tsx new file mode 100644 index 0000000..d92769e --- /dev/null +++ b/FixIt-frontend/src/common/components/TicketList/TicketDetailsModal/TicketDetailsModal.tsx @@ -0,0 +1,67 @@ +import { Box, LoadingOverlay, Tabs } from "@mantine/core"; + +import DetailsTab from "./DetailsTab/DetailsTab"; +import HistoryTab from "./HistoryTab/HistoryTab"; +import NotesTab from "./NotesTab/NotesTab"; + +import { useGetTicketDetailsQuery } from "../../../../store/tickets/ticketsApi"; + +interface TicketDetailsModalProps { + ticketId: string; +} + +const TicketDetailsModal = ({ ticketId }: TicketDetailsModalProps) => { + const { + data: ticket, + isLoading, + isError, + } = useGetTicketDetailsQuery(ticketId); + + if (isLoading) { + return ( + + + + ); + } + + if (isError || !ticket) { + return null; + } + + return ( + + + + + Szczegóły + Notatki + Historia + + + + + + + + + + + + + ); +}; + +export default TicketDetailsModal; diff --git a/FixIt-frontend/src/common/components/TicketList/TicketItem/TicketItem.tsx b/FixIt-frontend/src/common/components/TicketList/TicketItem/TicketItem.tsx index 85153c8..c05c13e 100644 --- a/FixIt-frontend/src/common/components/TicketList/TicketItem/TicketItem.tsx +++ b/FixIt-frontend/src/common/components/TicketList/TicketItem/TicketItem.tsx @@ -1,4 +1,10 @@ -import { Badge, Card, Group, Stack, Text, Box } from "@mantine/core"; +import { useState } from "react"; +import { Badge, Card, Group, Stack, Text, Box, Button } from "@mantine/core"; + +import Modal from "../../Modal/Modal"; +import TicketDetailsModal from "../TicketDetailsModal/TicketDetailsModal"; + +import { textTruncate } from "../../../utils/textTruncate"; import { statusColorMap, @@ -8,47 +14,45 @@ import { interface TicketItemProps { ticket: Ticket; - rightSection?: React.ReactNode; - onClick?: (ticket: Ticket) => void; } -const TicketItem = ({ ticket, rightSection, onClick }: TicketItemProps) => { +const TicketItem = ({ ticket }: TicketItemProps) => { + const [isOpen, setIsOpen] = useState(false); + return ( - onClick?.(ticket)} - > - - - - {ticket.title} - - {statusLabelMap[ticket.status]} - - - - {ticket.description} - - - - {new Date(ticket.createdAt).toLocaleDateString()} - - - {ticket.technicianId - ? `Technik: ${ticket.technicianId}` - : "Brak przypisanego technika"} + <> + + + + + {ticket.title} + + {statusLabelMap[ticket.status]} + + + + {textTruncate(ticket.description, 100)} - - - {rightSection && {rightSection}} - - + + + {new Date(ticket.createdAt).toLocaleDateString()} + + + + + + + + + setIsOpen(false)} + isButtonClose + maxWidth={1200} + > + + + ); }; diff --git a/FixIt-frontend/src/common/components/TicketList/TicketList.tsx b/FixIt-frontend/src/common/components/TicketList/TicketList.tsx index a08ddd0..59b0b98 100644 --- a/FixIt-frontend/src/common/components/TicketList/TicketList.tsx +++ b/FixIt-frontend/src/common/components/TicketList/TicketList.tsx @@ -7,10 +7,9 @@ import type { Ticket } from "../../types/tickets"; interface TicketListProps { tickets: Ticket[]; emptyLabel?: string; - onItemClick?: (ticket: Ticket) => void; } -const TicketList = ({ tickets, emptyLabel, onItemClick }: TicketListProps) => { +const TicketList = ({ tickets, emptyLabel }: TicketListProps) => { if (tickets.length === 0) { return ( @@ -22,7 +21,7 @@ const TicketList = ({ tickets, emptyLabel, onItemClick }: TicketListProps) => { return ( {tickets.map((ticket) => ( - + ))} ); diff --git a/FixIt-frontend/src/common/types/tickets.ts b/FixIt-frontend/src/common/types/tickets.ts index cba9335..1fc480a 100644 --- a/FixIt-frontend/src/common/types/tickets.ts +++ b/FixIt-frontend/src/common/types/tickets.ts @@ -17,6 +17,36 @@ export interface Ticket { technicianId?: string; } +export interface TicketNote { + id: string; + content: string; + createdAt: string; + authorId: string; + authorFullName: string; +} + +export interface HistoryLog { + id: string; + description: string; + createdAt: string; + changedByUserId: string; + userFullName: string; +} + +export interface TicketDetails { + id: string; + title: string; + description: string; + status: TicketStatus; + createdAt: string; + clientId: string; + clientName: string; + technicianId?: string; + technicianName?: string; + ticketNotes: TicketNote[]; + historyLogs: HistoryLog[]; +} + export const statusLabelMap: Record = { New: "Nowe", Assigned: "Przypisane", diff --git a/FixIt-frontend/src/common/utils/avatar.ts b/FixIt-frontend/src/common/utils/avatar.ts new file mode 100644 index 0000000..34cb03b --- /dev/null +++ b/FixIt-frontend/src/common/utils/avatar.ts @@ -0,0 +1,17 @@ +const colors = ["fixit-blue", "fixit-success", "fixit-warning", "fixit-error"]; + +export const getInitials = (name: string): string => { + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); +}; + +export const getColorFromName = (name: string): string => { + const hash = name + .split("") + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + return colors[hash % colors.length]; +}; diff --git a/FixIt-frontend/src/common/utils/textTruncate.ts b/FixIt-frontend/src/common/utils/textTruncate.ts new file mode 100644 index 0000000..8e5db72 --- /dev/null +++ b/FixIt-frontend/src/common/utils/textTruncate.ts @@ -0,0 +1,3 @@ +export const textTruncate = (str: string, number: number): string => { + return str && str.length > number ? str.slice(0, number) + "..." : str; +}; diff --git a/FixIt-frontend/src/components/User/UserTickets/UserTicketList/UserTicketList.tsx b/FixIt-frontend/src/components/User/UserTickets/UserTicketList/UserTicketList.tsx index 873f178..4a7907c 100644 --- a/FixIt-frontend/src/components/User/UserTickets/UserTicketList/UserTicketList.tsx +++ b/FixIt-frontend/src/components/User/UserTickets/UserTicketList/UserTicketList.tsx @@ -52,7 +52,7 @@ const UserTicketList = () => { return ( <> - + diff --git a/FixIt-frontend/src/store/tickets/ticketsApi.ts b/FixIt-frontend/src/store/tickets/ticketsApi.ts index 54ac56e..35b04fe 100644 --- a/FixIt-frontend/src/store/tickets/ticketsApi.ts +++ b/FixIt-frontend/src/store/tickets/ticketsApi.ts @@ -3,6 +3,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; import type { GetTicketsParams, PaginatedTicketsResponse, + TicketDetailsResponse, TicketRequest, TicketResponse, } from "./ticketsApi.types"; @@ -26,6 +27,10 @@ export const ticketsApi = createApi({ }), providesTags: ["Tickets"], }), + getTicketDetails: builder.query({ + query: (id) => `tickets/${id}`, + providesTags: (_r, _e, id) => [{ type: "Tickets", id }], + }), createTicket: builder.mutation({ query: (body) => ({ url: "tickets", @@ -37,4 +42,8 @@ export const ticketsApi = createApi({ }), }); -export const { useGetAllTicketsQuery, useCreateTicketMutation } = ticketsApi; +export const { + useGetAllTicketsQuery, + useGetTicketDetailsQuery, + useCreateTicketMutation, +} = ticketsApi; diff --git a/FixIt-frontend/src/store/tickets/ticketsApi.types.ts b/FixIt-frontend/src/store/tickets/ticketsApi.types.ts index 394f616..93de7bb 100644 --- a/FixIt-frontend/src/store/tickets/ticketsApi.types.ts +++ b/FixIt-frontend/src/store/tickets/ticketsApi.types.ts @@ -36,3 +36,33 @@ export interface PaginatedTicketsResponse { pageSize: number; result: TicketResponse[]; } + +export interface TicketNote { + id: string; + content: string; + createdAt: string; + authorId: string; + authorFullName: string; +} + +export interface HistoryLog { + id: string; + description: string; + createdAt: string; + changedByUserId: string; + userFullName: string; +} + +export interface TicketDetailsResponse { + id: string; + title: string; + description: string; + status: TicketStatus; + createdAt: string; + clientId: string; + clientName: string; + technicianId?: string; + technicianName?: string; + ticketNotes: TicketNote[]; + historyLogs: HistoryLog[]; +}