diff --git a/app/(dashboard)/stories/[id]/error.tsx b/app/(dashboard)/stories/[id]/error.tsx
new file mode 100644
index 0000000..198453d
--- /dev/null
+++ b/app/(dashboard)/stories/[id]/error.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import { useEffect } from 'react';
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { AlertCircle } from 'lucide-react';
+
+export default function StoryDetailError({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ // Log the error to an error reporting service
+ console.error('Story detail page error:', error);
+ }, [error]);
+
+ return (
+
+
+
+
+
+
Error Loading Story
+
+
+ {error.message || 'An unexpected error occurred while loading the story.'}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(dashboard)/stories/[id]/loading.tsx b/app/(dashboard)/stories/[id]/loading.tsx
new file mode 100644
index 0000000..f2e6f5e
--- /dev/null
+++ b/app/(dashboard)/stories/[id]/loading.tsx
@@ -0,0 +1,66 @@
+import { Skeleton } from '@/components/ui/skeleton';
+import { Card, CardContent, CardHeader } from '@/components/ui/card';
+
+export default function StoryDetailLoading() {
+ return (
+
+ {/* Breadcrumb Skeleton */}
+
+
+ {/* Header Skeleton */}
+
+
+ {/* Two-column layout Skeleton */}
+
+ {/* Main Content */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Sidebar */}
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/app/(dashboard)/stories/[id]/page.tsx b/app/(dashboard)/stories/[id]/page.tsx
new file mode 100644
index 0000000..54c1f77
--- /dev/null
+++ b/app/(dashboard)/stories/[id]/page.tsx
@@ -0,0 +1,478 @@
+'use client';
+
+import { use, useState } from 'react';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import {
+ ArrowLeft,
+ Edit,
+ Trash2,
+ Loader2,
+ Clock,
+ Calendar,
+ User,
+ Layers,
+} from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Badge } from '@/components/ui/badge';
+import { Skeleton } from '@/components/ui/skeleton';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@/components/ui/alert-dialog';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+import { useStory, useUpdateStory, useDeleteStory, useChangeStoryStatus } from '@/lib/hooks/use-stories';
+import { useEpic } from '@/lib/hooks/use-epics';
+import { useProject } from '@/lib/hooks/use-projects';
+import { StoryForm } from '@/components/projects/story-form';
+import { formatDistanceToNow } from 'date-fns';
+import { toast } from 'sonner';
+import type { WorkItemStatus, WorkItemPriority } from '@/types/project';
+
+interface StoryDetailPageProps {
+ params: Promise<{ id: string }>;
+}
+
+export default function StoryDetailPage({ params }: StoryDetailPageProps) {
+ const { id: storyId } = use(params);
+ const router = useRouter();
+ const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+
+ const { data: story, isLoading: storyLoading, error: storyError } = useStory(storyId);
+ const { data: epic, isLoading: epicLoading } = useEpic(story?.epicId || '');
+ const { data: project, isLoading: projectLoading } = useProject(story?.projectId || '');
+ const updateStory = useUpdateStory();
+ const deleteStory = useDeleteStory();
+ const changeStatus = useChangeStoryStatus();
+
+ const handleDeleteStory = async () => {
+ try {
+ await deleteStory.mutateAsync(storyId);
+ toast.success('Story deleted successfully');
+ // Navigate back to epic detail page
+ router.push(`/epics/${story?.epicId}`);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Failed to delete story';
+ toast.error(message);
+ }
+ };
+
+ const handleStatusChange = async (status: WorkItemStatus) => {
+ if (!story) return;
+ try {
+ await changeStatus.mutateAsync({ id: storyId, status });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Failed to update status';
+ toast.error(message);
+ }
+ };
+
+ const handlePriorityChange = async (priority: WorkItemPriority) => {
+ if (!story) return;
+ try {
+ await updateStory.mutateAsync({
+ id: storyId,
+ data: { priority },
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : 'Failed to update priority';
+ toast.error(message);
+ }
+ };
+
+ const getStatusColor = (status: WorkItemStatus) => {
+ switch (status) {
+ case 'Backlog':
+ return 'secondary';
+ case 'Todo':
+ return 'outline';
+ case 'InProgress':
+ return 'default';
+ case 'Done':
+ return 'success' as any;
+ default:
+ return 'secondary';
+ }
+ };
+
+ const getPriorityColor = (priority: WorkItemPriority) => {
+ switch (priority) {
+ case 'Low':
+ return 'bg-blue-100 text-blue-700 hover:bg-blue-100';
+ case 'Medium':
+ return 'bg-yellow-100 text-yellow-700 hover:bg-yellow-100';
+ case 'High':
+ return 'bg-orange-100 text-orange-700 hover:bg-orange-100';
+ case 'Critical':
+ return 'bg-red-100 text-red-700 hover:bg-red-100';
+ default:
+ return 'secondary';
+ }
+ };
+
+ // Loading state
+ if (storyLoading || epicLoading || projectLoading) {
+ return (
+
+ );
+ }
+
+ // Error state
+ if (storyError || !story) {
+ return (
+
+
+
+ Error Loading Story
+
+ {storyError instanceof Error ? storyError.message : 'Story not found'}
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Breadcrumb Navigation */}
+
+
+ Projects
+
+ /
+ {project && (
+ <>
+
+ {project.name}
+
+ /
+ >
+ )}
+
+ Epics
+
+ /
+ {epic && (
+ <>
+
+ {epic.name}
+
+ /
+ >
+ )}
+ Stories
+ /
+
+ {story.title}
+
+
+
+ {/* Header */}
+
+
+
+
+
+
{story.title}
+
+ {story.status}
+ {story.priority}
+
+
+
+
+
+
+
+
+
+
+ {/* Two-column layout */}
+
+ {/* Main Content Area (2/3 width) */}
+
+ {/* Story Details Card */}
+
+
+ Story Details
+
+
+ {story.description ? (
+
+
+ Description
+
+
{story.description}
+
+ ) : (
+ No description
+ )}
+
+
+
+ {/* Tasks Section - Placeholder for Story 2 */}
+
+
+
+ Tasks
+
+
+
+
+
+ Task management will be available in the next update.
+
+
+
+
+
+ {/* Metadata Sidebar (1/3 width) */}
+
+ {/* Status */}
+
+
+ Status
+
+
+
+
+
+
+ {/* Priority */}
+
+
+ Priority
+
+
+
+
+
+
+ {/* Assignee */}
+ {story.assigneeId && (
+
+
+ Assignee
+
+
+
+
+ {story.assigneeId}
+
+
+
+ )}
+
+ {/* Time Tracking */}
+ {(story.estimatedHours !== undefined || story.actualHours !== undefined) && (
+
+
+ Time Tracking
+
+
+ {story.estimatedHours !== undefined && (
+
+
+ Estimated: {story.estimatedHours}h
+
+ )}
+ {story.actualHours !== undefined && (
+
+
+ Actual: {story.actualHours}h
+
+ )}
+
+
+ )}
+
+ {/* Dates */}
+
+
+ Dates
+
+
+
+
+
+
Created
+
+ {formatDistanceToNow(new Date(story.createdAt), { addSuffix: true })}
+
+
+
+
+
+
+
Updated
+
+ {formatDistanceToNow(new Date(story.updatedAt), { addSuffix: true })}
+
+
+
+
+
+
+ {/* Parent Epic Card */}
+ {epic && (
+
+
+ Parent Epic
+
+
+
+
+
+ {epic.name}
+
+
+
+ {epic.status}
+
+
+ {epic.priority}
+
+
+
+
+
+ )}
+
+
+
+ {/* Edit Story Dialog */}
+
+
+ {/* Delete Story Confirmation Dialog */}
+
+
+
+ Are you sure?
+
+ This action cannot be undone. This will permanently delete the story
+ and all its associated tasks.
+
+
+
+ Cancel
+
+ {deleteStory.isPending ? (
+ <>
+
+ Deleting...
+ >
+ ) : (
+ 'Delete Story'
+ )}
+
+
+
+
+
+ );
+}
diff --git a/lib/api/pm.ts b/lib/api/pm.ts
index 65c780b..3a02796 100644
--- a/lib/api/pm.ts
+++ b/lib/api/pm.ts
@@ -28,7 +28,7 @@ export const epicsApi = {
create: async (data: CreateEpicDto): Promise => {
console.log('[epicsApi.create] Sending request', { url: '/api/v1/epics', data });
try {
- const result = await api.post('/api/v1/epics', data);
+ const result = await api.post('/api/v1/epics', data);
console.log('[epicsApi.create] Request successful', result);
return result;
} catch (error) {