Spaces:
Running
Running
User commited on
Commit Β·
8a8894d
1
Parent(s): f608ab4
feat: remove file attachment
Browse files- TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs +0 -18
- TaskTrackingSystem.Database/AppDbContextModels/FileAttachment.cs +0 -31
- TaskTrackingSystem.Database/AppDbContextModels/Task.cs +0 -2
- TaskTrackingSystem.Shared/Models/Attachment/FileAttachmentDto.cs +0 -16
- TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs +0 -153
- TaskTrackingSystem.WebApp/Components/Pages/Backlog.razor +1 -1
- TaskTrackingSystem.WebApp/Components/Pages/TaskDetails.razor +55 -249
TaskTrackingSystem.Database/AppDbContextModels/AppDbContext.cs
CHANGED
|
@@ -20,7 +20,6 @@ public partial class AppDbContext : DbContext
|
|
| 20 |
|
| 21 |
public virtual DbSet<Comment> Comments { get; set; }
|
| 22 |
|
| 23 |
-
public virtual DbSet<FileAttachment> FileAttachments { get; set; }
|
| 24 |
|
| 25 |
public virtual DbSet<Menu> Menus { get; set; }
|
| 26 |
|
|
@@ -98,27 +97,10 @@ public partial class AppDbContext : DbContext
|
|
| 98 |
.HasConstraintName("FK_Comments_Users");
|
| 99 |
});
|
| 100 |
|
| 101 |
-
modelBuilder.Entity<FileAttachment>(entity =>
|
| 102 |
-
{
|
| 103 |
-
entity.HasIndex(e => e.TaskId, "IX_FileAttachments_TaskId");
|
| 104 |
-
|
| 105 |
-
entity.Property(e => e.CreatedAt)
|
| 106 |
-
.HasDefaultValueSql("(getdate())")
|
| 107 |
-
.HasColumnType("datetime");
|
| 108 |
-
entity.Property(e => e.FileName).HasMaxLength(255);
|
| 109 |
-
entity.Property(e => e.FileType).HasMaxLength(100);
|
| 110 |
-
entity.Property(e => e.UpdatedAt).HasColumnType("datetime");
|
| 111 |
|
| 112 |
-
entity.HasOne(d => d.Task).WithMany(p => p.FileAttachments)
|
| 113 |
-
.HasForeignKey(d => d.TaskId)
|
| 114 |
-
.OnDelete(DeleteBehavior.ClientSetNull)
|
| 115 |
-
.HasConstraintName("FK_FileAttachments_Tasks");
|
| 116 |
-
});
|
| 117 |
|
| 118 |
modelBuilder.Entity<Menu>(entity =>
|
| 119 |
{
|
| 120 |
-
entity.HasIndex(e => e.ParentMenuId, "IX_Menus_ParentMenuId");
|
| 121 |
-
|
| 122 |
entity.HasIndex(e => e.MenuCode, "UQ_Menus_MenuCode").IsUnique();
|
| 123 |
|
| 124 |
entity.Property(e => e.CreatedAt)
|
|
|
|
| 20 |
|
| 21 |
public virtual DbSet<Comment> Comments { get; set; }
|
| 22 |
|
|
|
|
| 23 |
|
| 24 |
public virtual DbSet<Menu> Menus { get; set; }
|
| 25 |
|
|
|
|
| 97 |
.HasConstraintName("FK_Comments_Users");
|
| 98 |
});
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
modelBuilder.Entity<Menu>(entity =>
|
| 103 |
{
|
|
|
|
|
|
|
| 104 |
entity.HasIndex(e => e.MenuCode, "UQ_Menus_MenuCode").IsUnique();
|
| 105 |
|
| 106 |
entity.Property(e => e.CreatedAt)
|
TaskTrackingSystem.Database/AppDbContextModels/FileAttachment.cs
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
ο»Ώusing System;
|
| 2 |
-
using System.Collections.Generic;
|
| 3 |
-
|
| 4 |
-
namespace TaskTrackingSystem.Database.AppDbContextModels;
|
| 5 |
-
|
| 6 |
-
public partial class FileAttachment
|
| 7 |
-
{
|
| 8 |
-
public long Id { get; set; }
|
| 9 |
-
|
| 10 |
-
public long TaskId { get; set; }
|
| 11 |
-
|
| 12 |
-
public string FileName { get; set; } = null!;
|
| 13 |
-
|
| 14 |
-
public string FileType { get; set; } = null!;
|
| 15 |
-
|
| 16 |
-
public string FilePath { get; set; } = null!;
|
| 17 |
-
|
| 18 |
-
public long FileSizeInBytes { get; set; }
|
| 19 |
-
|
| 20 |
-
public DateTime? CreatedAt { get; set; }
|
| 21 |
-
|
| 22 |
-
public long? CreatedBy { get; set; }
|
| 23 |
-
|
| 24 |
-
public DateTime? UpdatedAt { get; set; }
|
| 25 |
-
|
| 26 |
-
public long? UpdatedBy { get; set; }
|
| 27 |
-
|
| 28 |
-
public bool IsDeleted { get; set; }
|
| 29 |
-
|
| 30 |
-
public virtual Task Task { get; set; } = null!;
|
| 31 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskTrackingSystem.Database/AppDbContextModels/Task.cs
CHANGED
|
@@ -42,8 +42,6 @@ public partial class Task
|
|
| 42 |
|
| 43 |
public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();
|
| 44 |
|
| 45 |
-
public virtual ICollection<FileAttachment> FileAttachments { get; set; } = new List<FileAttachment>();
|
| 46 |
-
|
| 47 |
public virtual Project Project { get; set; } = null!;
|
| 48 |
|
| 49 |
public virtual ICollection<TaskHistory> TaskHistories { get; set; } = new List<TaskHistory>();
|
|
|
|
| 42 |
|
| 43 |
public virtual ICollection<Comment> Comments { get; set; } = new List<Comment>();
|
| 44 |
|
|
|
|
|
|
|
| 45 |
public virtual Project Project { get; set; } = null!;
|
| 46 |
|
| 47 |
public virtual ICollection<TaskHistory> TaskHistories { get; set; } = new List<TaskHistory>();
|
TaskTrackingSystem.Shared/Models/Attachment/FileAttachmentDto.cs
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
using System;
|
| 2 |
-
|
| 3 |
-
namespace TaskTrackingSystem.Shared.Models.Attachment
|
| 4 |
-
{
|
| 5 |
-
public class FileAttachmentDto
|
| 6 |
-
{
|
| 7 |
-
public long Id { get; set; }
|
| 8 |
-
public long TaskId { get; set; }
|
| 9 |
-
public string FileName { get; set; } = null!;
|
| 10 |
-
public string FileType { get; set; } = null!;
|
| 11 |
-
public long FileSizeInBytes { get; set; }
|
| 12 |
-
public DateTime CreatedAt { get; set; }
|
| 13 |
-
public long? CreatedBy { get; set; }
|
| 14 |
-
public string CreatedByName { get; set; } = null!;
|
| 15 |
-
}
|
| 16 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TaskTrackingSystem.WebApi/Features/Task/TaskDetailsController.cs
CHANGED
|
@@ -10,7 +10,6 @@ using System.Threading.Tasks;
|
|
| 10 |
using TaskTrackingSystem.Database.AppDbContextModels;
|
| 11 |
using TaskTrackingSystem.Shared;
|
| 12 |
using TaskTrackingSystem.Shared.Models.Comment;
|
| 13 |
-
using TaskTrackingSystem.Shared.Models.Attachment;
|
| 14 |
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 15 |
|
| 16 |
namespace TaskTrackingSystem.WebApi.Features.Task
|
|
@@ -143,157 +142,5 @@ namespace TaskTrackingSystem.WebApi.Features.Task
|
|
| 143 |
|
| 144 |
return Ok(Result.Success(200));
|
| 145 |
}
|
| 146 |
-
|
| 147 |
-
// βββ ATTACHMENTS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 148 |
-
|
| 149 |
-
[HttpGet("tasks/{taskId}/attachments")]
|
| 150 |
-
public async Task<ActionResult<IEnumerable<FileAttachmentDto>>> GetAttachments(long taskId)
|
| 151 |
-
{
|
| 152 |
-
var taskExists = await _db.Tasks.AnyAsync(t => t.Id == taskId && !t.IsDeleted);
|
| 153 |
-
if (!taskExists)
|
| 154 |
-
{
|
| 155 |
-
return NotFound(new { message = $"Task with ID {taskId} not found." });
|
| 156 |
-
}
|
| 157 |
-
|
| 158 |
-
var attachments = await _db.FileAttachments
|
| 159 |
-
.Where(f => f.TaskId == taskId && !f.IsDeleted)
|
| 160 |
-
.OrderBy(f => f.CreatedAt)
|
| 161 |
-
.Select(f => new FileAttachmentDto
|
| 162 |
-
{
|
| 163 |
-
Id = f.Id,
|
| 164 |
-
TaskId = f.TaskId,
|
| 165 |
-
FileName = f.FileName,
|
| 166 |
-
FileType = f.FileType,
|
| 167 |
-
FileSizeInBytes = f.FileSizeInBytes,
|
| 168 |
-
CreatedAt = f.CreatedAt ?? DateTime.UtcNow,
|
| 169 |
-
CreatedBy = f.CreatedBy,
|
| 170 |
-
CreatedByName = _db.Users
|
| 171 |
-
.Where(u => u.Id == f.CreatedBy)
|
| 172 |
-
.Select(u => $"{u.FirstName} {u.LastName}")
|
| 173 |
-
.FirstOrDefault() ?? "Unknown"
|
| 174 |
-
})
|
| 175 |
-
.ToListAsync();
|
| 176 |
-
|
| 177 |
-
return Ok(attachments);
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
[HttpPost("tasks/{taskId}/attachments")]
|
| 181 |
-
public async Task<ActionResult<Result<FileAttachmentDto>>> UploadAttachment(long taskId, IFormFile file)
|
| 182 |
-
{
|
| 183 |
-
if (file == null || file.Length == 0)
|
| 184 |
-
{
|
| 185 |
-
return BadRequest(Result<FileAttachmentDto>.Failure("No file was uploaded.", 400));
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
var task = await _db.Tasks.FirstOrDefaultAsync(t => t.Id == taskId && !t.IsDeleted);
|
| 189 |
-
if (task == null)
|
| 190 |
-
{
|
| 191 |
-
return NotFound(Result<FileAttachmentDto>.Failure($"Task with ID {taskId} not found.", 404));
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
var userId = User.GetUserId();
|
| 195 |
-
var user = await _db.Users.FirstOrDefaultAsync(u => u.Id == userId);
|
| 196 |
-
if (user == null)
|
| 197 |
-
{
|
| 198 |
-
return Unauthorized();
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
// Create uploads directory
|
| 202 |
-
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
| 203 |
-
if (!Directory.Exists(uploadsFolder))
|
| 204 |
-
{
|
| 205 |
-
Directory.CreateDirectory(uploadsFolder);
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
// Generate unique name
|
| 209 |
-
var uniqueFileName = $"{Guid.NewGuid()}_{file.FileName}";
|
| 210 |
-
var filePath = Path.Combine(uploadsFolder, uniqueFileName);
|
| 211 |
-
|
| 212 |
-
// Save file physically
|
| 213 |
-
using (var stream = new FileStream(filePath, FileMode.Create))
|
| 214 |
-
{
|
| 215 |
-
await file.CopyToAsync(stream);
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
var attachment = new FileAttachment
|
| 219 |
-
{
|
| 220 |
-
TaskId = taskId,
|
| 221 |
-
FileName = file.FileName,
|
| 222 |
-
FileType = file.ContentType,
|
| 223 |
-
FilePath = uniqueFileName, // Store the unique disk filename as FilePath
|
| 224 |
-
FileSizeInBytes = file.Length,
|
| 225 |
-
CreatedAt = DateTime.UtcNow,
|
| 226 |
-
CreatedBy = userId,
|
| 227 |
-
IsDeleted = false
|
| 228 |
-
};
|
| 229 |
-
|
| 230 |
-
_db.FileAttachments.Add(attachment);
|
| 231 |
-
await _db.SaveChangesAsync();
|
| 232 |
-
|
| 233 |
-
var resultDto = new FileAttachmentDto
|
| 234 |
-
{
|
| 235 |
-
Id = attachment.Id,
|
| 236 |
-
TaskId = attachment.TaskId,
|
| 237 |
-
FileName = attachment.FileName,
|
| 238 |
-
FileType = attachment.FileType,
|
| 239 |
-
FileSizeInBytes = attachment.FileSizeInBytes,
|
| 240 |
-
CreatedAt = attachment.CreatedAt ?? DateTime.UtcNow,
|
| 241 |
-
CreatedBy = attachment.CreatedBy,
|
| 242 |
-
CreatedByName = $"{user.FirstName} {user.LastName}"
|
| 243 |
-
};
|
| 244 |
-
|
| 245 |
-
return StatusCode(201, Result<FileAttachmentDto>.Success(resultDto, 201));
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
[HttpGet("attachments/{attachmentId}/download")]
|
| 249 |
-
[AllowAnonymous] // Allow download endpoint to stream, we can check custom token validation or basic checks if desired, or let Blazor fetch it
|
| 250 |
-
public async Task<IActionResult> DownloadAttachment(long attachmentId)
|
| 251 |
-
{
|
| 252 |
-
var attachment = await _db.FileAttachments.FirstOrDefaultAsync(f => f.Id == attachmentId && !f.IsDeleted);
|
| 253 |
-
if (attachment == null)
|
| 254 |
-
{
|
| 255 |
-
return NotFound();
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "Uploads");
|
| 259 |
-
var filePath = Path.Combine(uploadsFolder, attachment.FilePath);
|
| 260 |
-
|
| 261 |
-
if (!System.IO.File.Exists(filePath))
|
| 262 |
-
{
|
| 263 |
-
return NotFound();
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
var bytes = await System.IO.File.ReadAllBytesAsync(filePath);
|
| 267 |
-
return File(bytes, attachment.FileType, attachment.FileName);
|
| 268 |
-
}
|
| 269 |
-
|
| 270 |
-
[HttpDelete("attachments/{attachmentId}")]
|
| 271 |
-
public async Task<ActionResult<Result>> DeleteAttachment(long attachmentId)
|
| 272 |
-
{
|
| 273 |
-
var attachment = await _db.FileAttachments.FirstOrDefaultAsync(f => f.Id == attachmentId && !f.IsDeleted);
|
| 274 |
-
if (attachment == null)
|
| 275 |
-
{
|
| 276 |
-
return NotFound(Result.Failure("Attachment not found.", 404));
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
var userId = User.GetUserId();
|
| 280 |
-
var isAdmin = User.IsAdmin();
|
| 281 |
-
|
| 282 |
-
// Only attachment creator or Admin can delete
|
| 283 |
-
if (attachment.CreatedBy != userId && !isAdmin)
|
| 284 |
-
{
|
| 285 |
-
return Forbid();
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
attachment.IsDeleted = true;
|
| 289 |
-
attachment.UpdatedAt = DateTime.UtcNow;
|
| 290 |
-
attachment.UpdatedBy = userId;
|
| 291 |
-
|
| 292 |
-
_db.FileAttachments.Update(attachment);
|
| 293 |
-
await _db.SaveChangesAsync();
|
| 294 |
-
|
| 295 |
-
// Optional: delete physical file or leave it for archiving. We'll leave it as soft-deleted in DB.
|
| 296 |
-
return Ok(Result.Success(200));
|
| 297 |
-
}
|
| 298 |
}
|
| 299 |
}
|
|
|
|
| 10 |
using TaskTrackingSystem.Database.AppDbContextModels;
|
| 11 |
using TaskTrackingSystem.Shared;
|
| 12 |
using TaskTrackingSystem.Shared.Models.Comment;
|
|
|
|
| 13 |
using TaskTrackingSystem.WebApi.Infrastructure;
|
| 14 |
|
| 15 |
namespace TaskTrackingSystem.WebApi.Features.Task
|
|
|
|
| 142 |
|
| 143 |
return Ok(Result.Success(200));
|
| 144 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
}
|
TaskTrackingSystem.WebApp/Components/Pages/Backlog.razor
CHANGED
|
@@ -147,7 +147,7 @@
|
|
| 147 |
<div class="bg-white border border-slate-200 rounded-2xl p-12 shadow-sm text-center flex flex-col items-center justify-center min-h-[600px] text-slate-400">
|
| 148 |
<i data-lucide="layers" class="h-12 w-12 text-slate-300 mb-3 animate-pulse"></i>
|
| 149 |
<h4 class="text-sm font-bold text-slate-700">Select a task from the backlog</h4>
|
| 150 |
-
<p class="text-xs text-slate-400 mt-1 max-w-sm">Click any task on the left list to view details, update status, post comments
|
| 151 |
</div>
|
| 152 |
}
|
| 153 |
</div>
|
|
|
|
| 147 |
<div class="bg-white border border-slate-200 rounded-2xl p-12 shadow-sm text-center flex flex-col items-center justify-center min-h-[600px] text-slate-400">
|
| 148 |
<i data-lucide="layers" class="h-12 w-12 text-slate-300 mb-3 animate-pulse"></i>
|
| 149 |
<h4 class="text-sm font-bold text-slate-700">Select a task from the backlog</h4>
|
| 150 |
+
<p class="text-xs text-slate-400 mt-1 max-w-sm">Click any task on the left list to view details, update status, or post comments inline.</p>
|
| 151 |
</div>
|
| 152 |
}
|
| 153 |
</div>
|
TaskTrackingSystem.WebApp/Components/Pages/TaskDetails.razor
CHANGED
|
@@ -5,7 +5,6 @@
|
|
| 5 |
@using TaskTrackingSystem.Shared
|
| 6 |
@using TaskTrackingSystem.Shared.Models.Task
|
| 7 |
@using TaskTrackingSystem.Shared.Models.Comment
|
| 8 |
-
@using TaskTrackingSystem.Shared.Models.Attachment
|
| 9 |
@using TaskTrackingSystem.Shared.Models.Project
|
| 10 |
@using TaskTrackingSystem.Shared.Models.User
|
| 11 |
@using TaskTrackingSystem.WebApp.Components.Partial
|
|
@@ -247,144 +246,70 @@
|
|
| 247 |
</div>
|
| 248 |
</div>
|
| 249 |
|
| 250 |
-
<!-- Right Grid:
|
| 251 |
<div class="space-y-6">
|
| 252 |
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden flex flex-col h-[580px]">
|
| 253 |
-
<!--
|
| 254 |
-
<div class="
|
| 255 |
-
<
|
| 256 |
-
<i data-lucide="message-square" class="h-4 w-4"></i>
|
| 257 |
-
|
| 258 |
-
</
|
| 259 |
-
<button @onclick='() => SetTab("files")' class="flex-1 py-2 rounded-lg text-xs font-bold flex items-center justify-center gap-2 transition-all @(currentTab == "files" ? "bg-white text-slate-800 shadow-sm" : "text-slate-500 hover:text-slate-800")">
|
| 260 |
-
<i data-lucide="paperclip" class="h-4 w-4"></i>
|
| 261 |
-
<span>Files (@attachments.Count)</span>
|
| 262 |
-
</button>
|
| 263 |
</div>
|
| 264 |
|
| 265 |
-
<!--
|
| 266 |
<div class="flex-1 overflow-y-auto p-4 min-h-0">
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
@if (comments.Any())
|
| 272 |
-
{
|
| 273 |
-
@foreach (var c in comments)
|
| 274 |
-
{
|
| 275 |
-
<div class="flex items-start gap-3">
|
| 276 |
-
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold shrink-0 mt-0.5">
|
| 277 |
-
@(c.UserFullName.Length > 0 ? c.UserFullName[0].ToString().ToUpper() : "U")
|
| 278 |
-
</span>
|
| 279 |
-
<div class="flex-1 min-w-0 bg-slate-50 rounded-xl p-3 border border-slate-100">
|
| 280 |
-
<div class="flex items-center justify-between gap-2">
|
| 281 |
-
<span class="text-xs font-bold text-slate-800 truncate">@c.UserFullName</span>
|
| 282 |
-
<span class="text-[9px] text-slate-400 shrink-0 font-sans">@c.CreatedAt.ToString("g")</span>
|
| 283 |
-
</div>
|
| 284 |
-
<p class="text-xs text-slate-400 mt-0.5">@c.UserRoleName</p>
|
| 285 |
-
<div class="text-xs text-slate-700 mt-2 whitespace-pre-wrap break-words">@c.Message</div>
|
| 286 |
-
</div>
|
| 287 |
-
@if (c.UserId == currentUserId || isAdmin)
|
| 288 |
-
{
|
| 289 |
-
<button @onclick="() => ConfirmDeleteComment(c.Id)" class="p-1 text-slate-300 hover:text-rose-500 rounded-lg transition-colors shrink-0" title="Delete comment">
|
| 290 |
-
<i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
|
| 291 |
-
</button>
|
| 292 |
-
}
|
| 293 |
-
</div>
|
| 294 |
-
}
|
| 295 |
-
}
|
| 296 |
-
else
|
| 297 |
-
{
|
| 298 |
-
<div class="text-center py-12 text-slate-400 space-y-2">
|
| 299 |
-
<i data-lucide="messages-square" class="h-8 w-8 mx-auto text-slate-300"></i>
|
| 300 |
-
<p class="text-xs">No comments posted yet.</p>
|
| 301 |
-
<p class="text-[10px]">Start the conversation below!</p>
|
| 302 |
-
</div>
|
| 303 |
-
}
|
| 304 |
-
</div>
|
| 305 |
-
}
|
| 306 |
-
else
|
| 307 |
-
{
|
| 308 |
-
<!-- Attachments List -->
|
| 309 |
-
<div class="space-y-3 font-sans">
|
| 310 |
-
@if (attachments.Any())
|
| 311 |
{
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
</span>
|
| 319 |
-
<
|
| 320 |
-
<p class="text-xs font-semibold text-slate-800 truncate" title="@file.FileName">@file.FileName</p>
|
| 321 |
-
<p class="text-[10px] text-slate-400 mt-0.5 font-sans">@FormatBytes(file.FileSizeInBytes) Γ’β¬Β’ by @file.CreatedByName</p>
|
| 322 |
-
</div>
|
| 323 |
-
</div>
|
| 324 |
-
<div class="flex items-center gap-1.5 shrink-0">
|
| 325 |
-
<a href="@($"{ApiClient.BaseUrl}/api/TaskDetails/attachments/{file.Id}/download")" target="_blank" class="p-1.5 text-slate-400 hover:text-violet-600 hover:bg-violet-50 rounded-lg transition-all" title="Download">
|
| 326 |
-
<i data-lucide="download" class="h-4 w-4"></i>
|
| 327 |
-
</a>
|
| 328 |
-
@if (file.CreatedBy == currentUserId || isAdmin)
|
| 329 |
-
{
|
| 330 |
-
<button @onclick="() => ConfirmDeleteAttachment(file.Id)" class="p-1.5 text-slate-300 hover:text-rose-500 hover:bg-rose-50 rounded-lg transition-all" title="Delete file">
|
| 331 |
-
<i data-lucide="trash-2" class="h-4 w-4"></i>
|
| 332 |
-
</button>
|
| 333 |
-
}
|
| 334 |
</div>
|
|
|
|
|
|
|
| 335 |
</div>
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
<p class="text-xs">No attachments found.</p>
|
| 343 |
</div>
|
| 344 |
}
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
</div>
|
| 348 |
|
| 349 |
-
<!--
|
| 350 |
<div class="p-4 border-t border-slate-100 bg-slate-50/50">
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
<textarea @bind="newCommentMessage" rows="2" placeholder="Write a comment..." class="w-full rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" disabled="@isSavingComment"></textarea>
|
| 356 |
-
@if (!string.IsNullOrEmpty(commentError))
|
| 357 |
-
{
|
| 358 |
-
<p class="text-[10px] text-red-500 mt-1 font-sans">@commentError</p>
|
| 359 |
-
}
|
| 360 |
-
</div>
|
| 361 |
-
<button @onclick="PostComment" disabled="@(isSavingComment || string.IsNullOrWhiteSpace(newCommentMessage))" class="rounded-xl bg-violet-600 text-white p-2.5 hover:bg-violet-700 disabled:opacity-50 transition-all shadow-sm shrink-0">
|
| 362 |
-
<i data-lucide="send" class="h-4 w-4"></i>
|
| 363 |
-
</button>
|
| 364 |
-
</div>
|
| 365 |
-
}
|
| 366 |
-
else
|
| 367 |
-
{
|
| 368 |
-
<div>
|
| 369 |
-
<InputFile OnChange="OnInputFileChange" class="hidden" id="fileUpload" disabled="@isUploading" />
|
| 370 |
-
<label for="fileUpload" class="cursor-pointer flex flex-col items-center justify-center border-2 border-dashed border-slate-200 rounded-xl p-4 hover:border-violet-500 hover:bg-violet-50/20 transition-all">
|
| 371 |
-
@if (isUploading)
|
| 372 |
-
{
|
| 373 |
-
<LoadingSpinner Message="Uploading file..." />
|
| 374 |
-
}
|
| 375 |
-
else
|
| 376 |
-
{
|
| 377 |
-
<i data-lucide="upload-cloud" class="h-7 w-7 text-slate-400 mb-1.5"></i>
|
| 378 |
-
<span class="text-xs font-semibold text-slate-600">Click to upload file</span>
|
| 379 |
-
<span class="text-[9px] text-slate-400 mt-0.5">Maximum size: 10MB</span>
|
| 380 |
-
}
|
| 381 |
-
</label>
|
| 382 |
-
@if (!string.IsNullOrEmpty(uploadError))
|
| 383 |
{
|
| 384 |
-
<p class="text-[10px] text-red-500 mt-1
|
| 385 |
}
|
| 386 |
</div>
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
| 388 |
</div>
|
| 389 |
</div>
|
| 390 |
</div>
|
|
@@ -392,7 +317,6 @@
|
|
| 392 |
}
|
| 393 |
</div>
|
| 394 |
|
| 395 |
-
@* Confirm Actions dialog *@
|
| 396 |
<ConfirmDialog IsVisible="showConfirm"
|
| 397 |
Title="@confirmTitle"
|
| 398 |
Message="@confirmMessage"
|
|
@@ -410,13 +334,13 @@
|
|
| 410 |
[Parameter] public EventCallback OnTaskUpdated { get; set; }
|
| 411 |
[SupplyParameterFromQuery(Name = "tab")]
|
| 412 |
public string? TabQuery { get; set; }
|
|
|
|
| 413 |
|
| 414 |
private bool isLoading = true;
|
| 415 |
private TaskDto? task;
|
| 416 |
private ProjectDto? project;
|
| 417 |
private UserDto? assignee;
|
| 418 |
private List<CommentDto> comments = new();
|
| 419 |
-
private List<FileAttachmentDto> attachments = new();
|
| 420 |
private List<UserDto> allUsers = new();
|
| 421 |
|
| 422 |
private bool isAdmin = false;
|
|
@@ -427,22 +351,15 @@
|
|
| 427 |
|
| 428 |
private List<string> scopes = new();
|
| 429 |
|
| 430 |
-
private string currentTab = "comments";
|
| 431 |
-
|
| 432 |
// Comments Form
|
| 433 |
private string newCommentMessage = "";
|
| 434 |
private bool isSavingComment = false;
|
| 435 |
private string? commentError;
|
| 436 |
|
| 437 |
-
// Attachments Form
|
| 438 |
-
private bool isUploading = false;
|
| 439 |
-
private string? uploadError;
|
| 440 |
-
|
| 441 |
// Confirm dialog
|
| 442 |
private bool showConfirm = false;
|
| 443 |
private string confirmTitle = "";
|
| 444 |
private string confirmMessage = "";
|
| 445 |
-
private Func<Task>? confirmAction;
|
| 446 |
|
| 447 |
// Inline edit states
|
| 448 |
private bool isEditingDescription = false;
|
|
@@ -480,11 +397,6 @@
|
|
| 480 |
assignee = null;
|
| 481 |
await LoadAllData();
|
| 482 |
}
|
| 483 |
-
|
| 484 |
-
if (!string.IsNullOrWhiteSpace(TabQuery))
|
| 485 |
-
{
|
| 486 |
-
currentTab = TabQuery.Equals("files", StringComparison.OrdinalIgnoreCase) ? "files" : "comments";
|
| 487 |
-
}
|
| 488 |
}
|
| 489 |
|
| 490 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
@@ -492,15 +404,6 @@
|
|
| 492 |
await JS.InvokeVoidAsync("initIcons");
|
| 493 |
}
|
| 494 |
|
| 495 |
-
private void SetTab(string tab)
|
| 496 |
-
{
|
| 497 |
-
currentTab = tab;
|
| 498 |
-
_ = InvokeAsync(async () => {
|
| 499 |
-
await Task.Delay(50);
|
| 500 |
-
await JS.InvokeVoidAsync("initIcons");
|
| 501 |
-
});
|
| 502 |
-
}
|
| 503 |
-
|
| 504 |
private async Task LoadAllData()
|
| 505 |
{
|
| 506 |
isLoading = true;
|
|
@@ -509,15 +412,13 @@
|
|
| 509 |
{
|
| 510 |
var taskUrl = $"Task/{TaskId}";
|
| 511 |
var commentsUrl = $"TaskDetails/tasks/{TaskId}/comments";
|
| 512 |
-
var attachmentsUrl = $"TaskDetails/tasks/{TaskId}/attachments";
|
| 513 |
var usersUrl = "User";
|
| 514 |
|
| 515 |
var taskResponseTask = client.GetAsync(taskUrl);
|
| 516 |
var commentsResponseTask = client.GetAsync(commentsUrl);
|
| 517 |
-
var attachmentsResponseTask = client.GetAsync(attachmentsUrl);
|
| 518 |
var usersResponseTask = client.GetAsync(usersUrl);
|
| 519 |
|
| 520 |
-
await Task.WhenAll(taskResponseTask, commentsResponseTask,
|
| 521 |
|
| 522 |
var taskResponse = taskResponseTask.Result;
|
| 523 |
if (taskResponse.IsSuccessStatusCode)
|
|
@@ -531,12 +432,6 @@
|
|
| 531 |
comments = await commentsResponse.Content.ReadFromJsonAsync<List<CommentDto>>(Serialization.CaseInsensitive) ?? new();
|
| 532 |
}
|
| 533 |
|
| 534 |
-
var attachmentsResponse = attachmentsResponseTask.Result;
|
| 535 |
-
if (attachmentsResponse.IsSuccessStatusCode)
|
| 536 |
-
{
|
| 537 |
-
attachments = await attachmentsResponse.Content.ReadFromJsonAsync<List<FileAttachmentDto>>(Serialization.CaseInsensitive) ?? new();
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
var usersResponse = usersResponseTask.Result;
|
| 541 |
if (usersResponse.IsSuccessStatusCode)
|
| 542 |
{
|
|
@@ -591,7 +486,7 @@
|
|
| 591 |
}
|
| 592 |
}
|
| 593 |
|
| 594 |
-
//
|
| 595 |
|
| 596 |
private async Task OnStatusChanged(ChangeEventArgs e)
|
| 597 |
{
|
|
@@ -736,7 +631,7 @@
|
|
| 736 |
isSavingTitle = false;
|
| 737 |
}
|
| 738 |
|
| 739 |
-
//
|
| 740 |
|
| 741 |
private async Task PostComment()
|
| 742 |
{
|
|
@@ -750,7 +645,7 @@
|
|
| 750 |
var response = await client.PostAsJsonAsync($"TaskDetails/tasks/{TaskId}/comments", new CreateCommentDto { Message = newCommentMessage });
|
| 751 |
var res = await response.Content.ReadFromJsonAsync<Result<CommentDto>>(Serialization.CaseInsensitive);
|
| 752 |
|
| 753 |
-
if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
|
| 754 |
{
|
| 755 |
comments.Add(res.Value);
|
| 756 |
newCommentMessage = "";
|
|
@@ -798,75 +693,7 @@
|
|
| 798 |
}
|
| 799 |
}
|
| 800 |
|
| 801 |
-
//
|
| 802 |
-
|
| 803 |
-
private async Task OnInputFileChange(InputFileChangeEventArgs e)
|
| 804 |
-
{
|
| 805 |
-
var file = e.File;
|
| 806 |
-
if (file == null) return;
|
| 807 |
-
|
| 808 |
-
isUploading = true;
|
| 809 |
-
uploadError = null;
|
| 810 |
-
|
| 811 |
-
try
|
| 812 |
-
{
|
| 813 |
-
var content = new MultipartFormDataContent();
|
| 814 |
-
var fileContent = new StreamContent(file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024));
|
| 815 |
-
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(file.ContentType);
|
| 816 |
-
content.Add(fileContent, "file", file.Name);
|
| 817 |
-
|
| 818 |
-
var client = ApiClient.CreateClient();
|
| 819 |
-
var response = await client.PostAsync($"TaskDetails/tasks/{TaskId}/attachments", content);
|
| 820 |
-
var res = await response.Content.ReadFromJsonAsync<Result<FileAttachmentDto>>(Serialization.CaseInsensitive);
|
| 821 |
-
|
| 822 |
-
if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
|
| 823 |
-
{
|
| 824 |
-
attachments.Add(res.Value);
|
| 825 |
-
await JS.InvokeVoidAsync("initIcons");
|
| 826 |
-
}
|
| 827 |
-
else
|
| 828 |
-
{
|
| 829 |
-
uploadError = res?.ErrorMessage ?? "Failed to upload file.";
|
| 830 |
-
}
|
| 831 |
-
}
|
| 832 |
-
catch (Exception ex)
|
| 833 |
-
{
|
| 834 |
-
uploadError = ex.Message;
|
| 835 |
-
}
|
| 836 |
-
finally
|
| 837 |
-
{
|
| 838 |
-
isUploading = false;
|
| 839 |
-
}
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
private void ConfirmDeleteAttachment(long attachmentId)
|
| 843 |
-
{
|
| 844 |
-
confirmTitle = "Delete File?";
|
| 845 |
-
confirmMessage = "Are you sure you want to delete this file attachment?";
|
| 846 |
-
confirmAction = () => DeleteAttachment(attachmentId);
|
| 847 |
-
showConfirm = true;
|
| 848 |
-
}
|
| 849 |
-
|
| 850 |
-
private async Task DeleteAttachment(long attachmentId)
|
| 851 |
-
{
|
| 852 |
-
var client = ApiClient.CreateClient();
|
| 853 |
-
try
|
| 854 |
-
{
|
| 855 |
-
var response = await client.DeleteAsync($"TaskDetails/attachments/{attachmentId}");
|
| 856 |
-
var res = await response.Content.ReadFromJsonAsync<Result>(Serialization.CaseInsensitive);
|
| 857 |
-
if (response.IsSuccessStatusCode && res != null && res.IsSuccess)
|
| 858 |
-
{
|
| 859 |
-
var idx = attachments.FindIndex(f => f.Id == attachmentId);
|
| 860 |
-
if (idx >= 0) attachments.RemoveAt(idx);
|
| 861 |
-
}
|
| 862 |
-
}
|
| 863 |
-
catch (Exception ex)
|
| 864 |
-
{
|
| 865 |
-
Console.WriteLine($"Delete file error: {ex.Message}");
|
| 866 |
-
}
|
| 867 |
-
}
|
| 868 |
-
|
| 869 |
-
// Γ’ββ¬Γ’ββ¬Γ’ββ¬ CONFIRM DIALOG LOGIC Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬Γ’ββ¬
|
| 870 |
|
| 871 |
private async Task ExecuteConfirmAction()
|
| 872 |
{
|
|
@@ -884,7 +711,7 @@
|
|
| 884 |
confirmAction = null;
|
| 885 |
}
|
| 886 |
|
| 887 |
-
//
|
| 888 |
|
| 889 |
private string GetStatusLabel(AppTaskStatus statusId) => statusId switch
|
| 890 |
{
|
|
@@ -917,25 +744,4 @@
|
|
| 917 |
TaskPriority.High => "bg-rose-50 text-rose-700 border border-rose-100",
|
| 918 |
_ => "bg-slate-50 text-slate-600"
|
| 919 |
};
|
| 920 |
-
|
| 921 |
-
private string GetFileIcon(string fileType)
|
| 922 |
-
{
|
| 923 |
-
if (fileType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) return "image";
|
| 924 |
-
if (fileType.Equals("application/pdf", StringComparison.OrdinalIgnoreCase)) return "file-text";
|
| 925 |
-
if (fileType.Contains("word") || fileType.Contains("officedocument.wordprocessingml")) return "file-text";
|
| 926 |
-
if (fileType.Contains("excel") || fileType.Contains("officedocument.spreadsheetml")) return "file-spreadsheet";
|
| 927 |
-
return "file";
|
| 928 |
-
}
|
| 929 |
-
|
| 930 |
-
private string FormatBytes(long bytes)
|
| 931 |
-
{
|
| 932 |
-
string[] suffix = { "B", "KB", "MB", "GB", "TB" };
|
| 933 |
-
int i;
|
| 934 |
-
double dblBytes = bytes;
|
| 935 |
-
for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024)
|
| 936 |
-
{
|
| 937 |
-
dblBytes = bytes / 1024.0;
|
| 938 |
-
}
|
| 939 |
-
return $"{dblBytes:0.##} {suffix[i]}";
|
| 940 |
-
}
|
| 941 |
}
|
|
|
|
| 5 |
@using TaskTrackingSystem.Shared
|
| 6 |
@using TaskTrackingSystem.Shared.Models.Task
|
| 7 |
@using TaskTrackingSystem.Shared.Models.Comment
|
|
|
|
| 8 |
@using TaskTrackingSystem.Shared.Models.Project
|
| 9 |
@using TaskTrackingSystem.Shared.Models.User
|
| 10 |
@using TaskTrackingSystem.WebApp.Components.Partial
|
|
|
|
| 246 |
</div>
|
| 247 |
</div>
|
| 248 |
|
| 249 |
+
<!-- Right Grid: Comments -->
|
| 250 |
<div class="space-y-6">
|
| 251 |
<div class="rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden flex flex-col h-[580px]">
|
| 252 |
+
<!-- Header -->
|
| 253 |
+
<div class="border-b border-slate-100 bg-slate-50/50 p-4">
|
| 254 |
+
<h3 class="text-sm font-bold text-slate-800 flex items-center gap-2">
|
| 255 |
+
<i data-lucide="message-square" class="h-4 w-4 text-violet-600"></i>
|
| 256 |
+
Comments (@comments.Count)
|
| 257 |
+
</h3>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
</div>
|
| 259 |
|
| 260 |
+
<!-- Comments Body -->
|
| 261 |
<div class="flex-1 overflow-y-auto p-4 min-h-0">
|
| 262 |
+
<div class="space-y-4 font-sans">
|
| 263 |
+
@if (comments.Any())
|
| 264 |
+
{
|
| 265 |
+
@foreach (var c in comments)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
{
|
| 267 |
+
<div class="flex items-start gap-3">
|
| 268 |
+
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-100 text-violet-700 text-xs font-bold shrink-0 mt-0.5">
|
| 269 |
+
@(c.UserFullName.Length > 0 ? c.UserFullName[0].ToString().ToUpper() : "U")
|
| 270 |
+
</span>
|
| 271 |
+
<div class="flex-1 min-w-0 bg-slate-50 rounded-xl p-3 border border-slate-100">
|
| 272 |
+
<div class="flex items-center justify-between gap-2">
|
| 273 |
+
<span class="text-xs font-bold text-slate-800 truncate">@c.UserFullName</span>
|
| 274 |
+
<span class="text-[9px] text-slate-400 shrink-0 font-sans">@c.CreatedAt.ToString("g")</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
</div>
|
| 276 |
+
<p class="text-xs text-slate-400 mt-0.5">@c.UserRoleName</p>
|
| 277 |
+
<div class="text-xs text-slate-700 mt-2 whitespace-pre-wrap break-words">@c.Message</div>
|
| 278 |
</div>
|
| 279 |
+
@if (c.UserId == currentUserId || isAdmin)
|
| 280 |
+
{
|
| 281 |
+
<button @onclick="() => ConfirmDeleteComment(c.Id)" class="p-1 text-slate-300 hover:text-rose-500 rounded-lg transition-colors shrink-0" title="Delete comment">
|
| 282 |
+
<i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
|
| 283 |
+
</button>
|
| 284 |
+
}
|
|
|
|
| 285 |
</div>
|
| 286 |
}
|
| 287 |
+
}
|
| 288 |
+
else
|
| 289 |
+
{
|
| 290 |
+
<div class="text-center py-12 text-slate-400 space-y-2">
|
| 291 |
+
<i data-lucide="messages-square" class="h-8 w-8 mx-auto text-slate-300"></i>
|
| 292 |
+
<p class="text-xs">No comments posted yet.</p>
|
| 293 |
+
<p class="text-[10px]">Start the conversation below!</p>
|
| 294 |
+
</div>
|
| 295 |
+
}
|
| 296 |
+
</div>
|
| 297 |
</div>
|
| 298 |
|
| 299 |
+
<!-- Footer (Inputs) -->
|
| 300 |
<div class="p-4 border-t border-slate-100 bg-slate-50/50">
|
| 301 |
+
<div class="flex items-start gap-2">
|
| 302 |
+
<div class="flex-1">
|
| 303 |
+
<textarea @bind="newCommentMessage" rows="2" placeholder="Write a comment..." class="w-full rounded-xl border border-slate-200 px-3 py-2 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-600 focus:border-transparent transition-all" disabled="@isSavingComment"></textarea>
|
| 304 |
+
@if (!string.IsNullOrEmpty(commentError))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
{
|
| 306 |
+
<p class="text-[10px] text-red-500 mt-1 font-sans">@commentError</p>
|
| 307 |
}
|
| 308 |
</div>
|
| 309 |
+
<button @onclick="PostComment" disabled="@(isSavingComment || string.IsNullOrWhiteSpace(newCommentMessage))" class="rounded-xl bg-violet-600 text-white p-2.5 hover:bg-violet-700 disabled:opacity-50 transition-all shadow-sm shrink-0">
|
| 310 |
+
<i data-lucide="send" class="h-4 w-4"></i>
|
| 311 |
+
</button>
|
| 312 |
+
</div>
|
| 313 |
</div>
|
| 314 |
</div>
|
| 315 |
</div>
|
|
|
|
| 317 |
}
|
| 318 |
</div>
|
| 319 |
|
|
|
|
| 320 |
<ConfirmDialog IsVisible="showConfirm"
|
| 321 |
Title="@confirmTitle"
|
| 322 |
Message="@confirmMessage"
|
|
|
|
| 334 |
[Parameter] public EventCallback OnTaskUpdated { get; set; }
|
| 335 |
[SupplyParameterFromQuery(Name = "tab")]
|
| 336 |
public string? TabQuery { get; set; }
|
| 337 |
+
private Func<Task>? confirmAction;
|
| 338 |
|
| 339 |
private bool isLoading = true;
|
| 340 |
private TaskDto? task;
|
| 341 |
private ProjectDto? project;
|
| 342 |
private UserDto? assignee;
|
| 343 |
private List<CommentDto> comments = new();
|
|
|
|
| 344 |
private List<UserDto> allUsers = new();
|
| 345 |
|
| 346 |
private bool isAdmin = false;
|
|
|
|
| 351 |
|
| 352 |
private List<string> scopes = new();
|
| 353 |
|
|
|
|
|
|
|
| 354 |
// Comments Form
|
| 355 |
private string newCommentMessage = "";
|
| 356 |
private bool isSavingComment = false;
|
| 357 |
private string? commentError;
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
// Confirm dialog
|
| 360 |
private bool showConfirm = false;
|
| 361 |
private string confirmTitle = "";
|
| 362 |
private string confirmMessage = "";
|
|
|
|
| 363 |
|
| 364 |
// Inline edit states
|
| 365 |
private bool isEditingDescription = false;
|
|
|
|
| 397 |
assignee = null;
|
| 398 |
await LoadAllData();
|
| 399 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
}
|
| 401 |
|
| 402 |
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
|
|
| 404 |
await JS.InvokeVoidAsync("initIcons");
|
| 405 |
}
|
| 406 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
private async Task LoadAllData()
|
| 408 |
{
|
| 409 |
isLoading = true;
|
|
|
|
| 412 |
{
|
| 413 |
var taskUrl = $"Task/{TaskId}";
|
| 414 |
var commentsUrl = $"TaskDetails/tasks/{TaskId}/comments";
|
|
|
|
| 415 |
var usersUrl = "User";
|
| 416 |
|
| 417 |
var taskResponseTask = client.GetAsync(taskUrl);
|
| 418 |
var commentsResponseTask = client.GetAsync(commentsUrl);
|
|
|
|
| 419 |
var usersResponseTask = client.GetAsync(usersUrl);
|
| 420 |
|
| 421 |
+
await Task.WhenAll(taskResponseTask, commentsResponseTask, usersResponseTask);
|
| 422 |
|
| 423 |
var taskResponse = taskResponseTask.Result;
|
| 424 |
if (taskResponse.IsSuccessStatusCode)
|
|
|
|
| 432 |
comments = await commentsResponse.Content.ReadFromJsonAsync<List<CommentDto>>(Serialization.CaseInsensitive) ?? new();
|
| 433 |
}
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
var usersResponse = usersResponseTask.Result;
|
| 436 |
if (usersResponse.IsSuccessStatusCode)
|
| 437 |
{
|
|
|
|
| 486 |
}
|
| 487 |
}
|
| 488 |
|
| 489 |
+
// βββ INLINE MUTATIONS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 490 |
|
| 491 |
private async Task OnStatusChanged(ChangeEventArgs e)
|
| 492 |
{
|
|
|
|
| 631 |
isSavingTitle = false;
|
| 632 |
}
|
| 633 |
|
| 634 |
+
// βββ COMMENTS ACTIONS ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 635 |
|
| 636 |
private async Task PostComment()
|
| 637 |
{
|
|
|
|
| 645 |
var response = await client.PostAsJsonAsync($"TaskDetails/tasks/{TaskId}/comments", new CreateCommentDto { Message = newCommentMessage });
|
| 646 |
var res = await response.Content.ReadFromJsonAsync<Result<CommentDto>>(Serialization.CaseInsensitive);
|
| 647 |
|
| 648 |
+
if (response.IsSuccessStatusCode && res != null && res.IsSuccess && res.Value != null)
|
| 649 |
{
|
| 650 |
comments.Add(res.Value);
|
| 651 |
newCommentMessage = "";
|
|
|
|
| 693 |
}
|
| 694 |
}
|
| 695 |
|
| 696 |
+
// βββ CONFIRM DIALOG LOGIC βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
|
| 698 |
private async Task ExecuteConfirmAction()
|
| 699 |
{
|
|
|
|
| 711 |
confirmAction = null;
|
| 712 |
}
|
| 713 |
|
| 714 |
+
// βββ DISPLAY UTILITIES ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 715 |
|
| 716 |
private string GetStatusLabel(AppTaskStatus statusId) => statusId switch
|
| 717 |
{
|
|
|
|
| 744 |
TaskPriority.High => "bg-rose-50 text-rose-700 border border-rose-100",
|
| 745 |
_ => "bg-slate-50 text-slate-600"
|
| 746 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 747 |
}
|