Skip to content
Merged

Bork #24

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions MtdrSpring/README_DELETE_FUNCTIONALITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Delete Functionality - Team Manager Only

This document describes the new delete functionality that has been added to the TaskO application.

## Backend Changes

### 1. Sprint Delete Endpoint
- **Endpoint**: `DELETE /sprint/{id}`
- **Location**: `SprintItemController.java`
- **Description**: Allows deletion of sprints by UUID

### 2. Task Delete Endpoint
- **Endpoint**: `DELETE /task/{id}`
- **Location**: `TaskController.java` (already existed)
- **Description**: Allows deletion of tasks by UUID

### 3. Manager Check Endpoint
- **Endpoint**: `GET /projects/{projectId}/manager/{userId}`
- **Location**: `ProjectMemberItemController.java`
- **Description**: Checks if a user is a manager for a specific project
- **Logic**: Currently considers the first user in a project as the manager

## Frontend Changes

### 1. Delete Sprint Dialog (`DeleteSprintDialog.tsx`)
- Confirmation dialog for deleting sprints
- Shows warning about consequences
- Only accessible to team managers

### 2. Delete Task Dialog (`DeleteTaskDialog.tsx`)
- Confirmation dialog for deleting tasks
- Shows warning about permanent deletion
- Only accessible to team managers

### 3. Manager Hook (`useManager.tsx`)
- Custom React hook to check if current user is a manager
- Automatically checks manager status for the current project
- Returns loading state and manager status

### 4. Updated Components

#### TaskItem Component (`Task-item.tsx`)
- Added `isManager` prop
- Delete button only visible to managers
- Integrates with delete task functionality

#### Sprints Component (`Sprints.tsx`)
- Uses `useManager` hook to check manager status
- Shows delete sprint button only to managers
- Passes manager status to TaskItem components
- Handles sprint deletion and UI updates

## Manager Logic

Currently, the application considers the **first user added to a project** as the team manager. This is a simple implementation that can be enhanced later with:

- Role-based permissions
- Multiple managers per project
- Admin roles
- Permission inheritance

## Security Notes

- All delete operations require manager privileges
- Frontend UI only shows delete buttons to managers
- Backend endpoints should also validate manager status (recommended enhancement)
- Delete operations are permanent and cannot be undone

## How to Test

1. **Restart your backend** after running the database migration for priority support
2. **Login as the first user** who was added to a project (they will be the manager)
3. **Navigate to Sprints page**
4. **Look for red trash icons** next to sprints (if you're a manager)
5. **Expand a sprint** and look for red trash icons next to tasks (if you're a manager)
6. **Try deleting** - you'll see confirmation dialogs with warnings

## Future Enhancements

- Add role-based access control (RBAC)
- Add audit logging for delete operations
- Add soft delete with recovery options
- Add bulk delete operations
- Add permission management UI
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ public ResponseEntity<ProjectMemberItem> addUserToProject(@RequestBody Map<Strin
String userId = payload.get("userId");
return projectMemberItemService.addUserToProject(userId, projectId, teamId);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,18 @@ public ResponseEntity<SprintItem> addSprintItem(@RequestBody SprintItem sprintIt
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@DeleteMapping("/sprint/{id}")
public ResponseEntity<Void> deleteSprintItem(@PathVariable("id") UUID id) {
try {
boolean deleted = sprintItemService.deleteSprintItem(id);
if (deleted) {
return ResponseEntity.noContent().build();
} else {
return ResponseEntity.notFound().build();
}
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ public enum Status {
private Double estimatedHours;
@Column(name = "REALHOURS")
private Double realHours;
@Column(name = "PRIORITY")
private String priority;

public TaskItem(){
}
public TaskItem(UUID projectId, UUID sprintId, UUID taskId, String title, String description, String assignee, Status status, OffsetDateTime startDate, OffsetDateTime endDate, String comments, Integer storyPoints, Double estimatedHours, Double realHours) {
public TaskItem(UUID projectId, UUID sprintId, UUID taskId, String title, String description, String assignee, Status status, OffsetDateTime startDate, OffsetDateTime endDate, String comments, Integer storyPoints, Double estimatedHours, Double realHours, String priority) {
this.projectId = projectId;
this.sprintId = sprintId;
this.taskId = taskId;
Expand All @@ -65,6 +67,7 @@ public TaskItem(UUID projectId, UUID sprintId, UUID taskId, String title, String
this.storyPoints = storyPoints;
this.estimatedHours = estimatedHours;
this.realHours = realHours;
this.priority = priority;
}

public UUID getProjectId() {
Expand Down Expand Up @@ -174,6 +177,14 @@ public Double getRealHours() {
public void setRealHours(Double realHours) {
this.realHours = realHours;
}

public String getPriority() {
return priority;
}

public void setPriority(String priority) {
this.priority = priority;
}

@Override
public String toString() {
Expand All @@ -191,6 +202,7 @@ public String toString() {
", storyPoints=" + storyPoints +
", estimatedHours=" + estimatedHours +
", realHours=" + realHours +
", priority='" + priority + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ public ResponseEntity<ProjectMemberItem> addUserToProject( String UserId, UUID p
return new ResponseEntity<>(projectMemberItem, HttpStatus.CREATED);
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public TaskItem updateTaskItem(UUID id, TaskItem t) {
if (t.getEstimatedHours() != null) {
toDoItem.setEstimatedHours(t.getEstimatedHours());
}
if (t.getPriority() != null) {
toDoItem.setPriority(t.getPriority());
}

TaskItem savedTask = toDoItemRepository.save(toDoItem);
System.out.println("Updated task state: " + savedTask);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
#oracle.jdbc.fanEnabled=false
##this is not used when deployed in kubernetes. Just for local testing
spring.datasource.url=jdbc:oracle:thin:@mtdrdb291_medium?TNS_ADMIN=/Users/ID140/Documents/TaskO/MtdrSpring/backend/db_wallet
spring.datasource.url=jdbc:oracle:thin:@mtdrdb291_medium?TNS_ADMIN=/Users/sadracaramburo/Desktop/VsCode/TaskO/MtdrSpring/backend/db_wallet
spring.datasource.username=ADMIN
spring.datasource.password=Taskopassword123
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#spring.jpa.database-platform=org.hibernate.dialect.Oracle12cDialect
#oracle.jdbc.fanEnabled=false
##this is not used when deployed in kubernetes. Just for local testing
spring.datasource.url=jdbc:oracle:thin:@tasko_medium?TNS_ADMIN=/Users/ID140/Documents/TaskO/MtdrSpring/backend/db_wallet
spring.datasource.url=jdbc:oracle:thin:@tasko_medium?TNS_ADMIN=/Users/sadracaramburo/Desktop/VsCode/TaskO/MtdrSpring/backend/db_wallet
spring.datasource.username=ADMIN
spring.datasource.password=Taskopassword123
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CircleDot } from "lucide-react"
import { AssignUserDialog } from "@/components/pages/home/AssignUserDialog"
import { ChangeStatusDialog } from "@/components/pages/home/ChangeStatusDialog"
import { useState, useEffect } from "react"
import { DeleteTaskDialog } from "./pages/home/DeleteTaskDialog"

// Tipo de estado para el backend
export type BackendStatus = "TODO" | "IN_PROGRESS" | "COMPLETED";
Expand Down Expand Up @@ -46,11 +47,14 @@ export interface TaskItemProps {
readonly date: string;
readonly image: string;
readonly assignee?: string;
readonly assigneeId?: string;
readonly sprintId?: string;
readonly onTaskUpdated?: () => void;
readonly estimatedHours?: number;
readonly realHours?: number;
readonly currentUserId?: string;
readonly storyPoints?: number;
readonly isManager?: boolean;
}

export function TaskItem({
Expand All @@ -62,10 +66,13 @@ export function TaskItem({
date,
image,
assignee,
assigneeId,
estimatedHours = 0,
realHours = 0,
currentUserId,
onTaskUpdated
onTaskUpdated,
storyPoints = 0,
isManager = false
}: TaskItemProps) {
const [localRealHours, setLocalRealHours] = useState<number>(realHours);

Expand Down Expand Up @@ -254,6 +261,12 @@ export function TaskItem({
}
};

const handleDeleteTask = () => {
if (onTaskUpdated) {
onTaskUpdated();
}
};

return (
<div className="border border-gray-100 rounded-lg p-3">
<div className="flex justify-between items-start">
Expand All @@ -278,6 +291,12 @@ export function TaskItem({
</div>
<div className="text-gray-400">Created: {date}</div>

{/* Story Points */}
<div className="flex items-center gap-2">
<span className="text-gray-500">Story Points: </span>
<span className="font-medium">{storyPoints}</span>
</div>

{/* Hours information */}
<div className="flex items-center gap-2">
<span className="text-gray-500">Est. Hours: </span>
Expand All @@ -287,14 +306,14 @@ export function TaskItem({
{/* Real hours display/input - only show input to assigned user */}
<div className="flex items-center gap-2">
<span className="text-gray-500">Real Hours: </span>
{assignee && currentUserId === assignee ? (
{assigneeId && currentUserId === assigneeId ? (
<input
type="number"
min="0"
step="0.5"
value={localRealHours}
onChange={(e) => {
const value = parseFloat(e.target.value);
const value = parseFloat(e.target.value) || 0;
setLocalRealHours(value);
handleRealHoursUpdate(id, value);
}}
Expand Down Expand Up @@ -325,6 +344,15 @@ export function TaskItem({
currentAssignee={assignee}
onAssign={handleAssignUser}
/>

{/* Delete button - only visible to managers */}
{isManager && (
<DeleteTaskDialog
taskId={id}
taskTitle={title}
onDelete={handleDeleteTask}
/>
)}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useState, useEffect, useMemo } from "react";
import { useProjects } from "../../context/ProjectContext";

export const useManager = (projectId: string | null) => {
const { userMetadata } = useProjects();

// Use the same logic as Dashboard.tsx - check userMetadata?.manager === true
const isManager = useMemo(() => userMetadata?.manager === true, [userMetadata?.manager]);

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

// Since we're using userMetadata, we don't need to make API calls
// The loading state should be based on whether userMetadata is available
useEffect(() => {
if (!projectId) {
setIsLoading(false);
setError("No project selected");
return;
}

// If userMetadata is not loaded yet, we're still loading
if (userMetadata === undefined) {
setIsLoading(true);
setError(null);
} else {
setIsLoading(false);
setError(null);
}
}, [projectId, userMetadata]);

return {
isManager,
isLoading,
error,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function AddTaskDialog({ onAddTask, sprintId, projectId }: AddTaskDialogP
endDate: date.toISOString(),
comments: description, // Si no tienes campo comments específico
storyPoints: parseInt(storyPoints),
priority: priority, // Add priority to backend request
estimatedHours: estimatedHours ? parseInt(estimatedHours) : 0,
realHours: realHours ? parseInt(realHours) : 0,
};
Expand Down Expand Up @@ -237,21 +238,28 @@ export function AddTaskDialog({ onAddTask, sprintId, projectId }: AddTaskDialogP
<div className="flex items-center space-x-6">
<div className="flex items-center space-x-2">
<div
className={`w-4 h-4 rounded-full ${priority === "Extreme" ? "bg-[#ff6767]" : "border border-gray-300"}`}
className={`w-4 h-4 rounded-full cursor-pointer ${priority === "Extreme" ? "bg-[#ff6767]" : "border border-gray-300"}`}
onClick={() => setPriority("Extreme")}
/>
<span className="text-sm">Extreme</span>
</div>
<div className="flex items-center space-x-2">
<div
className={`w-4 h-4 rounded-full ${priority === "Moderate" ? "bg-[#ffef3a]" : "border border-gray-300"}`}
className={`w-4 h-4 rounded-full cursor-pointer ${priority === "High" ? "bg-[#ff9f43]" : "border border-gray-300"}`}
onClick={() => setPriority("High")}
/>
<span className="text-sm">High</span>
</div>
<div className="flex items-center space-x-2">
<div
className={`w-4 h-4 rounded-full cursor-pointer ${priority === "Moderate" ? "bg-[#ffef3a]" : "border border-gray-300"}`}
onClick={() => setPriority("Moderate")}
/>
<span className="text-sm">Moderate</span>
</div>
<div className="flex items-center space-x-2">
<div
className={`w-4 h-4 rounded-full ${priority === "Low" ? "bg-[#4ed64c]" : "border border-gray-300"}`}
className={`w-4 h-4 rounded-full cursor-pointer ${priority === "Low" ? "bg-[#4ed64c]" : "border border-gray-300"}`}
onClick={() => setPriority("Low")}
/>
<span className="text-sm">Low</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
BackendStatus,
FrontendStatus,
getBackendStatus,
} from "@/components/ui/Task-item";
} from "@/components/Task-item";

interface ChangeStatusDialogProps {
readonly taskId: string;
Expand Down
Loading
Loading