diff --git a/app/(dashboard)/api-test/page.tsx b/app/(dashboard)/api-test/page.tsx new file mode 100644 index 0000000..4b5386a --- /dev/null +++ b/app/(dashboard)/api-test/page.tsx @@ -0,0 +1,203 @@ +'use client'; + +import { useProjects } from '@/lib/hooks/use-projects'; +import { useEpics } from '@/lib/hooks/use-epics'; +import { useStories } from '@/lib/hooks/use-stories'; +import { useTasks } from '@/lib/hooks/use-tasks'; +import { Card } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Badge } from '@/components/ui/badge'; + +export default function ApiTestPage() { + const { data: projects, isLoading: projectsLoading, error: projectsError } = useProjects(); + const { data: epics, isLoading: epicsLoading, error: epicsError } = useEpics(); + const { data: stories, isLoading: storiesLoading, error: storiesError } = useStories(); + const { data: tasks, isLoading: tasksLoading, error: tasksError } = useTasks(); + + return ( +
+
+

API Connection Test

+

+ This page tests the connection to ProjectManagement API endpoints +

+
+ + {/* Projects Section */} +
+ {projects && projects.length > 0 ? ( +
+ {projects.map((project) => ( + +

{project.name}

+

{project.key}

+ {project.description && ( +

{project.description}

+ )} +
+ ID: {project.id.substring(0, 8)}... +
+
+ ))} +
+ ) : ( + + )} +
+ + {/* Epics Section */} +
+ {epics && epics.length > 0 ? ( +
+ {epics.map((epic) => ( + +

{epic.title}

+ {epic.description && ( +

{epic.description}

+ )} +
+ {epic.status} + {epic.priority} + {epic.estimatedHours && ( + {epic.estimatedHours}h + )} +
+
+ ))} +
+ ) : ( + + )} +
+ + {/* Stories Section */} +
+ {stories && stories.length > 0 ? ( +
+ {stories.map((story) => ( + +

{story.title}

+ {story.description && ( +

{story.description}

+ )} +
+ {story.status} + {story.priority} + {story.estimatedHours && ( + {story.estimatedHours}h + )} +
+
+ ))} +
+ ) : ( + + )} +
+ + {/* Tasks Section */} +
+ {tasks && tasks.length > 0 ? ( +
+ {tasks.map((task) => ( + +

{task.title}

+ {task.description && ( +

{task.description}

+ )} +
+ {task.status} + {task.priority} + {task.estimatedHours && ( + {task.estimatedHours}h + )} +
+
+ ))} +
+ ) : ( + + )} +
+
+ ); +} + +// Helper Components +interface SectionProps { + title: string; + count?: number; + loading: boolean; + error: any; + children: React.ReactNode; +} + +function Section({ title, count, loading, error, children }: SectionProps) { + return ( +
+
+

+ {title} + {count !== undefined && ( + ({count}) + )} +

+ {loading && Loading...} + {error && Error} +
+ + {loading ? ( +
+ + + +
+ ) : error ? ( + +

Error Loading {title}

+

+ {error.message || 'Unknown error occurred'} +

+ {error.response?.status && ( +

+ Status Code: {error.response.status} +

+ )} +
+ ) : ( + children + )} +
+ ); +} + +function EmptyState({ message }: { message: string }) { + return ( + +

{message}

+

+ Try creating some data via the API or check your authentication +

+
+ ); +} diff --git a/lib/api/config.ts b/lib/api/config.ts index 8153115..7483c0e 100644 --- a/lib/api/config.ts +++ b/lib/api/config.ts @@ -1,4 +1,4 @@ -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5167'; export const API_ENDPOINTS = { // Auth @@ -17,7 +17,25 @@ export const API_ENDPOINTS = { ASSIGN_ROLE: (tenantId: string, userId: string) => `/api/tenants/${tenantId}/users/${userId}/role`, - // Projects (to be implemented) - PROJECTS: '/api/projects', - PROJECT: (id: string) => `/api/projects/${id}`, + // Projects + PROJECTS: '/api/v1/projects', + PROJECT: (id: string) => `/api/v1/projects/${id}`, + + // Epics + EPICS: '/api/v1/epics', + EPIC: (id: string) => `/api/v1/epics/${id}`, + EPIC_STATUS: (id: string) => `/api/v1/epics/${id}/status`, + EPIC_ASSIGN: (id: string) => `/api/v1/epics/${id}/assign`, + + // Stories + STORIES: '/api/v1/stories', + STORY: (id: string) => `/api/v1/stories/${id}`, + STORY_STATUS: (id: string) => `/api/v1/stories/${id}/status`, + STORY_ASSIGN: (id: string) => `/api/v1/stories/${id}/assign`, + + // Tasks + TASKS: '/api/v1/tasks', + TASK: (id: string) => `/api/v1/tasks/${id}`, + TASK_STATUS: (id: string) => `/api/v1/tasks/${id}/status`, + TASK_ASSIGN: (id: string) => `/api/v1/tasks/${id}/assign`, }; diff --git a/lib/api/pm.ts b/lib/api/pm.ts new file mode 100644 index 0000000..05de3ab --- /dev/null +++ b/lib/api/pm.ts @@ -0,0 +1,109 @@ +import { api } from './client'; +import type { + Epic, + CreateEpicDto, + UpdateEpicDto, + Story, + CreateStoryDto, + UpdateStoryDto, + Task, + CreateTaskDto, + UpdateTaskDto, + WorkItemStatus, +} from '@/types/project'; + +// ==================== Epics API ==================== +export const epicsApi = { + list: async (projectId?: string): Promise => { + const params = projectId ? { projectId } : undefined; + return api.get('/api/v1/epics', { params }); + }, + + get: async (id: string): Promise => { + return api.get(`/api/v1/epics/${id}`); + }, + + create: async (data: CreateEpicDto): Promise => { + return api.post('/api/v1/epics', data); + }, + + update: async (id: string, data: UpdateEpicDto): Promise => { + return api.put(`/api/v1/epics/${id}`, data); + }, + + delete: async (id: string): Promise => { + return api.delete(`/api/v1/epics/${id}`); + }, + + changeStatus: async (id: string, status: WorkItemStatus): Promise => { + return api.put(`/api/v1/epics/${id}/status`, { status }); + }, + + assign: async (id: string, assigneeId: string): Promise => { + return api.put(`/api/v1/epics/${id}/assign`, { assigneeId }); + }, +}; + +// ==================== Stories API ==================== +export const storiesApi = { + list: async (epicId?: string): Promise => { + const params = epicId ? { epicId } : undefined; + return api.get('/api/v1/stories', { params }); + }, + + get: async (id: string): Promise => { + return api.get(`/api/v1/stories/${id}`); + }, + + create: async (data: CreateStoryDto): Promise => { + return api.post('/api/v1/stories', data); + }, + + update: async (id: string, data: UpdateStoryDto): Promise => { + return api.put(`/api/v1/stories/${id}`, data); + }, + + delete: async (id: string): Promise => { + return api.delete(`/api/v1/stories/${id}`); + }, + + changeStatus: async (id: string, status: WorkItemStatus): Promise => { + return api.put(`/api/v1/stories/${id}/status`, { status }); + }, + + assign: async (id: string, assigneeId: string): Promise => { + return api.put(`/api/v1/stories/${id}/assign`, { assigneeId }); + }, +}; + +// ==================== Tasks API ==================== +export const tasksApi = { + list: async (storyId?: string): Promise => { + const params = storyId ? { storyId } : undefined; + return api.get('/api/v1/tasks', { params }); + }, + + get: async (id: string): Promise => { + return api.get(`/api/v1/tasks/${id}`); + }, + + create: async (data: CreateTaskDto): Promise => { + return api.post('/api/v1/tasks', data); + }, + + update: async (id: string, data: UpdateTaskDto): Promise => { + return api.put(`/api/v1/tasks/${id}`, data); + }, + + delete: async (id: string): Promise => { + return api.delete(`/api/v1/tasks/${id}`); + }, + + changeStatus: async (id: string, status: WorkItemStatus): Promise => { + return api.put(`/api/v1/tasks/${id}/status`, { status }); + }, + + assign: async (id: string, assigneeId: string): Promise => { + return api.put(`/api/v1/tasks/${id}/assign`, { assigneeId }); + }, +}; diff --git a/lib/hooks/use-epics.ts b/lib/hooks/use-epics.ts new file mode 100644 index 0000000..9c397b0 --- /dev/null +++ b/lib/hooks/use-epics.ts @@ -0,0 +1,167 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { epicsApi } from '@/lib/api/pm'; +import type { Epic, CreateEpicDto, UpdateEpicDto, WorkItemStatus } from '@/types/project'; +import { toast } from 'sonner'; + +// ==================== Query Hooks ==================== +export function useEpics(projectId?: string) { + return useQuery({ + queryKey: ['epics', projectId], + queryFn: async () => { + console.log('[useEpics] Fetching epics...', { projectId }); + try { + const result = await epicsApi.list(projectId); + console.log('[useEpics] Fetch successful:', result); + return result; + } catch (error) { + console.error('[useEpics] Fetch failed:', error); + throw error; + } + }, + staleTime: 5 * 60 * 1000, // 5 minutes + retry: 1, + }); +} + +export function useEpic(id: string) { + return useQuery({ + queryKey: ['epics', id], + queryFn: () => epicsApi.get(id), + enabled: !!id, + }); +} + +// ==================== Mutation Hooks ==================== +export function useCreateEpic() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateEpicDto) => epicsApi.create(data), + onSuccess: (newEpic) => { + // Invalidate all epic queries (including filtered by projectId) + queryClient.invalidateQueries({ queryKey: ['epics'] }); + + // Also invalidate project details if exists + queryClient.invalidateQueries({ queryKey: ['projects', newEpic.projectId] }); + + toast.success('Epic created successfully!'); + }, + onError: (error: any) => { + console.error('[useCreateEpic] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to create epic'); + }, + }); +} + +export function useUpdateEpic() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateEpicDto }) => + epicsApi.update(id, data), + onMutate: async ({ id, data }) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['epics', id] }); + + // Snapshot previous value + const previousEpic = queryClient.getQueryData(['epics', id]); + + // Optimistically update + queryClient.setQueryData(['epics', id], (old) => ({ + ...old!, + ...data, + })); + + return { previousEpic }; + }, + onError: (error: any, variables, context) => { + console.error('[useUpdateEpic] Error:', error); + + // Rollback + if (context?.previousEpic) { + queryClient.setQueryData(['epics', variables.id], context.previousEpic); + } + + toast.error(error.response?.data?.detail || 'Failed to update epic'); + }, + onSuccess: (updatedEpic) => { + toast.success('Epic updated successfully!'); + }, + onSettled: (_, __, variables) => { + queryClient.invalidateQueries({ queryKey: ['epics', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['epics'] }); + }, + }); +} + +export function useDeleteEpic() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => epicsApi.delete(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['epics'] }); + queryClient.removeQueries({ queryKey: ['epics', id] }); + toast.success('Epic deleted successfully!'); + }, + onError: (error: any) => { + console.error('[useDeleteEpic] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to delete epic'); + }, + }); +} + +export function useChangeEpicStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: WorkItemStatus }) => + epicsApi.changeStatus(id, status), + onMutate: async ({ id, status }) => { + await queryClient.cancelQueries({ queryKey: ['epics', id] }); + + const previousEpic = queryClient.getQueryData(['epics', id]); + + queryClient.setQueryData(['epics', id], (old) => ({ + ...old!, + status, + })); + + return { previousEpic }; + }, + onError: (error: any, variables, context) => { + console.error('[useChangeEpicStatus] Error:', error); + + if (context?.previousEpic) { + queryClient.setQueryData(['epics', variables.id], context.previousEpic); + } + + toast.error(error.response?.data?.detail || 'Failed to change epic status'); + }, + onSuccess: () => { + toast.success('Epic status changed successfully!'); + }, + onSettled: (_, __, variables) => { + queryClient.invalidateQueries({ queryKey: ['epics', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['epics'] }); + }, + }); +} + +export function useAssignEpic() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, assigneeId }: { id: string; assigneeId: string }) => + epicsApi.assign(id, assigneeId), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['epics', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['epics'] }); + toast.success('Epic assigned successfully!'); + }, + onError: (error: any) => { + console.error('[useAssignEpic] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to assign epic'); + }, + }); +} diff --git a/lib/hooks/use-stories.ts b/lib/hooks/use-stories.ts new file mode 100644 index 0000000..c278941 --- /dev/null +++ b/lib/hooks/use-stories.ts @@ -0,0 +1,163 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { storiesApi } from '@/lib/api/pm'; +import type { Story, CreateStoryDto, UpdateStoryDto, WorkItemStatus } from '@/types/project'; +import { toast } from 'sonner'; + +// ==================== Query Hooks ==================== +export function useStories(epicId?: string) { + return useQuery({ + queryKey: ['stories', epicId], + queryFn: async () => { + console.log('[useStories] Fetching stories...', { epicId }); + try { + const result = await storiesApi.list(epicId); + console.log('[useStories] Fetch successful:', result); + return result; + } catch (error) { + console.error('[useStories] Fetch failed:', error); + throw error; + } + }, + staleTime: 5 * 60 * 1000, // 5 minutes + retry: 1, + }); +} + +export function useStory(id: string) { + return useQuery({ + queryKey: ['stories', id], + queryFn: () => storiesApi.get(id), + enabled: !!id, + }); +} + +// ==================== Mutation Hooks ==================== +export function useCreateStory() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateStoryDto) => storiesApi.create(data), + onSuccess: (newStory) => { + // Invalidate all story queries + queryClient.invalidateQueries({ queryKey: ['stories'] }); + + // Also invalidate epic details + queryClient.invalidateQueries({ queryKey: ['epics', newStory.epicId] }); + + toast.success('Story created successfully!'); + }, + onError: (error: any) => { + console.error('[useCreateStory] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to create story'); + }, + }); +} + +export function useUpdateStory() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateStoryDto }) => + storiesApi.update(id, data), + onMutate: async ({ id, data }) => { + await queryClient.cancelQueries({ queryKey: ['stories', id] }); + + const previousStory = queryClient.getQueryData(['stories', id]); + + queryClient.setQueryData(['stories', id], (old) => ({ + ...old!, + ...data, + })); + + return { previousStory }; + }, + onError: (error: any, variables, context) => { + console.error('[useUpdateStory] Error:', error); + + if (context?.previousStory) { + queryClient.setQueryData(['stories', variables.id], context.previousStory); + } + + toast.error(error.response?.data?.detail || 'Failed to update story'); + }, + onSuccess: () => { + toast.success('Story updated successfully!'); + }, + onSettled: (_, __, variables) => { + queryClient.invalidateQueries({ queryKey: ['stories', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['stories'] }); + }, + }); +} + +export function useDeleteStory() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => storiesApi.delete(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['stories'] }); + queryClient.removeQueries({ queryKey: ['stories', id] }); + toast.success('Story deleted successfully!'); + }, + onError: (error: any) => { + console.error('[useDeleteStory] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to delete story'); + }, + }); +} + +export function useChangeStoryStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: WorkItemStatus }) => + storiesApi.changeStatus(id, status), + onMutate: async ({ id, status }) => { + await queryClient.cancelQueries({ queryKey: ['stories', id] }); + + const previousStory = queryClient.getQueryData(['stories', id]); + + queryClient.setQueryData(['stories', id], (old) => ({ + ...old!, + status, + })); + + return { previousStory }; + }, + onError: (error: any, variables, context) => { + console.error('[useChangeStoryStatus] Error:', error); + + if (context?.previousStory) { + queryClient.setQueryData(['stories', variables.id], context.previousStory); + } + + toast.error(error.response?.data?.detail || 'Failed to change story status'); + }, + onSuccess: () => { + toast.success('Story status changed successfully!'); + }, + onSettled: (_, __, variables) => { + queryClient.invalidateQueries({ queryKey: ['stories', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['stories'] }); + }, + }); +} + +export function useAssignStory() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, assigneeId }: { id: string; assigneeId: string }) => + storiesApi.assign(id, assigneeId), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['stories', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['stories'] }); + toast.success('Story assigned successfully!'); + }, + onError: (error: any) => { + console.error('[useAssignStory] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to assign story'); + }, + }); +} diff --git a/lib/hooks/use-tasks.ts b/lib/hooks/use-tasks.ts new file mode 100644 index 0000000..4c075b0 --- /dev/null +++ b/lib/hooks/use-tasks.ts @@ -0,0 +1,163 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { tasksApi } from '@/lib/api/pm'; +import type { Task, CreateTaskDto, UpdateTaskDto, WorkItemStatus } from '@/types/project'; +import { toast } from 'sonner'; + +// ==================== Query Hooks ==================== +export function useTasks(storyId?: string) { + return useQuery({ + queryKey: ['tasks', storyId], + queryFn: async () => { + console.log('[useTasks] Fetching tasks...', { storyId }); + try { + const result = await tasksApi.list(storyId); + console.log('[useTasks] Fetch successful:', result); + return result; + } catch (error) { + console.error('[useTasks] Fetch failed:', error); + throw error; + } + }, + staleTime: 5 * 60 * 1000, // 5 minutes + retry: 1, + }); +} + +export function useTask(id: string) { + return useQuery({ + queryKey: ['tasks', id], + queryFn: () => tasksApi.get(id), + enabled: !!id, + }); +} + +// ==================== Mutation Hooks ==================== +export function useCreateTask() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (data: CreateTaskDto) => tasksApi.create(data), + onSuccess: (newTask) => { + // Invalidate all task queries + queryClient.invalidateQueries({ queryKey: ['tasks'] }); + + // Also invalidate story details + queryClient.invalidateQueries({ queryKey: ['stories', newTask.storyId] }); + + toast.success('Task created successfully!'); + }, + onError: (error: any) => { + console.error('[useCreateTask] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to create task'); + }, + }); +} + +export function useUpdateTask() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, data }: { id: string; data: UpdateTaskDto }) => + tasksApi.update(id, data), + onMutate: async ({ id, data }) => { + await queryClient.cancelQueries({ queryKey: ['tasks', id] }); + + const previousTask = queryClient.getQueryData(['tasks', id]); + + queryClient.setQueryData(['tasks', id], (old) => ({ + ...old!, + ...data, + })); + + return { previousTask }; + }, + onError: (error: any, variables, context) => { + console.error('[useUpdateTask] Error:', error); + + if (context?.previousTask) { + queryClient.setQueryData(['tasks', variables.id], context.previousTask); + } + + toast.error(error.response?.data?.detail || 'Failed to update task'); + }, + onSuccess: () => { + toast.success('Task updated successfully!'); + }, + onSettled: (_, __, variables) => { + queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['tasks'] }); + }, + }); +} + +export function useDeleteTask() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => tasksApi.delete(id), + onSuccess: (_, id) => { + queryClient.invalidateQueries({ queryKey: ['tasks'] }); + queryClient.removeQueries({ queryKey: ['tasks', id] }); + toast.success('Task deleted successfully!'); + }, + onError: (error: any) => { + console.error('[useDeleteTask] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to delete task'); + }, + }); +} + +export function useChangeTaskStatus() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, status }: { id: string; status: WorkItemStatus }) => + tasksApi.changeStatus(id, status), + onMutate: async ({ id, status }) => { + await queryClient.cancelQueries({ queryKey: ['tasks', id] }); + + const previousTask = queryClient.getQueryData(['tasks', id]); + + queryClient.setQueryData(['tasks', id], (old) => ({ + ...old!, + status, + })); + + return { previousTask }; + }, + onError: (error: any, variables, context) => { + console.error('[useChangeTaskStatus] Error:', error); + + if (context?.previousTask) { + queryClient.setQueryData(['tasks', variables.id], context.previousTask); + } + + toast.error(error.response?.data?.detail || 'Failed to change task status'); + }, + onSuccess: () => { + toast.success('Task status changed successfully!'); + }, + onSettled: (_, __, variables) => { + queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['tasks'] }); + }, + }); +} + +export function useAssignTask() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, assigneeId }: { id: string; assigneeId: string }) => + tasksApi.assign(id, assigneeId), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ['tasks', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['tasks'] }); + toast.success('Task assigned successfully!'); + }, + onError: (error: any) => { + console.error('[useAssignTask] Error:', error); + toast.error(error.response?.data?.detail || 'Failed to assign task'); + }, + }); +} diff --git a/types/project.ts b/types/project.ts index 1095bb8..cba37ce 100644 --- a/types/project.ts +++ b/types/project.ts @@ -1,90 +1,129 @@ -export type ProjectStatus = 'Active' | 'Archived' | 'OnHold'; +// ==================== Common Types ==================== +export type WorkItemStatus = 'Backlog' | 'Todo' | 'InProgress' | 'Done'; +export type WorkItemPriority = 'Low' | 'Medium' | 'High' | 'Critical'; +// ==================== Project ==================== export interface Project { id: string; name: string; - description: string; key: string; - status: ProjectStatus; - ownerId: string; + description?: string; + tenantId: string; createdAt: string; - updatedAt?: string; + updatedAt: string; } export interface CreateProjectDto { name: string; - description: string; key: string; - ownerId?: string; // Optional in form, will be set automatically + description?: string; } export interface UpdateProjectDto { - name?: string; + name: string; + key: string; description?: string; - status?: ProjectStatus; } +// ==================== Epic ==================== export interface Epic { id: string; - name: string; - description: string; + title: string; + description?: string; projectId: string; - status: TaskStatus; - priority: TaskPriority; + status: WorkItemStatus; + priority: WorkItemPriority; + estimatedHours?: number; + actualHours?: number; + assigneeId?: string; + tenantId: string; createdAt: string; - createdBy: string; + updatedAt: string; } -export type TaskStatus = 'ToDo' | 'InProgress' | 'InReview' | 'Done' | 'Blocked'; -export type TaskPriority = 'Low' | 'Medium' | 'High' | 'Urgent'; +export interface CreateEpicDto { + projectId: string; + title: string; + description?: string; + priority: WorkItemPriority; + estimatedHours?: number; +} +export interface UpdateEpicDto { + title?: string; + description?: string; + priority?: WorkItemPriority; + estimatedHours?: number; + actualHours?: number; +} + +// ==================== Story ==================== export interface Story { id: string; title: string; - description: string; + description?: string; epicId: string; - status: TaskStatus; - priority: TaskPriority; + projectId: string; + status: WorkItemStatus; + priority: WorkItemPriority; estimatedHours?: number; actualHours?: number; assigneeId?: string; - createdBy: string; + tenantId: string; createdAt: string; - updatedAt?: string; + updatedAt: string; } +export interface CreateStoryDto { + epicId: string; + title: string; + description?: string; + priority: WorkItemPriority; + estimatedHours?: number; +} + +export interface UpdateStoryDto { + title?: string; + description?: string; + priority?: WorkItemPriority; + estimatedHours?: number; + actualHours?: number; +} + +// ==================== Task ==================== export interface Task { id: string; title: string; - description: string; + description?: string; storyId: string; - status: TaskStatus; - priority: TaskPriority; + projectId: string; + status: WorkItemStatus; + priority: WorkItemPriority; estimatedHours?: number; actualHours?: number; assigneeId?: string; - customFields?: Record; - createdBy: string; + tenantId: string; createdAt: string; - updatedAt?: string; + updatedAt: string; } export interface CreateTaskDto { - title: string; - description: string; storyId: string; - priority: TaskPriority; + title: string; + description?: string; + priority: WorkItemPriority; estimatedHours?: number; - assigneeId?: string; } export interface UpdateTaskDto { title?: string; description?: string; - status?: TaskStatus; - priority?: TaskPriority; + priority?: WorkItemPriority; estimatedHours?: number; actualHours?: number; - assigneeId?: string; - customFields?: Record; } + +// ==================== Legacy Types (for backward compatibility) ==================== +// Keep old type names as aliases for gradual migration +export type TaskStatus = WorkItemStatus; +export type TaskPriority = WorkItemPriority;