Changeset 545738f
- Timestamp:
- 06/22/26 19:00:39 (2 weeks ago)
- Branches:
- master
- Children:
- c0086c0
- Parents:
- 194c016
- Files:
-
- 6 added
- 16 edited
-
backend/Dockerfile (added)
-
backend/src/main/java/com/trekr/backend/config/SecurityConfig.java (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java (modified) (2 diffs)
-
backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java (modified) (2 diffs)
-
backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java (modified) (2 diffs)
-
backend/src/main/java/com/trekr/backend/entity/discipline/Task.java (modified) (4 diffs)
-
backend/src/main/java/com/trekr/backend/repository/TaskRepository.java (modified) (3 diffs)
-
backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java (modified) (5 diffs)
-
backend/src/main/java/com/trekr/backend/service/DisciplineService.java (modified) (8 diffs)
-
db-scripts/ddl.sql (modified) (1 diff)
-
db-scripts/dml.sql (modified) (1 diff)
-
db-scripts/docker-init/01-init.sql (added)
-
db-scripts/migrations/002_task_enhancements.sql (added)
-
docker-compose.yml (added)
-
frontend/Dockerfile (added)
-
frontend/nginx.conf (added)
-
frontend/src/api/discipline.js (modified) (1 diff)
-
frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx (modified) (8 diffs)
-
frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx (modified) (1 diff)
-
frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx (modified) (4 diffs)
-
frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
backend/src/main/java/com/trekr/backend/config/SecurityConfig.java
r194c016 r545738f 49 49 config.setAllowedOrigins(List.of( 50 50 "http://localhost:5173", 51 "http://127.0.0.1:5173")); 51 "http://127.0.0.1:5173", 52 "http://localhost:3000", 53 "http://127.0.0.1:3000")); 52 54 config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); 53 55 config.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With")); -
backend/src/main/java/com/trekr/backend/controller/CustomTrackingController.java
r194c016 r545738f 9 9 import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest; 10 10 import com.trekr.backend.dto.discipline.UpdateTaskRequest; 11 import com.trekr.backend.dto.discipline.UpdateTaskStatusRequest; 11 12 import com.trekr.backend.security.UserPrincipal; 12 13 import com.trekr.backend.service.CustomTrackingService; … … 79 80 } 80 81 82 @PatchMapping("/{customTrackingId}/tasks/{taskId}/status") 83 public TaskDto updateTaskStatus( 84 @PathVariable Long customTrackingId, 85 @PathVariable Long taskId, 86 @RequestBody UpdateTaskStatusRequest request, 87 Authentication authentication) { 88 UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); 89 return customTrackingService.updateCustomCategoryTaskStatus(principal.getUserId(), customTrackingId, taskId, request); 90 } 91 81 92 @DeleteMapping("/{customTrackingId}/tasks/{taskId}") 82 93 @ResponseStatus(HttpStatus.NO_CONTENT) -
backend/src/main/java/com/trekr/backend/dto/discipline/CreateTaskRequest.java
r194c016 r545738f 2 2 3 3 import jakarta.validation.constraints.NotBlank; 4 5 import java.time.LocalDate; 4 6 5 7 public class CreateTaskRequest { … … 8 10 private String name; 9 11 12 private String description; 13 private LocalDate dueDate; 14 private String priority; 15 10 16 public CreateTaskRequest() { 11 17 } 12 18 13 public String getName() { 14 return name; 15 } 19 public String getName() { return name; } 20 public void setName(String name) { this.name = name; } 16 21 17 public void setName(String name) { 18 this.name = name; 19 } 22 public String getDescription() { return description; } 23 public void setDescription(String description) { this.description = description; } 24 25 public LocalDate getDueDate() { return dueDate; } 26 public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; } 27 28 public String getPriority() { return priority; } 29 public void setPriority(String priority) { this.priority = priority; } 20 30 } 21 -
backend/src/main/java/com/trekr/backend/dto/discipline/TaskDto.java
r194c016 r545738f 1 1 package com.trekr.backend.dto.discipline; 2 2 3 public record TaskDto(Long taskId, String name, boolean isFinished) { 3 import com.trekr.backend.entity.discipline.TaskStatus; 4 5 import java.time.LocalDate; 6 7 public record TaskDto( 8 Long taskId, 9 String name, 10 boolean isFinished, 11 TaskStatus status, 12 String description, 13 LocalDate dueDate, 14 String priority 15 ) { 4 16 } 5 -
backend/src/main/java/com/trekr/backend/dto/discipline/UpdateTaskRequest.java
r194c016 r545738f 1 1 package com.trekr.backend.dto.discipline; 2 2 3 import com.trekr.backend.entity.discipline.TaskStatus; 3 4 import jakarta.validation.constraints.NotBlank; 5 6 import java.time.LocalDate; 4 7 5 8 public class UpdateTaskRequest { … … 8 11 private String name; 9 12 13 private String description; 14 private LocalDate dueDate; 15 private String priority; 16 private TaskStatus status; 17 10 18 public UpdateTaskRequest() { 11 19 } 12 20 13 public String getName() { 14 return name; 15 } 21 public String getName() { return name; } 22 public void setName(String name) { this.name = name; } 16 23 17 public void setName(String name) { 18 this.name = name; 19 } 24 public String getDescription() { return description; } 25 public void setDescription(String description) { this.description = description; } 26 27 public LocalDate getDueDate() { return dueDate; } 28 public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; } 29 30 public String getPriority() { return priority; } 31 public void setPriority(String priority) { this.priority = priority; } 32 33 public TaskStatus getStatus() { return status; } 34 public void setStatus(TaskStatus status) { this.status = status; } 20 35 } 21 -
backend/src/main/java/com/trekr/backend/entity/discipline/Task.java
r194c016 r545738f 3 3 import jakarta.persistence.Column; 4 4 import jakarta.persistence.Entity; 5 import jakarta.persistence.EnumType; 6 import jakarta.persistence.Enumerated; 5 7 import jakarta.persistence.FetchType; 6 8 import jakarta.persistence.GeneratedValue; … … 10 12 import jakarta.persistence.ManyToOne; 11 13 import jakarta.persistence.Table; 14 15 import java.time.LocalDate; 12 16 13 17 @Entity … … 23 27 private String name; 24 28 25 @Column(name = "is_finished", nullable = false) 26 private boolean finished; 29 @Enumerated(EnumType.STRING) 30 @Column(name = "status", nullable = false) 31 private TaskStatus status = TaskStatus.NOT_STARTED; 32 33 @Column(name = "description") 34 private String description; 35 36 @Column(name = "due_date") 37 private LocalDate dueDate; 38 39 @Column(name = "priority") 40 private String priority; 27 41 28 42 @ManyToOne(fetch = FetchType.LAZY) … … 37 51 } 38 52 39 public Long getTaskId() { 40 return taskId; 53 public Long getTaskId() { return taskId; } 54 public void setTaskId(Long taskId) { this.taskId = taskId; } 55 56 public String getName() { return name; } 57 public void setName(String name) { this.name = name; } 58 59 public TaskStatus getStatus() { return status; } 60 public void setStatus(TaskStatus status) { this.status = status; } 61 62 public boolean isFinished() { return TaskStatus.FINISHED == status; } 63 public void setFinished(boolean finished) { 64 this.status = finished ? TaskStatus.FINISHED : TaskStatus.NOT_STARTED; 41 65 } 42 66 43 public void setTaskId(Long taskId) { 44 this.taskId = taskId; 45 } 67 public String getDescription() { return description; } 68 public void setDescription(String description) { this.description = description; } 46 69 47 public String getName() { 48 return name; 49 } 70 public LocalDate getDueDate() { return dueDate; } 71 public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; } 50 72 51 public void setName(String name) { 52 this.name = name; 53 } 73 public String getPriority() { return priority; } 74 public void setPriority(String priority) { this.priority = priority; } 54 75 55 public boolean isFinished() { 56 return finished; 57 } 76 public DisciplineUser getDisciplineUser() { return disciplineUser; } 77 public void setDisciplineUser(DisciplineUser disciplineUser) { this.disciplineUser = disciplineUser; } 58 78 59 public void setFinished(boolean finished) { 60 this.finished = finished; 61 } 62 63 public DisciplineUser getDisciplineUser() { 64 return disciplineUser; 65 } 66 67 public void setDisciplineUser(DisciplineUser disciplineUser) { 68 this.disciplineUser = disciplineUser; 69 } 70 71 public CustomTrackingCategory getCustomTrackingCategory() { 72 return customTrackingCategory; 73 } 74 79 public CustomTrackingCategory getCustomTrackingCategory() { return customTrackingCategory; } 75 80 public void setCustomTrackingCategory(CustomTrackingCategory customTrackingCategory) { 76 81 this.customTrackingCategory = customTrackingCategory; -
backend/src/main/java/com/trekr/backend/repository/TaskRepository.java
r194c016 r545738f 2 2 3 3 import com.trekr.backend.entity.discipline.Task; 4 import com.trekr.backend.entity.discipline.TaskStatus; 4 5 import org.springframework.data.domain.Page; 5 6 import org.springframework.data.domain.Pageable; … … 19 20 long countByDisciplineUser_UserId(Long userId); 20 21 21 long countByDisciplineUser_UserIdAnd FinishedTrue(Long userId);22 long countByDisciplineUser_UserIdAndStatus(Long userId, TaskStatus status); 22 23 23 List<Task> findByDisciplineUser_UserIdAnd FinishedTrue(Long userId);24 List<Task> findByDisciplineUser_UserIdAndStatus(Long userId, TaskStatus status); 24 25 25 26 List<Task> findByCustomTrackingCategory_CustomTrackingIdAndCustomTrackingCategory_User_UserIdOrderByTaskIdDesc( … … 30 31 31 32 @Modifying 32 @Query("update Task t set t. finished = falsewhere t.disciplineUser.userId = :userId")33 int reset FinishedForUser(@Param("userId") Long userId);33 @Query("update Task t set t.status = :status where t.disciplineUser.userId = :userId") 34 int resetStatusForUser(@Param("userId") Long userId, @Param("status") TaskStatus status); 34 35 } 35 -
backend/src/main/java/com/trekr/backend/service/CustomTrackingService.java
r194c016 r545738f 8 8 import com.trekr.backend.dto.discipline.TasksResponse; 9 9 import com.trekr.backend.dto.discipline.UpdateTaskFinishedRequest; 10 import com.trekr.backend.dto.discipline.UpdateTaskStatusRequest; 11 import com.trekr.backend.entity.discipline.TaskStatus; 10 12 import com.trekr.backend.dto.discipline.UpdateTaskRequest; 11 13 import com.trekr.backend.entity.User; … … 81 83 customTrackingId, userId) 82 84 .stream() 83 .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished() ))85 .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished(), t.getStatus(), t.getDescription(), t.getDueDate(), t.getPriority())) 84 86 .toList(); 85 87 … … 101 103 Task task = new Task(); 102 104 task.setName(name.trim()); 103 task.setFinished(false); 105 task.setStatus(TaskStatus.NOT_STARTED); 106 task.setDescription(request.getDescription()); 107 task.setDueDate(request.getDueDate()); 108 task.setPriority(request.getPriority()); 104 109 task.setDisciplineUser(null); 105 110 task.setCustomTrackingCategory(category); 106 111 107 112 Task saved = taskRepository.save(task); 108 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished() );113 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 109 114 } 110 115 … … 127 132 128 133 task.setName(name.trim()); 129 Task saved = taskRepository.save(task); 130 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished()); 134 task.setDescription(request.getDescription()); 135 task.setDueDate(request.getDueDate()); 136 task.setPriority(request.getPriority()); 137 if (request.getStatus() != null) { 138 task.setStatus(request.getStatus()); 139 } 140 Task saved = taskRepository.save(task); 141 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 131 142 } 132 143 … … 151 162 task.setFinished(finished); 152 163 Task saved = taskRepository.save(task); 153 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished()); 164 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 165 } 166 167 @Transactional 168 public TaskDto updateCustomCategoryTaskStatus(Long userId, Long customTrackingId, Long taskId, 169 UpdateTaskStatusRequest request) { 170 Task task = taskRepository.findById(taskId) 171 .orElseThrow(() -> new RuntimeException("Task not found")); 172 173 if (task.getCustomTrackingCategory() == null 174 || !customTrackingId.equals(task.getCustomTrackingCategory().getCustomTrackingId()) 175 || task.getCustomTrackingCategory().getUser() == null 176 || !userId.equals(task.getCustomTrackingCategory().getUser().getUserId())) { 177 throw new RuntimeException("Task not found"); 178 } 179 180 if (request.getStatus() == null) { 181 throw new RuntimeException("status is required"); 182 } 183 184 task.setStatus(request.getStatus()); 185 Task saved = taskRepository.save(task); 186 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 154 187 } 155 188 -
backend/src/main/java/com/trekr/backend/service/DisciplineService.java
r194c016 r545738f 14 14 import com.trekr.backend.entity.discipline.Task; 15 15 import com.trekr.backend.entity.discipline.TaskDailyCompletion; 16 import com.trekr.backend.entity.discipline.TaskStatus; 16 17 import com.trekr.backend.entity.discipline.TaskDailyCompletionId; 17 18 import com.trekr.backend.repository.DailyCompletionRepository; … … 81 82 82 83 List<TaskDto> tasks = result.getContent().stream() 83 .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished() ))84 .map(t -> new TaskDto(t.getTaskId(), t.getName(), t.isFinished(), t.getStatus(), t.getDescription(), t.getDueDate(), t.getPriority())) 84 85 .toList(); 85 86 … … 107 108 108 109 Task saved = taskRepository.save(task); 109 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished() );110 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 110 111 } 111 112 … … 126 127 task.setName(name.trim()); 127 128 Task saved = taskRepository.save(task); 128 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished() );129 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 129 130 } 130 131 … … 145 146 task.setFinished(finished); 146 147 Task saved = taskRepository.save(task); 147 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished() );148 return new TaskDto(saved.getTaskId(), saved.getName(), saved.isFinished(), saved.getStatus(), saved.getDescription(), saved.getDueDate(), saved.getPriority()); 148 149 } 149 150 … … 187 188 188 189 long total = taskRepository.countByDisciplineUser_UserId(userId); 189 long finished = taskRepository.countByDisciplineUser_UserIdAnd FinishedTrue(userId);190 long finished = taskRepository.countByDisciplineUser_UserIdAndStatus(userId, TaskStatus.FINISHED); 190 191 191 192 BigDecimal percent; … … 210 211 // (not strictly required for now, but matches schema and enables future analytics) 211 212 if (finished > 0) { 212 List<TaskDailyCompletion> links = taskRepository.findByDisciplineUser_UserIdAnd FinishedTrue(userId)213 List<TaskDailyCompletion> links = taskRepository.findByDisciplineUser_UserIdAndStatus(userId, TaskStatus.FINISHED) 213 214 .stream() 214 215 .map(t -> { … … 224 225 225 226 // Reset all tasks for next day 226 taskRepository.reset FinishedForUser(userId);227 taskRepository.resetStatusForUser(userId, TaskStatus.NOT_STARTED); 227 228 228 229 DailyCompletionDto dto = new DailyCompletionDto(savedCompletion.getDailyCompletionId(), -
db-scripts/ddl.sql
r194c016 r545738f 96 96 task_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 97 97 name TEXT NOT NULL, 98 is_finished BOOLEAN NOT NULL DEFAULT FALSE, 98 status VARCHAR(20) NOT NULL DEFAULT 'NOT_STARTED', 99 description TEXT, 100 due_date DATE, 101 priority VARCHAR(10), 99 102 100 103 discipline_user_id BIGINT -
db-scripts/dml.sql
r194c016 r545738f 262 262 ); 263 263 264 INSERT INTO TASKS (task_id, name, is_finished, discipline_user_id, custom_tracking_id) VALUES265 (1, 'Morning Run', FALSE, 1, NULL),266 (2, 'Read Book', FALSE, 1, NULL),267 (3, 'Meditation', FALSE, 2, NULL),268 (4, 'Code Review', FALSE, NULL, 1),269 (5, 'Push Updates', FALSE, NULL, 1),270 (6, 'Plan Day', FALSE, 3, NULL),271 (7, 'No Sugar', FALSE, 3, NULL),272 (8, 'Stretch', FALSE, 3, NULL),273 (9, 'Drink Water', FALSE, 4, NULL),274 (10, 'Walk 8k Steps', FALSE, 4, NULL),275 (11, 'Journal', FALSE, 5, NULL),276 (12, 'Gym', FALSE, 5, NULL),277 (13, 'Duolingo', FALSE, NULL, 2),278 (14, 'Watch Lesson', FALSE, NULL, 2),279 (15, 'Practice Problems', FALSE, NULL, 5),280 (16, 'Stretch 10m', FALSE, 7, NULL),281 (17, 'No Soda', FALSE, 7, NULL),282 (18, 'Walk 30m', FALSE, 8, NULL),283 (19, 'Sleep 7h', FALSE, 8, NULL),284 (20, 'Read 20p', FALSE, 9, NULL),285 (21, 'Meditate', FALSE, 9, NULL),286 (22, 'No Sugar', FALSE, 9, NULL),287 (23, 'Drink Water', FALSE, 10, NULL),288 (24, 'Morning Run', FALSE, 11, NULL),289 (25, 'Plan Day', FALSE, 11, NULL),290 (26, 'Study 1h', FALSE, 11, NULL),291 (27, 'Yoga', FALSE, 12, NULL),292 (28, 'Cook Healthy', FALSE, 12, NULL),293 (29, 'Gym', FALSE, 13, NULL),294 (30, 'Protein Target', FALSE, 13, NULL),295 (31, 'Journal', FALSE, 14, NULL),296 (32, 'Early Bed', FALSE, 14, NULL),297 (33, 'Duolingo', FALSE, 15, NULL),298 (34, 'No Alcohol', FALSE, 15, NULL),299 (35, 'Walk 8k Steps', FALSE, 15, NULL),300 (36, 'Attend Lecture', FALSE, NULL, 6),301 (37, 'Solve Tasks', FALSE, NULL, 6),302 (38, 'Review', FALSE, NULL, 6),303 (39, 'Steps', FALSE, NULL, 7),304 (40, 'Water', FALSE, NULL, 7),305 (41, 'Pitch', FALSE, NULL, 8),306 (42, 'Build', FALSE, NULL, 8),307 (43, 'Ship', FALSE, NULL, 8),308 (44, 'Meal Prep', FALSE, NULL, 9),309 (45, 'Groceries', FALSE, NULL, 9),310 (46, 'Read Fiction', FALSE, NULL, 10),311 (47, 'Read Nonfiction', FALSE, NULL, 10),312 (48, 'Standup', FALSE, NULL, 11),313 (49, 'Tickets', FALSE, NULL, 11),314 (50, 'PR Review', FALSE, NULL, 11),315 (51, 'Track Expenses', FALSE, NULL, 12),316 (52, 'Invest', FALSE, NULL, 12),317 (53, 'Duolingo Session', FALSE, NULL, 13),318 (54, 'Podcast', FALSE, NULL, 13),319 (55, 'Squat', FALSE, NULL, 14),320 (56, 'Bench', FALSE, NULL, 14),321 (57, 'Deadlift', FALSE, NULL, 14),322 (58, 'Practice', FALSE, NULL, 15),323 (59, 'Mock', FALSE, NULL, 15),324 (60, 'Review Exam', FALSE, NULL, 15);264 INSERT INTO TASKS (task_id, name, status, discipline_user_id, custom_tracking_id) VALUES 265 (1, 'Morning Run', 'NOT_STARTED', 1, NULL), 266 (2, 'Read Book', 'NOT_STARTED', 1, NULL), 267 (3, 'Meditation', 'NOT_STARTED', 2, NULL), 268 (4, 'Code Review', 'NOT_STARTED', NULL, 1), 269 (5, 'Push Updates', 'NOT_STARTED', NULL, 1), 270 (6, 'Plan Day', 'NOT_STARTED', 3, NULL), 271 (7, 'No Sugar', 'NOT_STARTED', 3, NULL), 272 (8, 'Stretch', 'NOT_STARTED', 3, NULL), 273 (9, 'Drink Water', 'NOT_STARTED', 4, NULL), 274 (10, 'Walk 8k Steps', 'NOT_STARTED', 4, NULL), 275 (11, 'Journal', 'NOT_STARTED', 5, NULL), 276 (12, 'Gym', 'NOT_STARTED', 5, NULL), 277 (13, 'Duolingo', 'NOT_STARTED', NULL, 2), 278 (14, 'Watch Lesson', 'NOT_STARTED', NULL, 2), 279 (15, 'Practice Problems', 'NOT_STARTED', NULL, 5), 280 (16, 'Stretch 10m', 'NOT_STARTED', 7, NULL), 281 (17, 'No Soda', 'NOT_STARTED', 7, NULL), 282 (18, 'Walk 30m', 'NOT_STARTED', 8, NULL), 283 (19, 'Sleep 7h', 'NOT_STARTED', 8, NULL), 284 (20, 'Read 20p', 'NOT_STARTED', 9, NULL), 285 (21, 'Meditate', 'NOT_STARTED', 9, NULL), 286 (22, 'No Sugar', 'NOT_STARTED', 9, NULL), 287 (23, 'Drink Water', 'NOT_STARTED', 10, NULL), 288 (24, 'Morning Run', 'NOT_STARTED', 11, NULL), 289 (25, 'Plan Day', 'NOT_STARTED', 11, NULL), 290 (26, 'Study 1h', 'NOT_STARTED', 11, NULL), 291 (27, 'Yoga', 'NOT_STARTED', 12, NULL), 292 (28, 'Cook Healthy', 'NOT_STARTED', 12, NULL), 293 (29, 'Gym', 'NOT_STARTED', 13, NULL), 294 (30, 'Protein Target', 'NOT_STARTED', 13, NULL), 295 (31, 'Journal', 'NOT_STARTED', 14, NULL), 296 (32, 'Early Bed', 'NOT_STARTED', 14, NULL), 297 (33, 'Duolingo', 'NOT_STARTED', 15, NULL), 298 (34, 'No Alcohol', 'NOT_STARTED', 15, NULL), 299 (35, 'Walk 8k Steps', 'NOT_STARTED', 15, NULL), 300 (36, 'Attend Lecture', 'NOT_STARTED', NULL, 6), 301 (37, 'Solve Tasks', 'NOT_STARTED', NULL, 6), 302 (38, 'Review', 'NOT_STARTED', NULL, 6), 303 (39, 'Steps', 'NOT_STARTED', NULL, 7), 304 (40, 'Water', 'NOT_STARTED', NULL, 7), 305 (41, 'Pitch', 'NOT_STARTED', NULL, 8), 306 (42, 'Build', 'NOT_STARTED', NULL, 8), 307 (43, 'Ship', 'NOT_STARTED', NULL, 8), 308 (44, 'Meal Prep', 'NOT_STARTED', NULL, 9), 309 (45, 'Groceries', 'NOT_STARTED', NULL, 9), 310 (46, 'Read Fiction', 'NOT_STARTED', NULL, 10), 311 (47, 'Read Nonfiction', 'NOT_STARTED', NULL, 10), 312 (48, 'Standup', 'NOT_STARTED', NULL, 11), 313 (49, 'Tickets', 'NOT_STARTED', NULL, 11), 314 (50, 'PR Review', 'NOT_STARTED', NULL, 11), 315 (51, 'Track Expenses', 'NOT_STARTED', NULL, 12), 316 (52, 'Invest', 'NOT_STARTED', NULL, 12), 317 (53, 'Duolingo Session', 'NOT_STARTED', NULL, 13), 318 (54, 'Podcast', 'NOT_STARTED', NULL, 13), 319 (55, 'Squat', 'NOT_STARTED', NULL, 14), 320 (56, 'Bench', 'NOT_STARTED', NULL, 14), 321 (57, 'Deadlift', 'NOT_STARTED', NULL, 14), 322 (58, 'Practice', 'NOT_STARTED', NULL, 15), 323 (59, 'Mock', 'NOT_STARTED', NULL, 15), 324 (60, 'Review Exam', 'NOT_STARTED', NULL, 15); 325 325 326 326 SELECT setval( -
frontend/src/api/discipline.js
r194c016 r545738f 81 81 } 82 82 83 export async function updateCustomCategoryTaskStatus(customTrackingId, taskId, status) { 84 const res = await api.patch( 85 `/discipline/custom-tracking-categories/${customTrackingId}/tasks/${taskId}/status`, 86 { status } 87 ); 88 return res?.data; 89 } 90 83 91 export async function deleteCustomCategoryTask(customTrackingId, taskId) { 84 92 return api.delete( -
frontend/src/pages/Dashboard/pages/CustomCategory/CustomCategoryKanban.jsx
r194c016 r545738f 8 8 getCustomTrackingCategories, 9 9 updateCustomCategoryTask, 10 updateCustomCategoryTask Finished,10 updateCustomCategoryTaskStatus, 11 11 } from "../../../../api/discipline.js"; 12 12 … … 23 23 24 24 const lanes = useMemo(() => { 25 const notDone = (tasks ?? []).filter((t) => !t.isFinished); 26 const done = (tasks ?? []).filter((t) => t.isFinished); 27 return { notDone, done }; 25 const all = tasks ?? []; 26 return { 27 notStarted: all.filter((t) => t.status === "NOT_STARTED"), 28 inProgress: all.filter((t) => t.status === "IN_PROGRESS"), 29 finished: all.filter((t) => t.status === "FINISHED"), 30 }; 28 31 }, [tasks]); 29 32 … … 36 39 getCustomCategoryTasks(customTrackingId), 37 40 ]); 38 39 41 const cats = catsRes?.items ?? []; 40 42 const found = cats.find((c) => Number(c.customTrackingId) === customTrackingId); 41 43 setCategoryName(found?.name ?? "Custom Category"); 42 43 44 setTasks(tasksRes?.tasks ?? []); 44 45 } catch (e) { … … 59 60 }, [customTrackingId]); 60 61 61 async function handleAddTask( name) {62 const created = await createCustomCategoryTask(customTrackingId, { name });62 async function handleAddTask(payload) { 63 const created = await createCustomCategoryTask(customTrackingId, payload); 63 64 setTasks((prev) => [created, ...(prev ?? [])]); 64 65 } 65 66 66 async function handleEditTask(taskId, name) {67 const updated = await updateCustomCategoryTask(customTrackingId, taskId, { name });67 async function handleEditTask(taskId, payload) { 68 const updated = await updateCustomCategoryTask(customTrackingId, taskId, payload); 68 69 setTasks((prev) => (prev ?? []).map((t) => (t.taskId === taskId ? updated : t))); 69 70 } … … 74 75 } 75 76 76 async function handleMoveTask(taskId, toFinished) { 77 // optimistic update 77 async function handleMoveTask(taskId, toStatus) { 78 78 setTasks((prev) => 79 (prev ?? []).map((t) => 80 t.taskId === taskId ? { ...t, isFinished: toFinished } : t 81 ) 79 (prev ?? []).map((t) => (t.taskId === taskId ? { ...t, status: toStatus } : t)) 82 80 ); 83 84 81 try { 85 const saved = await updateCustomCategoryTaskFinished( 86 customTrackingId, 87 taskId, 88 toFinished 89 ); 82 const saved = await updateCustomCategoryTaskStatus(customTrackingId, taskId, toStatus); 90 83 setTasks((prev) => (prev ?? []).map((t) => (t.taskId === taskId ? saved : t))); 91 } catch (e) { 92 // revert by refetching (simple + correct) 84 } catch { 93 85 await refresh(); 94 throw e;95 86 } 96 87 } … … 102 93 <h1 className="text-2xl font-bold">{categoryName}</h1> 103 94 <p className="text-sm opacity-80"> 104 Drag tickets between columns to mark them done / not done.95 Drag tickets between columns or use the edit form to update status. 105 96 </p> 106 97 </div> … … 115 106 <CustomCategoryKanbanBoard 116 107 loading={loading} 117 notDone={lanes.notDone} 118 done={lanes.done} 108 notStarted={lanes.notStarted} 109 inProgress={lanes.inProgress} 110 finished={lanes.finished} 119 111 onAddTask={handleAddTask} 120 112 onEditTask={handleEditTask} … … 125 117 ); 126 118 } 127 128 -
frontend/src/pages/Dashboard/pages/CustomCategory/components/CustomCategoryKanbanBoard.jsx
r194c016 r545738f 1 1 import React, { useMemo, useState } from "react"; 2 3 import { 4 DndContext, 5 PointerSensor, 6 closestCenter, 7 useSensor, 8 useSensors, 9 } from "@dnd-kit/core"; 10 import { 11 SortableContext, 12 arrayMove, 13 rectSortingStrategy, 14 } from "@dnd-kit/sortable"; 2 import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core"; 3 import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"; 4 import { MdAdd, MdSort } from "react-icons/md"; 15 5 16 6 import KanbanColumn from "./KanbanColumn"; 17 7 import TaskTicketCard from "./TaskTicketCard"; 8 import TaskFormModal from "./TaskFormModal"; 18 9 19 function laneIdForTask(task) { 20 return task?.isFinished ? "done" : "notDone"; 10 const LANES = [ 11 { id: "NOT_STARTED", droppableId: "lane:NOT_STARTED", title: "Not Started" }, 12 { id: "IN_PROGRESS", droppableId: "lane:IN_PROGRESS", title: "In Progress" }, 13 { id: "FINISHED", droppableId: "lane:FINISHED", title: "Finished" }, 14 ]; 15 16 const PRIORITY_ORDER = { HIGH: 0, MEDIUM: 1, LOW: 2, null: 3, undefined: 3 }; 17 18 function sortTasks(tasks, sortBy) { 19 if (!tasks || tasks.length === 0) return tasks; 20 const arr = [...tasks]; 21 if (sortBy === "dueDate") { 22 arr.sort((a, b) => { 23 if (!a.dueDate && !b.dueDate) return 0; 24 if (!a.dueDate) return 1; 25 if (!b.dueDate) return -1; 26 return new Date(a.dueDate) - new Date(b.dueDate); 27 }); 28 } else if (sortBy === "priority") { 29 arr.sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 3) - (PRIORITY_ORDER[b.priority] ?? 3)); 30 } 31 return arr; 32 } 33 34 function statusForDropId(dropId) { 35 return dropId.replace("lane:", ""); 21 36 } 22 37 23 38 export default function CustomCategoryKanbanBoard({ 24 loading, 25 notDone, 26 done, 27 onAddTask, 28 onEditTask, 29 onDeleteTask, 30 onMoveTask, 39 loading, notStarted, inProgress, finished, 40 onAddTask, onEditTask, onDeleteTask, onMoveTask, 31 41 }) { 32 const [newName, setNewName] = useState(""); 33 const [submitting, setSubmitting] = useState(false); 42 const [modalOpen, setModalOpen] = useState(false); 43 const [editingTask, setEditingTask] = useState(null); 44 const [modalLoading, setModalLoading] = useState(false); 45 const [sortBy, setSortBy] = useState("default"); 34 46 35 47 const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } })); 36 48 37 // Keep a local order per lane for nicer UX, but still driven by server state. 38 const [notDoneOrder, setNotDoneOrder] = useState([]); 39 const [doneOrder, setDoneOrder] = useState([]); 49 const tasksByLane = useMemo(() => ({ 50 NOT_STARTED: sortTasks(notStarted ?? [], sortBy), 51 IN_PROGRESS: sortTasks(inProgress ?? [], sortBy), 52 FINISHED: sortTasks(finished ?? [], sortBy), 53 }), [notStarted, inProgress, finished, sortBy]); 40 54 41 const orderedNotDone = useMemo(() => { 42 const byId = new Map((notDone ?? []).map((t) => [String(t.taskId), t])); 43 const ids = [ 44 ...notDoneOrder.filter((id) => byId.has(id)), 45 ...Array.from(byId.keys()).filter((id) => !notDoneOrder.includes(id)), 46 ]; 47 return ids.map((id) => byId.get(id)); 48 }, [notDone, notDoneOrder]); 55 const allTasks = useMemo(() => [ 56 ...(notStarted ?? []), 57 ...(inProgress ?? []), 58 ...(finished ?? []), 59 ], [notStarted, inProgress, finished]); 49 60 50 const orderedDone = useMemo(() => { 51 const byId = new Map((done ?? []).map((t) => [String(t.taskId), t])); 52 const ids = [ 53 ...doneOrder.filter((id) => byId.has(id)), 54 ...Array.from(byId.keys()).filter((id) => !doneOrder.includes(id)), 55 ]; 56 return ids.map((id) => byId.get(id)); 57 }, [done, doneOrder]); 61 function findTask(id) { 62 return allTasks.find((t) => String(t.taskId) === String(id)); 63 } 58 64 59 async function submitNewTask(e) { 60 e.preventDefault(); 61 const name = newName.trim(); 62 if (!name || submitting) return; 63 setSubmitting(true); 64 try { 65 await onAddTask(name); 66 setNewName(""); 67 } finally { 68 setSubmitting(false); 65 async function handleDragEnd({ active, over }) { 66 if (!over) return; 67 const activeId = String(active.id); 68 const overId = String(over.id); 69 if (activeId === overId) return; 70 71 const task = findTask(activeId); 72 if (!task) return; 73 74 if (overId.startsWith("lane:")) { 75 const toStatus = statusForDropId(overId); 76 if (task.status !== toStatus) await onMoveTask(task.taskId, toStatus); 77 return; 78 } 79 80 const overTask = findTask(overId); 81 if (!overTask) return; 82 83 if (task.status !== overTask.status) { 84 await onMoveTask(task.taskId, overTask.status); 69 85 } 70 86 } 71 87 72 function findTaskById(id) { 73 const all = [...(notDone ?? []), ...(done ?? [])]; 74 return all.find((t) => String(t.taskId) === String(id)); 75 } 76 77 async function handleDragEnd(event) { 78 const { active, over } = event; 79 if (!over) return; 80 81 const activeId = String(active.id); 82 const overId = String(over.id); 83 84 if (activeId === overId) return; 85 86 const task = findTaskById(activeId); 87 if (!task) return; 88 89 // Moving over a column dropzone 90 if (overId === "lane:notDone" || overId === "lane:done") { 91 const toFinished = overId === "lane:done"; 92 if (Boolean(task.isFinished) === toFinished) return; 93 await onMoveTask(task.taskId, toFinished); 94 return; 95 } 96 97 // Sorting within a lane: keep it frontend-only. 98 const overTask = findTaskById(overId); 99 if (!overTask) return; 100 101 const fromLane = laneIdForTask(task); 102 const toLane = laneIdForTask(overTask); 103 104 if (fromLane !== toLane) { 105 // if dropped onto a card in the other lane => treat as lane move 106 await onMoveTask(task.taskId, toLane === "done"); 107 return; 108 } 109 110 if (fromLane === "notDone") { 111 const ids = orderedNotDone.map((t) => String(t.taskId)); 112 const oldIndex = ids.indexOf(activeId); 113 const newIndex = ids.indexOf(overId); 114 if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) { 115 setNotDoneOrder(arrayMove(ids, oldIndex, newIndex)); 88 async function handleModalSubmit(payload) { 89 setModalLoading(true); 90 try { 91 if (editingTask) { 92 await onEditTask(editingTask.taskId, payload); 93 } else { 94 await onAddTask(payload); 116 95 } 117 } else { 118 const ids = orderedDone.map((t) => String(t.taskId)); 119 const oldIndex = ids.indexOf(activeId); 120 const newIndex = ids.indexOf(overId); 121 if (oldIndex >= 0 && newIndex >= 0 && oldIndex !== newIndex) { 122 setDoneOrder(arrayMove(ids, oldIndex, newIndex)); 123 } 96 setModalOpen(false); 97 setEditingTask(null); 98 } finally { 99 setModalLoading(false); 124 100 } 125 101 } 126 102 103 const total = allTasks.length; 104 127 105 return ( 128 <div className="space-y-6"> 129 <div className="rounded-xl border border-white/10 bg-neutral-900/40 p-4"> 130 <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"> 131 <div> 132 <div className="text-sm font-semibold text-white/90"> 133 Tickets 134 <span className="ml-2 text-xs font-normal text-white/60"> 135 Drag cards left/right to change status. 136 </span> 137 </div> 138 <div className="mt-1 text-xs text-white/55"> 139 Tip: grab the <span className="font-semibold text-white/80">drag</span> handle. 140 </div> 106 <div className="space-y-4"> 107 <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-white/10 bg-neutral-900/40 px-4 py-3"> 108 <div className="flex flex-wrap items-center gap-3 text-xs text-white/70"> 109 <span className="rounded-full bg-slate-500/20 px-2 py-1 text-slate-200"> 110 Not started: {(notStarted ?? []).length} 111 </span> 112 <span className="rounded-full bg-amber-500/15 px-2 py-1 text-amber-200"> 113 In progress: {(inProgress ?? []).length} 114 </span> 115 <span className="rounded-full bg-emerald-500/15 px-2 py-1 text-emerald-200"> 116 Finished: {(finished ?? []).length} 117 </span> 118 <span className="text-white/40">/ {total} total</span> 119 </div> 120 121 <div className="flex items-center gap-2"> 122 <div className="flex items-center gap-1.5 rounded-lg border border-white/10 bg-black/30 px-2 py-1.5"> 123 <MdSort className="size-4 text-white/50" /> 124 <span className="text-xs text-white/50">Sort:</span> 125 <select 126 value={sortBy} 127 onChange={(e) => setSortBy(e.target.value)} 128 className="bg-transparent text-xs text-white/80 outline-none cursor-pointer" 129 > 130 <option value="default">Default</option> 131 <option value="dueDate">Due date</option> 132 <option value="priority">Priority</option> 133 </select> 141 134 </div> 142 135 143 <div className="flex items-center gap-3 text-xs text-white/70"> 144 <span className="rounded-full bg-sky-500/15 px-2 py-1 text-sky-200"> 145 Not done: {(notDone ?? []).length} 146 </span> 147 <span className="rounded-full bg-emerald-500/15 px-2 py-1 text-emerald-200"> 148 Done: {(done ?? []).length} 149 </span> 150 </div> 136 <button 137 onClick={() => { setEditingTask(null); setModalOpen(true); }} 138 className="flex items-center gap-1.5 rounded-lg bg-green-400 px-3 py-1.5 text-sm font-medium text-black hover:bg-green-300" 139 > 140 <MdAdd className="size-4" /> 141 New ticket 142 </button> 151 143 </div> 152 153 <form onSubmit={submitNewTask} className="mt-4 flex flex-col gap-3 md:flex-row">154 <input155 value={newName}156 onChange={(e) => setNewName(e.target.value)}157 placeholder="Add a new ticket (e.g. Fix bug #123)"158 maxLength={200}159 className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm outline-none focus:border-green-400"160 />161 <button162 className="rounded-lg bg-green-400 px-4 py-2 text-sm font-medium text-black disabled:opacity-60"163 type="submit"164 disabled={!newName.trim() || submitting}165 >166 {submitting ? "Adding..." : "Add"}167 </button>168 </form>169 144 </div> 170 145 171 <DndContext 172 sensors={sensors} 173 collisionDetection={closestCenter} 174 onDragEnd={handleDragEnd} 175 > 176 <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> 177 <KanbanColumn 178 title="Not done" 179 droppableId="lane:notDone" 180 count={orderedNotDone.length} 181 > 182 <SortableContext 183 items={orderedNotDone.map((t) => String(t.taskId))} 184 strategy={rectSortingStrategy} 185 > 186 <div className="space-y-3"> 187 {loading ? ( 188 <div className="text-sm opacity-70">Loading...</div> 189 ) : orderedNotDone.length === 0 ? ( 190 <div className="text-sm opacity-70">No tickets yet.</div> 191 ) : ( 192 orderedNotDone.map((t) => ( 193 <TaskTicketCard 194 key={t.taskId} 195 task={t} 196 onEdit={(name) => onEditTask(t.taskId, name)} 197 onDelete={() => onDeleteTask(t.taskId)} 198 /> 199 )) 200 )} 201 </div> 202 </SortableContext> 203 </KanbanColumn> 204 205 <KanbanColumn 206 title="Done" 207 droppableId="lane:done" 208 count={orderedDone.length} 209 > 210 <SortableContext 211 items={orderedDone.map((t) => String(t.taskId))} 212 strategy={rectSortingStrategy} 213 > 214 <div className="space-y-3"> 215 {loading ? ( 216 <div className="text-sm opacity-70">Loading...</div> 217 ) : orderedDone.length === 0 ? ( 218 <div className="text-sm opacity-70">Nothing done yet.</div> 219 ) : ( 220 orderedDone.map((t) => ( 221 <TaskTicketCard 222 key={t.taskId} 223 task={t} 224 onEdit={(name) => onEditTask(t.taskId, name)} 225 onDelete={() => onDeleteTask(t.taskId)} 226 /> 227 )) 228 )} 229 </div> 230 </SortableContext> 231 </KanbanColumn> 146 <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> 147 <div className="grid grid-cols-1 gap-4 md:grid-cols-3"> 148 {LANES.map((lane) => { 149 const tasks = tasksByLane[lane.id] ?? []; 150 return ( 151 <KanbanColumn 152 key={lane.id} 153 laneId={lane.id} 154 title={lane.title} 155 droppableId={lane.droppableId} 156 count={tasks.length} 157 > 158 <SortableContext 159 items={tasks.map((t) => String(t.taskId))} 160 strategy={rectSortingStrategy} 161 > 162 <div className="space-y-3"> 163 {loading ? ( 164 <div className="text-sm opacity-70">Loading...</div> 165 ) : tasks.length === 0 ? ( 166 <div className="text-sm opacity-40">No tickets.</div> 167 ) : ( 168 tasks.map((t) => ( 169 <TaskTicketCard 170 key={t.taskId} 171 task={t} 172 onEdit={() => { setEditingTask(t); setModalOpen(true); }} 173 onDelete={() => onDeleteTask(t.taskId)} 174 /> 175 )) 176 )} 177 </div> 178 </SortableContext> 179 </KanbanColumn> 180 ); 181 })} 232 182 </div> 233 183 </DndContext> 184 185 <TaskFormModal 186 isOpen={modalOpen} 187 onClose={() => { setModalOpen(false); setEditingTask(null); }} 188 onSubmit={handleModalSubmit} 189 task={editingTask} 190 isLoading={modalLoading} 191 /> 234 192 </div> 235 193 ); 236 194 } 237 238 -
frontend/src/pages/Dashboard/pages/CustomCategory/components/KanbanColumn.jsx
r194c016 r545738f 1 1 import React, { useMemo } from "react"; 2 2 import { useDroppable } from "@dnd-kit/core"; 3 import { MdCheckCircle, MdRadioButtonUnchecked } from "react-icons/md";3 import { MdCheckCircle, MdRadioButtonUnchecked, MdAutorenew } from "react-icons/md"; 4 4 5 export default function KanbanColumn({ title, droppableId, count, children }) { 5 const VARIANTS = { 6 NOT_STARTED: { 7 Icon: MdRadioButtonUnchecked, 8 header: "from-slate-500/20 via-slate-500/8 to-transparent", 9 badge: "bg-slate-500/15 text-slate-200 border-slate-500/30", 10 ring: "group-hover:ring-slate-500/25", 11 drop: "border-slate-400/60 bg-slate-400/10", 12 border: "border-slate-500/20", 13 }, 14 IN_PROGRESS: { 15 Icon: MdAutorenew, 16 header: "from-amber-500/20 via-amber-500/8 to-transparent", 17 badge: "bg-amber-500/15 text-amber-200 border-amber-500/30", 18 ring: "group-hover:ring-amber-500/25", 19 drop: "border-amber-400/60 bg-amber-400/10", 20 border: "border-amber-500/20", 21 }, 22 FINISHED: { 23 Icon: MdCheckCircle, 24 header: "from-emerald-500/25 via-emerald-500/10 to-transparent", 25 badge: "bg-emerald-500/15 text-emerald-200 border-emerald-500/30", 26 ring: "group-hover:ring-emerald-500/25", 27 drop: "border-emerald-400/60 bg-emerald-400/10", 28 border: "border-emerald-500/20", 29 }, 30 }; 31 32 export default function KanbanColumn({ laneId, title, droppableId, count, children }) { 6 33 const { setNodeRef, isOver } = useDroppable({ id: droppableId }); 7 8 const variant = useMemo(() => { 9 if (droppableId === "lane:done") { 10 return { 11 Icon: MdCheckCircle, 12 header: "from-emerald-500/25 via-emerald-500/10 to-transparent", 13 badge: "bg-emerald-500/15 text-emerald-200 border-emerald-500/30", 14 ring: "group-hover:ring-emerald-500/25", 15 drop: "border-emerald-400/60 bg-emerald-400/10", 16 border: "border-emerald-500/20", 17 }; 18 } 19 20 return { 21 Icon: MdRadioButtonUnchecked, 22 header: "from-sky-500/25 via-sky-500/10 to-transparent", 23 badge: "bg-sky-500/15 text-sky-200 border-sky-500/30", 24 ring: "group-hover:ring-sky-500/25", 25 drop: "border-sky-400/60 bg-sky-400/10", 26 border: "border-sky-500/20", 27 }; 28 }, [droppableId]); 34 const v = VARIANTS[laneId] ?? VARIANTS.NOT_STARTED; 29 35 30 36 return ( … … 32 38 className={ 33 39 "group overflow-hidden rounded-xl border bg-neutral-900/40 shadow-sm ring-1 ring-white/5 transition " + 34 variant.border + 35 " " + 36 variant.ring 40 v.border + " " + v.ring 37 41 } 38 42 > 39 <div className={"bg-gradient-to-b " + v ariant.header + " p-4"}>43 <div className={"bg-gradient-to-b " + v.header + " p-4"}> 40 44 <div className="flex items-center justify-between gap-3"> 41 45 <div className="flex items-center gap-2"> 42 <variant.Icon className="size-5 text-white/80" /> 43 <h2 className="text-sm font-semibold tracking-wide text-white/90"> 44 {title} 45 </h2> 46 <v.Icon className="size-5 text-white/80" /> 47 <h2 className="text-sm font-semibold tracking-wide text-white/90">{title}</h2> 46 48 </div> 47 <span 48 className={ 49 "rounded-full border px-2 py-0.5 text-xs font-medium " + 50 variant.badge 51 } 52 > 49 <span className={"rounded-full border px-2 py-0.5 text-xs font-medium " + v.badge}> 53 50 {count} 54 51 </span> … … 60 57 className={ 61 58 "min-h-[220px] p-4 transition-colors " + 62 (isOver ? v ariant.drop : "border-t border-white/10")59 (isOver ? v.drop : "border-t border-white/10") 63 60 } 64 61 > … … 68 65 ); 69 66 } 70 -
frontend/src/pages/Dashboard/pages/CustomCategory/components/TaskTicketCard.jsx
r194c016 r545738f 1 import React, { useMemo , useState} from "react";1 import React, { useMemo } from "react"; 2 2 import { useSortable } from "@dnd-kit/sortable"; 3 3 import { CSS } from "@dnd-kit/utilities"; 4 import { MdDelete, MdDragIndicator, MdEdit, MdSave } from "react-icons/md"; 4 import { MdDelete, MdDragIndicator, MdEdit, MdCalendarToday } from "react-icons/md"; 5 6 const PRIORITY_STYLES = { 7 HIGH: "bg-red-500/15 text-red-300 border-red-500/30", 8 MEDIUM: "bg-amber-500/15 text-amber-300 border-amber-500/30", 9 LOW: "bg-sky-500/15 text-sky-300 border-sky-500/30", 10 }; 11 12 const STATUS_DOT = { 13 NOT_STARTED: "bg-slate-400", 14 IN_PROGRESS: "bg-amber-400", 15 FINISHED: "bg-emerald-400", 16 }; 17 18 function formatDate(dateStr) { 19 if (!dateStr) return null; 20 const d = new Date(dateStr); 21 return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); 22 } 23 24 function isOverdue(dateStr) { 25 if (!dateStr) return false; 26 return new Date(dateStr) < new Date(new Date().toDateString()); 27 } 5 28 6 29 export default function TaskTicketCard({ task, onEdit, onDelete }) { 7 30 const id = String(task.taskId); 8 const { 9 attributes, 10 listeners, 11 setNodeRef, 12 transform, 31 const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id }); 32 33 const style = useMemo(() => ({ 34 transform: CSS.Transform.toString(transform), 13 35 transition, 14 isDragging, 15 } = useSortable({ id }); 36 }), [transform, transition]); 16 37 17 const style = useMemo( 18 () => ({ 19 transform: CSS.Transform.toString(transform), 20 transition, 21 }), 22 [transform, transition] 23 ); 24 25 const [editing, setEditing] = useState(false); 26 const [draft, setDraft] = useState(task.name ?? ""); 27 28 async function submitEdit(e) { 29 e.preventDefault(); 30 const next = draft.trim(); 31 if (!next) return; 32 await onEdit(next); 33 setEditing(false); 34 } 38 const overdue = task.status !== "FINISHED" && isOverdue(task.dueDate); 35 39 36 40 return ( … … 40 44 className={ 41 45 "group rounded-lg border border-white/10 bg-black/30 p-3 shadow-sm transition hover:border-white/20 hover:bg-black/35 " + 42 (task. isFinished ? "opacity-90" : "") +43 (isDragging ? " opacity- 60" : "")46 (task.status === "FINISHED" ? "opacity-80" : "") + 47 (isDragging ? " opacity-50 scale-95" : "") 44 48 } 45 49 > 46 <div className="flex items-start justify-between gap-3"> 50 {/* Header row */} 51 <div className="flex items-start justify-between gap-2"> 47 52 <div className="min-w-0 flex-1"> 48 {editing ? ( 49 <form onSubmit={submitEdit} className="flex gap-2"> 50 <input 51 value={draft} 52 onChange={(e) => setDraft(e.target.value)} 53 maxLength={200} 54 className="w-full rounded-md border border-white/10 bg-black/40 px-2 py-1 text-sm outline-none focus:border-green-400" 55 autoFocus 56 /> 57 <button 58 type="submit" 59 className="inline-flex items-center gap-1 rounded-md bg-green-400 px-2 py-1 text-sm font-medium text-black" 60 > 61 <MdSave className="size-4" /> 62 Save 63 </button> 64 </form> 65 ) : ( 66 <div className="flex items-center gap-2"> 67 <span 68 className={ 69 "h-2 w-2 shrink-0 rounded-full " + 70 (task.isFinished ? "bg-emerald-400" : "bg-sky-400") 71 } 72 /> 73 <div className="min-w-0 text-sm font-medium text-white/90"> 74 {task.name} 75 </div> 76 </div> 77 )} 53 <div className="flex items-center gap-2"> 54 <span className={"h-2 w-2 shrink-0 rounded-full " + (STATUS_DOT[task.status] ?? "bg-slate-400")} /> 55 <span className="text-sm font-medium text-white/90 leading-snug">{task.name}</span> 56 </div> 78 57 79 <div className="mt-1 text-xs text-white/50">Ticket #{task.taskId}</div> 58 {task.description ? ( 59 <p className="mt-1.5 text-xs text-white/50 line-clamp-2">{task.description}</p> 60 ) : null} 80 61 </div> 81 62 82 <div className="flex shrink-0 items-center gap-2"> 83 {!editing ? ( 84 <button 85 type="button" 86 onClick={() => { 87 setDraft(task.name ?? ""); 88 setEditing(true); 89 }} 90 className="rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white" 91 title="Edit" 92 > 93 <MdEdit className="size-5" /> 94 </button> 95 ) : null} 96 63 <div className="flex shrink-0 items-center gap-1"> 64 <button 65 type="button" 66 onClick={onEdit} 67 className="rounded-md p-1 text-white/50 hover:bg-white/10 hover:text-white" 68 title="Edit" 69 > 70 <MdEdit className="size-4" /> 71 </button> 97 72 <button 98 73 type="button" 99 74 onClick={onDelete} 100 className="rounded-md p-1 text-white/ 70 hover:bg-white/10 hover:text-white"75 className="rounded-md p-1 text-white/50 hover:bg-white/10 hover:text-white" 101 76 title="Delete" 102 77 > 103 <MdDelete className="size- 5" />78 <MdDelete className="size-4" /> 104 79 </button> 105 106 80 <button 107 81 type="button" 108 className=" inline-flex cursor-grab items-center gap-1 rounded-md p-1 text-white/70 hover:bg-white/10 hover:text-white active:cursor-grabbing"82 className="cursor-grab rounded-md p-1 text-white/50 hover:bg-white/10 hover:text-white active:cursor-grabbing" 109 83 title="Drag" 110 84 {...attributes} 111 85 {...listeners} 112 86 > 113 <MdDragIndicator className="size- 5" />87 <MdDragIndicator className="size-4" /> 114 88 </button> 115 89 </div> 90 </div> 91 92 {/* Footer row: priority + due date */} 93 <div className="mt-2 flex flex-wrap items-center gap-2"> 94 <span className="text-xs text-white/30">#{task.taskId}</span> 95 96 {task.priority ? ( 97 <span className={"rounded-full border px-2 py-0.5 text-xs font-medium " + (PRIORITY_STYLES[task.priority] ?? PRIORITY_STYLES.LOW)}> 98 {task.priority.charAt(0) + task.priority.slice(1).toLowerCase()} 99 </span> 100 ) : null} 101 102 {task.dueDate ? ( 103 <span className={"flex items-center gap-1 text-xs " + (overdue ? "text-red-400" : "text-white/40")}> 104 <MdCalendarToday className="size-3" /> 105 {overdue ? "Overdue · " : ""}{formatDate(task.dueDate)} 106 </span> 107 ) : null} 116 108 </div> 117 109 </div> 118 110 ); 119 111 } 120
Note:
See TracChangeset
for help on using the changeset viewer.
