diff --git a/components/projects/acceptance-criteria-editor.tsx b/components/projects/acceptance-criteria-editor.tsx new file mode 100644 index 0000000..7637d12 --- /dev/null +++ b/components/projects/acceptance-criteria-editor.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Checkbox } from '@/components/ui/checkbox'; +import { X, Plus } from 'lucide-react'; + +interface AcceptanceCriteriaEditorProps { + criteria: string[]; + onChange: (criteria: string[]) => void; + disabled?: boolean; +} + +export function AcceptanceCriteriaEditor({ + criteria, + onChange, + disabled, +}: AcceptanceCriteriaEditorProps) { + const [newCriterion, setNewCriterion] = useState(''); + + const addCriterion = () => { + if (newCriterion.trim()) { + onChange([...criteria, newCriterion.trim()]); + setNewCriterion(''); + } + }; + + const removeCriterion = (index: number) => { + onChange(criteria.filter((_, i) => i !== index)); + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + addCriterion(); + } + }; + + return ( +
+ {/* Existing criteria list */} + {criteria.length > 0 && ( +
+ {criteria.map((criterion, index) => ( +
+ + {criterion} + {!disabled && ( + + )} +
+ ))} +
+ )} + + {/* Add new criterion */} + {!disabled && ( +
+ setNewCriterion(e.target.value)} + onKeyPress={handleKeyPress} + /> + +
+ )} + + {criteria.length === 0 && disabled && ( +

+ No acceptance criteria defined +

+ )} +
+ ); +} diff --git a/components/projects/story-form.tsx b/components/projects/story-form.tsx index 2c7837c..e5a6456 100644 --- a/components/projects/story-form.tsx +++ b/components/projects/story-form.tsx @@ -28,6 +28,8 @@ import type { Story, WorkItemPriority } from '@/types/project'; import { toast } from 'sonner'; import { Loader2 } from 'lucide-react'; import { useAuthStore } from '@/stores/authStore'; +import { AcceptanceCriteriaEditor } from './acceptance-criteria-editor'; +import { TagsInput } from './tags-input'; const storySchema = z.object({ epicId: z.string().min(1, 'Parent Epic is required'), @@ -45,6 +47,16 @@ const storySchema = z.object({ .min(0, 'Estimated hours must be positive') .optional() .or(z.literal('')), + // Sprint 4 Story 3: New fields + acceptanceCriteria: z.array(z.string()).default([]), + assigneeId: z.string().optional(), + tags: z.array(z.string()).default([]), + storyPoints: z + .number() + .min(0, 'Story points must be positive') + .max(100, 'Story points must be less than 100') + .optional() + .or(z.literal('')), }); type StoryFormValues = z.infer; @@ -80,6 +92,11 @@ export function StoryForm({ description: story?.description || '', priority: story?.priority || 'Medium', estimatedHours: story?.estimatedHours || ('' as any), + // Sprint 4 Story 3: New field defaults + acceptanceCriteria: story?.acceptanceCriteria || [], + assigneeId: story?.assigneeId || '', + tags: story?.tags || [], + storyPoints: story?.storyPoints || ('' as any), }, }); @@ -94,6 +111,12 @@ export function StoryForm({ priority: data.priority, estimatedHours: typeof data.estimatedHours === 'number' ? data.estimatedHours : undefined, + // Sprint 4 Story 3: New fields + acceptanceCriteria: data.acceptanceCriteria, + assigneeId: data.assigneeId || undefined, + tags: data.tags, + storyPoints: + typeof data.storyPoints === 'number' ? data.storyPoints : undefined, }, }); toast.success('Story updated successfully'); @@ -115,6 +138,12 @@ export function StoryForm({ estimatedHours: typeof data.estimatedHours === 'number' ? data.estimatedHours : undefined, createdBy: user.id, + // Sprint 4 Story 3: New fields + acceptanceCriteria: data.acceptanceCriteria, + assigneeId: data.assigneeId || undefined, + tags: data.tags, + storyPoints: + typeof data.storyPoints === 'number' ? data.storyPoints : undefined, }); toast.success('Story created successfully'); } @@ -259,6 +288,111 @@ export function StoryForm({ /> + {/* Sprint 4 Story 3: Acceptance Criteria */} + ( + + Acceptance Criteria + + + + + Define conditions that must be met for this story to be complete + + + + )} + /> + + {/* Sprint 4 Story 3: Assignee and Story Points */} +
+ ( + + Assignee + + Assign to team member + + + )} + /> + + ( + + Story Points + + { + const value = e.target.value; + field.onChange(value === '' ? '' : parseInt(value)); + }} + value={field.value === undefined ? '' : field.value} + disabled={isLoading} + /> + + Fibonacci: 1, 2, 3, 5, 8, 13... + + + )} + /> +
+ + {/* Sprint 4 Story 3: Tags */} + ( + + Tags + + + + + Add tags to categorize this story (e.g., frontend, bug, urgent) + + + + )} + /> +
{onCancel && ( + )} + + ))} +
+ )} + + {/* Input for new tags */} + {!disabled && ( + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={() => inputValue && addTag()} + /> + )} + + ); +} diff --git a/types/project.ts b/types/project.ts index 707e180..393698c 100644 --- a/types/project.ts +++ b/types/project.ts @@ -71,6 +71,10 @@ export interface Story { estimatedHours?: number; actualHours?: number; assigneeId?: string; + assigneeName?: string; // Sprint 4 Story 3: Assignee display name + acceptanceCriteria?: string[]; // Sprint 4 Story 3: Acceptance criteria list + tags?: string[]; // Sprint 4 Story 3: Tags/labels + storyPoints?: number; // Sprint 4 Story 3: Story points tenantId: string; createdAt: string; updatedAt: string; @@ -84,6 +88,10 @@ export interface CreateStoryDto { priority: WorkItemPriority; estimatedHours?: number; createdBy: string; // Required field matching backend API + assigneeId?: string; // Sprint 4 Story 3 + acceptanceCriteria?: string[]; // Sprint 4 Story 3 + tags?: string[]; // Sprint 4 Story 3 + storyPoints?: number; // Sprint 4 Story 3 } export interface UpdateStoryDto { @@ -92,6 +100,10 @@ export interface UpdateStoryDto { priority?: WorkItemPriority; estimatedHours?: number; actualHours?: number; + assigneeId?: string; // Sprint 4 Story 3 + acceptanceCriteria?: string[]; // Sprint 4 Story 3 + tags?: string[]; // Sprint 4 Story 3 + storyPoints?: number; // Sprint 4 Story 3 } // ==================== Task ====================