diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/milestone/MilestoneResponse.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/milestone/MilestoneResponse.java index dd0b780a..4215bf8d 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/milestone/MilestoneResponse.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/milestone/MilestoneResponse.java @@ -6,11 +6,8 @@ public record MilestoneResponse ( UUID id, - String name, - String description, - LocalDate date ) { public static MilestoneResponse from(ProjectMilestone milestone) { diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/AssignmentsByEmployeeResponse.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/AssignmentsByEmployeeResponse.java new file mode 100644 index 00000000..180facc0 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/AssignmentsByEmployeeResponse.java @@ -0,0 +1,27 @@ +package pl.edu.agh.project_manager.controller.dto.project; + +import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment; +import pl.edu.agh.project_manager.domain.entity.user.User; + +import java.util.List; +import java.util.UUID; + +public record AssignmentsByEmployeeResponse( + UUID userId, + String name, + String surname, + String email, + List assignments +) { + public static AssignmentsByEmployeeResponse from(User user, List userAssignments) { + return new AssignmentsByEmployeeResponse( + user.getId(), + user.getName(), + user.getSurname(), + user.getEmail(), + userAssignments.stream() + .map(ProjectAssignmentResponse::from) + .toList() + ); + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectAssignmentResponse.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectAssignmentResponse.java new file mode 100644 index 00000000..2a9723e4 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectAssignmentResponse.java @@ -0,0 +1,26 @@ +package pl.edu.agh.project_manager.controller.dto.project; + +import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment; + +import java.time.LocalDate; +import java.util.UUID; + +public record ProjectAssignmentResponse( + UUID id, + LocalDate startDate, + LocalDate endDate, + AssignmentStatusResponse status, + String roleName, + int utilizationPercentage +) { + public static ProjectAssignmentResponse from(ProjectAssignment assignment) { + return new ProjectAssignmentResponse( + assignment.getId(), + assignment.getStartDate(), + assignment.getEndDate(), + AssignmentStatusResponse.from(assignment.getStatus()), + assignment.getRoleName(), + assignment.getUtilizationPercentage() + ); + } +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectTimelineResponse.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectTimelineResponse.java new file mode 100644 index 00000000..5a3358e3 --- /dev/null +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/dto/project/ProjectTimelineResponse.java @@ -0,0 +1,11 @@ +package pl.edu.agh.project_manager.controller.dto.project; + +import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneResponse; + +import java.util.List; + +public record ProjectTimelineResponse( + List milestones, + List assignments +) { +} diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectController.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectController.java index 09cd69bf..1198774e 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectController.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/controller/project/ProjectController.java @@ -70,6 +70,15 @@ public ResponseEntity getProjectMembers( ProjectMembersResponse project = projectService.getProjectMembers(projectId); return ResponseEntity.ok(project); } + + @GetMapping("/{projectId}/timeline") + @PreAuthorize("hasAnyRole('ADMINISTRATOR', 'AUTHORITY') or @projectAccess.canAccessProject(#projectId, authentication.principal)") + public ResponseEntity getTimelineData( + @PathVariable UUID projectId + ) { + ProjectTimelineResponse response = projectService.getTimelineData(projectId); + return ResponseEntity.ok(response); + } } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectAssignmentRepository.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectAssignmentRepository.java index 675085cd..74eba985 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectAssignmentRepository.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectAssignmentRepository.java @@ -21,4 +21,11 @@ public interface ProjectAssignmentRepository extends JpaRepository findActiveAndPendingByProjectIdOrderByCreatedAtAsc(@Param("projectId") UUID projectId); } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectRepository.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectRepository.java index 0ad30300..c94d8180 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectRepository.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/repository/project/ProjectRepository.java @@ -45,4 +45,7 @@ public interface ProjectRepository extends JpaRepository { @EntityGraph(attributePaths = {"projectManager", "projectGroup"}) List findAll(Specification spec); + + @EntityGraph(attributePaths = {"milestones"}) + Optional findWithMilestonesById(UUID id); } diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java index 1ae6a2e8..2091eeeb 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectAssignmentService.java @@ -5,6 +5,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import pl.edu.agh.project_manager.controller.dto.project.AssignmentResponse; +import pl.edu.agh.project_manager.controller.dto.project.AssignmentsByEmployeeResponse; +import pl.edu.agh.project_manager.controller.dto.project.ProjectAssignmentResponse; import pl.edu.agh.project_manager.domain.entity.project.Project; import pl.edu.agh.project_manager.domain.entity.user.User; import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment; @@ -15,12 +17,15 @@ import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; import pl.edu.agh.project_manager.repository.project.ProjectAssignmentRepository; +import pl.edu.agh.project_manager.repository.project.ProjectRepository; import pl.edu.agh.project_manager.service.user.UserService; import pl.edu.agh.project_manager.service.command.project.AssignmentCommand; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor diff --git a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectService.java b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectService.java index 1758cc2c..f5168209 100644 --- a/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectService.java +++ b/backend/project-manager/src/main/java/pl/edu/agh/project_manager/service/project/ProjectService.java @@ -1,31 +1,31 @@ package pl.edu.agh.project_manager.service.project; -import jakarta.persistence.criteria.Join; -import jakarta.persistence.criteria.Predicate; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; -import pl.edu.agh.project_manager.controller.dto.project.ProjectMembersResponse; -import pl.edu.agh.project_manager.controller.dto.project.ProjectResponse; +import org.springframework.transaction.annotation.Transactional; +import pl.edu.agh.project_manager.controller.dto.milestone.MilestoneResponse; +import pl.edu.agh.project_manager.controller.dto.project.*; import pl.edu.agh.project_manager.domain.entity.project.Project; +import pl.edu.agh.project_manager.domain.entity.project.ProjectAssignment; import pl.edu.agh.project_manager.domain.entity.project.ProjectRisk; import pl.edu.agh.project_manager.domain.entity.projectgroup.ProjectGroup; import pl.edu.agh.project_manager.domain.entity.user.User; import pl.edu.agh.project_manager.domain.entity.project.ProjectMilestone; -import pl.edu.agh.project_manager.domain.enums.GroupType; import pl.edu.agh.project_manager.domain.exception.ApiErrorCode; import pl.edu.agh.project_manager.domain.exception.ApplicationException; +import pl.edu.agh.project_manager.repository.project.ProjectAssignmentRepository; import pl.edu.agh.project_manager.repository.project.ProjectRepository; -import pl.edu.agh.project_manager.security.UserPrincipal; import pl.edu.agh.project_manager.service.command.project.MilestoneCommand; import pl.edu.agh.project_manager.service.command.project.ProjectCreationCommand; import pl.edu.agh.project_manager.service.command.project.RiskCommand; -import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.UUID; -import pl.edu.agh.project_manager.domain.enums.UserRole; +import java.util.stream.Collectors; + import pl.edu.agh.project_manager.service.command.project.SearchProjectCommand; import pl.edu.agh.project_manager.service.projectgroup.ProjectGroupsService; import pl.edu.agh.project_manager.service.user.UserService; @@ -37,6 +37,7 @@ public class ProjectService { private final UserService userService; private final ProjectGroupsService projectGroupService; private final ProjectRepository projectRepository; + private final ProjectAssignmentRepository assignmentRepository; @Transactional @@ -83,6 +84,44 @@ public ProjectMembersResponse getProjectMembers(UUID projectId) { return ProjectMembersResponse.from(project); } + @Transactional(readOnly = true) + public ProjectTimelineResponse getTimelineData(UUID projectId) { + Project project = projectRepository.findWithMilestonesById(projectId) + .orElseThrow(() -> new ApplicationException( + ApiErrorCode.PROJECT_NOT_FOUND, + "Cannot find provided project - " + projectId + )); + + List assignments = assignmentRepository.findActiveAndPendingByProjectIdOrderByCreatedAtAsc(projectId); + + return new ProjectTimelineResponse( + project.getMilestones().stream().map(MilestoneResponse::from).toList(), + groupAssignmentsByEmployee(assignments) + ); + } + + private List groupAssignmentsByEmployee(List assignments) { + Map> assignmentsByUser = assignments.stream() + .collect(Collectors.groupingBy(ProjectAssignment::getUser, LinkedHashMap::new, Collectors.toList())); + + return assignmentsByUser.entrySet().stream() + .map(entry -> { + User employee = entry.getKey(); + List userAssignments = entry.getValue(); + + return new AssignmentsByEmployeeResponse( + employee.getId(), + employee.getName(), + employee.getSurname(), + employee.getEmail(), + userAssignments.stream() + .map(ProjectAssignmentResponse::from) + .toList() + ); + }) + .toList(); + } + private Project buildProject(ProjectCreationCommand command, User projectManager) { return Project.builder() .title(command.title()) diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index db5d9d57..0cdadf57 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -27,7 +27,8 @@ export const ENDPOINTS = { BASE: `/projects/${id}`, RISKS: `/projects/${id}/risks`, MEMBERS: `/projects/${id}/members`, - ASSIGNMENTS: `/projects/${id}/assignments` + ASSIGNMENTS: `/projects/${id}/assignments`, + TIMELINE: `/projects/${id}/timeline` }), }, PROJECT_GROUP: { diff --git a/frontend/src/components/ui/tooltip.tsx b/frontend/src/components/ui/tooltip.tsx index 7413f6e3..e83a9ccd 100644 --- a/frontend/src/components/ui/tooltip.tsx +++ b/frontend/src/components/ui/tooltip.tsx @@ -30,7 +30,7 @@ function TooltipTrigger({ function TooltipContent({ className, - sideOffset = 0, + sideOffset = 8, children, ...props }: React.ComponentProps) { @@ -46,7 +46,7 @@ function TooltipContent({ {...props} > {children} - + {/* */} ) diff --git a/frontend/src/features/project/components/CreateAssignmentModal.tsx b/frontend/src/features/project/components/CreateAssignmentModal.tsx index 74598aad..aa698791 100644 --- a/frontend/src/features/project/components/CreateAssignmentModal.tsx +++ b/frontend/src/features/project/components/CreateAssignmentModal.tsx @@ -84,7 +84,12 @@ export const CreateAssignmentModal = ({ project }: CreateAssignmentModalProps) =
open ? setIsOpenModal(true) : closeModal()}> - + diff --git a/frontend/src/features/project/components/ProjectTimeline.tsx b/frontend/src/features/project/components/ProjectTimeline.tsx index 15a4fbcc..0ffdfce4 100644 --- a/frontend/src/features/project/components/ProjectTimeline.tsx +++ b/frontend/src/features/project/components/ProjectTimeline.tsx @@ -1,31 +1,18 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { CalendarDays, MapPin } from "lucide-react"; - -// interface ProjectTimelineProps { -// projectId: string; -// } - -// Dane tymczasowe -const MOCK_DATA = { - projectStart: new Date("2026-04-01"), - projectEnd: new Date("2026-07-31"), - milestones: [ - { id: "m1", name: "Faza Testów", description: "Zakończenie pierwszej iteracji programistycznej i start QA", date: new Date("2026-05-15") }, - { id: "m2", name: "Akceptacja UAT", description: "Odbiór modułów przez klienta biznesowego", date: new Date("2026-06-20") }, - { id: "m3", name: "Wdrożenie PROD", description: "Zrzut na środowisko produkcyjne", date: new Date("2026-07-28") }, - ], - assignments: [ - { id: "a1", userName: "Jan Kowalski", role: "Programista Java", startDate: new Date("2026-04-10"), endDate: new Date("2026-05-20"), utilization: 100 }, - { id: "a2", userName: "Jan Kowalski", role: "Wsparcie QA", startDate: new Date("2026-06-01"), endDate: new Date("2026-07-15"), utilization: 20 }, - { id: "a3", userName: "Anna Nowak", role: "UX Designer", startDate: new Date("2026-04-01"), endDate: new Date("2026-04-30"), utilization: 50 }, - ] -}; +import { CalendarDays, Check, HelpCircle, MapPin } from "lucide-react"; +import type { ProjectDetailsResponse } from "../project.types"; +import { useTimeline } from "../project.hooks"; +import { EmployeeAssignmentStatus } from "@/features/employee-assignments/employee-assignments.types"; +import { useNavigate } from "react-router-dom"; +import { PATHS } from "@/routes/paths"; +import type { ReactNode } from "react"; const calculatePositionAndWidth = (start: Date, end: Date, projectStart: Date, projectEnd: Date) => { - const totalProjectDuration = projectEnd.getTime() - projectStart.getTime(); - const taskDuration = end.getTime() - start.getTime(); + const DAY_MS = 86400000; // one day in milliseconds — include end day as inclusive + const totalProjectDuration = projectEnd.getTime() - projectStart.getTime() + DAY_MS; + const taskDuration = end.getTime() - start.getTime() + DAY_MS; const timeOffset = start.getTime() - projectStart.getTime(); const widthPercent = Math.min(Math.max((taskDuration / totalProjectDuration) * 100, 0), 100); @@ -34,102 +21,161 @@ const calculatePositionAndWidth = (start: Date, end: Date, projectStart: Date, p return { left: `${leftPercent}%`, width: `${widthPercent}%` }; }; -export const ProjectTimeline = (/*{ projectId }: ProjectTimelineProps*/) => { - const { projectStart, projectEnd, milestones, assignments } = MOCK_DATA; +interface ProjectTimelineProps { + project: ProjectDetailsResponse; + children?: ReactNode; +} + +export const ProjectTimeline = ({ project, children }: ProjectTimelineProps) => { + const { data: timelineData } = useTimeline(project.id); + const navigate = useNavigate(); - const assignmentsByUser = assignments.reduce((acc, curr) => { - if (!acc[curr.userName]) acc[curr.userName] = []; - acc[curr.userName].push(curr); - return acc; - }, {} as Record); + const projectStart = new Date(project.startDate); + const projectEnd = new Date(project.endDate); + + const milestones = timelineData?.milestones ?? []; + const assignmentsByUser = timelineData?.assignments ?? []; + const markerGuideHeight = Math.max(assignmentsByUser.length * 56, 80); + + const getAssignmentStatusClasses = (status: EmployeeAssignmentStatus) => status === EmployeeAssignmentStatus.ACCEPTED + ? "bg-emerald-500/95 hover:bg-emerald-600 border-emerald-800/35 ring-1 ring-inset ring-white/20 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.25)]" + : status === EmployeeAssignmentStatus.PENDING + ? "bg-amber-400/95 hover:bg-amber-500 border-amber-700/30 ring-1 ring-inset ring-white/15 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.12)]" + : "bg-indigo-500/95 hover:bg-indigo-600 border-indigo-800/35 ring-1 ring-inset ring-white/20 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.25)]"; + + const getAssignmentStatusIcon = (status: EmployeeAssignmentStatus) => status === EmployeeAssignmentStatus.ACCEPTED + ? + : status === EmployeeAssignmentStatus.PENDING + ? + : null; return ( - + - Harmonogram Projektu (Timeline) + Harmonogram projektu + {children} - + -
-
- {projectStart.toLocaleDateString()} - {projectEnd.toLocaleDateString()} -
-
- -
- -
- {milestones.map((milestone) => { - const position = calculatePositionAndWidth(milestone.date, milestone.date, projectStart, projectEnd); - return ( - - -
- -
{/* Pionowa linia w dół */} -
- - -

{milestone.name}

-

{milestone.date.toLocaleDateString()}

-

{milestone.description}

-
- - ); - })} -
+
+
- {Object.entries(assignmentsByUser).map(([userName, userAssignments], index) => { - const initials = userName.split(' ').map(n => n[0]).join('').toUpperCase(); - - return ( -
-
- - - {initials} - - -
- -
- {userAssignments.map((assignment) => { - const position = calculatePositionAndWidth(assignment.startDate, assignment.endDate, projectStart, projectEnd); - - return ( - +
+
+
+ {projectStart.toLocaleDateString()} + {projectEnd.toLocaleDateString()} + {milestones.map((milestone, index) => { + const milestoneDate = new Date(milestone.date); + const position = calculatePositionAndWidth(milestoneDate, milestoneDate, projectStart, projectEnd); + return ( +
- {/* Jeśli pasek jest dostatecznie szeroki, pokazujemy procent w środku */} - {parseFloat(position.width) > 10 && ( - {assignment.utilization}% - )} + +
- -

{assignment.role}

-

- {assignment.startDate.toLocaleDateString()} - {assignment.endDate.toLocaleDateString()} -

-

Zaangażowanie: {assignment.utilization}%

+ +
{milestone.name}
+
{milestoneDate.toLocaleDateString()}
- ); - })} + ); + })} +
+
+ + {assignmentsByUser.length === 0 ? ( +
+
+ + +
+ + ? + +
+
+ +

Brak przypisań

+
+
+
+ +
+ Brak przypisań do projektu
- ); - })} + ) : assignmentsByUser.map((employee) => { + const userName = `${employee.name} ${employee.surname}`; + const initials = userName.split(' ').map(n => n[0]).join('').toUpperCase(); + + return ( +
+
+ + + + + +

{userName}

+
+
+
+ +
+ {employee.assignments.map((assignment) => { + const assignmentStartDate = new Date(assignment.startDate); + const assignmentEndDate = new Date(assignment.endDate); + const position = calculatePositionAndWidth(assignmentStartDate, assignmentEndDate, projectStart, projectEnd); + return ( + + +
+ {getAssignmentStatusIcon(assignment.status)} + {parseFloat(position.width) > 12 && ( + {assignment.utilizationPercentage}% + )} +
+
+ +
{assignment.roleName}
+
+ {assignmentStartDate.toLocaleDateString()} - {assignmentEndDate.toLocaleDateString()} +
+
+ Zaangażowanie: {assignment.utilizationPercentage}% +
+
+
+ ); + })} +
+
+ ); + })} +
diff --git a/frontend/src/features/project/project.hooks.ts b/frontend/src/features/project/project.hooks.ts index 335c2875..fcefa467 100644 --- a/frontend/src/features/project/project.hooks.ts +++ b/frontend/src/features/project/project.hooks.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { projectService } from "@/features/project/project.service.ts"; import { toast } from "sonner"; -import { PROJECT_KEYS, ROLES_KEYS } from "@/features/project/project.keys.ts"; +import { PROJECT_KEYS } from "@/features/project/project.keys.ts"; import type { SearchProjectsRequest } from "@/features/project/project.types.ts"; import { hasProjectSearchFilters } from "@/features/project/project.utils.ts"; @@ -23,13 +23,13 @@ export const useProjectDetails = (id: string) => { export const useCreateProject = () => { const queryClient = useQueryClient(); - return useMutation({ - mutationFn: projectService.create, - onSuccess: () => { - toast.success("Dodano projekt."); - queryClient.invalidateQueries({ queryKey: PROJECT_KEYS.all }); - }, - }); + return useMutation({ + mutationFn: projectService.createProject, + onSuccess: () => { + toast.success('Dodano projekt.'); + queryClient.invalidateQueries({ queryKey: PROJECT_KEYS.all }); + }, + }); }; export const useProjectRisks = (id: string) => { @@ -60,13 +60,23 @@ export const useSearchProjects = (request: SearchProjectsRequest) => { export const useCreateEmployeeAssignment = (projectId: string) => { const queryClient = useQueryClient(); - const assignmentMutation = useMutation({ - mutationFn: projectService.createEmployeeAssignment, - onSuccess: () => queryClient.invalidateQueries({ queryKey: ROLES_KEYS.status(projectId) }), - }); + const assignmentMutation = useMutation({ + mutationFn: projectService.createEmployeeAssignment, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: PROJECT_KEYS.timeline(projectId) }); + } + }); return { createAssignment: assignmentMutation.mutate, isCreatingAssignment: assignmentMutation.isPending, }; }; + +export const useTimeline = (projectId: string) => { + return useQuery({ + queryKey: PROJECT_KEYS.timeline(projectId), + queryFn: () => projectService.getProjectTimeline(projectId), + enabled: !!projectId + }); +}; \ No newline at end of file diff --git a/frontend/src/features/project/project.keys.ts b/frontend/src/features/project/project.keys.ts index 57895a48..dcba6c44 100644 --- a/frontend/src/features/project/project.keys.ts +++ b/frontend/src/features/project/project.keys.ts @@ -6,12 +6,8 @@ export const PROJECT_KEYS = { details: () => [...PROJECT_KEYS.all, 'detail'] as const, detail: (id: string) => [...PROJECT_KEYS.details(), id] as const, search: (request: SearchProjectsRequest) => [...PROJECT_KEYS.all, 'search', request] as const, + timeline: (projectId: string) => [...PROJECT_KEYS.detail(projectId), 'timeline'] as const, risks: (projectId: string) => [...PROJECT_KEYS.detail(projectId), 'risks'] as const, members: (projectId: string) => [...PROJECT_KEYS.detail(projectId), 'members'] as const, -}; - -export const ROLES_KEYS = { - all: ['roles'] as const, - status: (projectId: string) => [...PROJECT_KEYS.detail(projectId), 'roles-status'] as const, }; \ No newline at end of file diff --git a/frontend/src/features/project/project.service.ts b/frontend/src/features/project/project.service.ts index 2f8e486e..79c7430f 100644 --- a/frontend/src/features/project/project.service.ts +++ b/frontend/src/features/project/project.service.ts @@ -6,6 +6,7 @@ import type { CreateEmployeeAssignmentRequest, RiskResponse, ProjectMembersResponse, + ProjectTimelineResponse, SearchProjectsRequest, } from "./project.types"; import { buildProjectSearchParams } from "./project.utils"; @@ -16,10 +17,10 @@ export const projectService = { return data; }, - create: async (projectData: ProjectCreationRequest): Promise => { - const response = await api.post(ENDPOINTS.PROJECT.BASE, projectData); - return response.data; - }, + createProject: async (projectData: ProjectCreationRequest): Promise => { + const response = await api.post(ENDPOINTS.PROJECT.BASE, projectData); + return response.data; + }, getDetails: async (id: string): Promise => { const response = await api.get(ENDPOINTS.PROJECT.DETAILS(id).BASE); @@ -46,7 +47,11 @@ export const projectService = { const response = await api.get(ENDPOINTS.PROJECT.BASE, { params, }); - return response.data; }, + + getProjectTimeline: async (projectId: string) => { + const response = await api.get(ENDPOINTS.PROJECT.DETAILS(projectId).TIMELINE); + return response.data; + } }; diff --git a/frontend/src/features/project/project.types.ts b/frontend/src/features/project/project.types.ts index 45d9a689..86535668 100644 --- a/frontend/src/features/project/project.types.ts +++ b/frontend/src/features/project/project.types.ts @@ -1,4 +1,5 @@ import type { SimpleUserResponse, UserResponse } from "@/features/user-management"; +import type { EmployeeAssignmentStatus } from "@/features/employee-assignments/employee-assignments.types"; import type { SingleGroupResponse } from "../project_group/project_group.types"; export interface ProjectCreationRequest { @@ -55,6 +56,28 @@ export interface ProjectDetailsResponse { group: SingleGroupResponse; } +export interface ProjectTimelineResponse { + milestones: Milestone[]; + assignments: EmployeeWithAssignmentsResponse[]; +} + +export interface EmployeeWithAssignmentsResponse { + userId: string; + name: string; + surname: string; + email: string; + assignments: ProjectAssignmentResponse[]; +} + +export interface ProjectAssignmentResponse { + id: string; + roleName: string; + startDate: string; + endDate: string; + utilizationPercentage: number; + status: EmployeeAssignmentStatus; +} + export interface CreateEmployeeAssignmentRequest { projectId: string; userId: string; diff --git a/frontend/src/pages/ProjectDetailsPage.tsx b/frontend/src/pages/ProjectDetailsPage.tsx index d08ece01..b84af2c2 100644 --- a/frontend/src/pages/ProjectDetailsPage.tsx +++ b/frontend/src/pages/ProjectDetailsPage.tsx @@ -19,14 +19,14 @@ export const ProjectDetailsPage = () => { } return ( -
+
-
+
- - - + + +