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;