diff --git a/new-frontend/src/components/tasks/CreateTaskModal.tsx b/new-frontend/src/components/tasks/CreateTaskModal.tsx index bb63d30..08d7686 100644 --- a/new-frontend/src/components/tasks/CreateTaskModal.tsx +++ b/new-frontend/src/components/tasks/CreateTaskModal.tsx @@ -5,6 +5,7 @@ import type { OrganizationMember } from '../../services/organizationService' import { projectService } from '../../services/projectService' import { categoryService } from '../../services/categoryService' import { organizationService } from '../../services/organizationService' +import { CustomSelect } from '../ui/Select' interface CreateTaskModalProps { isOpen: boolean @@ -272,18 +273,20 @@ export const CreateTaskModal: React.FC = ({ ) : ( - + setProjectId(Number(val) === 0 ? undefined : Number(val))} + placeholder="Sem projeto" + leftIcon={} + options={[ + { value: 0, label: 'Sem projeto' }, + ...projects.map((p) => ({ + value: p.id, + label: p.name, + color: p.color || '#0f766e', + })), + ]} + /> )} @@ -297,7 +300,7 @@ export const CreateTaskModal: React.FC = ({ @@ -321,18 +324,20 @@ export const CreateTaskModal: React.FC = ({ ) : ( - + setCategoryId(Number(val) === 0 ? undefined : Number(val))} + placeholder="Sem categoria" + leftIcon={} + options={[ + { value: 0, label: 'Sem categoria' }, + ...categories.map((c) => ({ + value: c.id, + label: c.name, + color: c.color || '#d97706', + })), + ]} + /> )} @@ -345,20 +350,20 @@ export const CreateTaskModal: React.FC = ({ Responsável - + setAssigneeUserId(Number(val) === 0 ? undefined : Number(val))} + placeholder="Atribuir a mim mesmo / Sem responsável" + leftIcon={} + options={[ + { value: 0, label: 'Sem responsável (atribuir a mim)' }, + ...members.map((m) => ({ + value: m.userId, + label: m.userName, + description: m.userEmail, + })), + ]} + /> {/* Status */} @@ -366,16 +371,16 @@ export const CreateTaskModal: React.FC = ({ - + onChange={(val) => setStatus(val as TaskStatus)} + options={[ + { value: 'NOT_STARTED', label: 'A Fazer', color: '#71717a' }, + { value: 'IN_PROGRESS', label: 'Em Andamento', color: '#14b8a6' }, + { value: 'COMPLETED', label: 'Concluído', color: '#10b981' }, + { value: 'CANCELED', label: 'Cancelado', color: '#f43f5e' }, + ]} + /> diff --git a/new-frontend/src/components/tasks/TaskListView.tsx b/new-frontend/src/components/tasks/TaskListView.tsx index 26b8185..413a98c 100644 --- a/new-frontend/src/components/tasks/TaskListView.tsx +++ b/new-frontend/src/components/tasks/TaskListView.tsx @@ -2,6 +2,7 @@ import React from 'react' import { Play, Square, Clock, Folder, Tag, MoreHorizontal, CheckCircle2 } from 'lucide-react' import type { Task, TaskStatus } from '../../types/task' import { isTaskRunning, calculateTaskTotalSeconds, formatSecondsToTimer } from '../../lib/timeUtils' +import { CustomSelect } from '../ui/Select' interface TaskListViewProps { tasks: Task[] @@ -12,24 +13,6 @@ interface TaskListViewProps { activeTaskSeconds?: number } -const statusBadge: Record = { - NOT_STARTED: { - label: 'A Fazer', - badgeClass: 'bg-zinc-800 text-zinc-300 border-zinc-700/60', - }, - IN_PROGRESS: { - label: 'Em Andamento', - badgeClass: 'bg-teal-500/15 text-teal-300 border-teal-500/30', - }, - COMPLETED: { - label: 'Concluído', - badgeClass: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30', - }, - CANCELED: { - label: 'Cancelado', - badgeClass: 'bg-rose-500/15 text-rose-300 border-rose-500/30', - }, -} export const TaskListView: React.FC = ({ tasks, @@ -145,17 +128,19 @@ export const TaskListView: React.FC = ({ {/* Status Dropdown */} - +
+ onStatusChange(task, newStatus as TaskStatus)} + options={[ + { value: 'NOT_STARTED', label: 'A Fazer', color: '#71717a' }, + { value: 'IN_PROGRESS', label: 'Em Andamento', color: '#14b8a6' }, + { value: 'COMPLETED', label: 'Concluído', color: '#10b981' }, + { value: 'CANCELED', label: 'Cancelado', color: '#f43f5e' }, + ]} + /> +
{/* Time Spent */} diff --git a/new-frontend/src/components/ui/Select.tsx b/new-frontend/src/components/ui/Select.tsx new file mode 100644 index 0000000..ca6dbbb --- /dev/null +++ b/new-frontend/src/components/ui/Select.tsx @@ -0,0 +1,176 @@ +import { useState, useRef, useEffect, type ReactNode } from 'react' +import { ChevronDown, Check } from 'lucide-react' + +export interface SelectOption { + value: T + label: string + icon?: ReactNode + color?: string + badgeClass?: string + description?: string +} + +export interface CustomSelectProps { + value: T | undefined + onChange: (value: T) => void + options: SelectOption[] + placeholder?: string + className?: string + triggerClassName?: string + disabled?: boolean + leftIcon?: ReactNode + size?: 'sm' | 'md' +} + +export function CustomSelect({ + value, + onChange, + options, + placeholder = 'Selecione uma opção', + className = '', + triggerClassName = '', + disabled = false, + leftIcon, + size = 'md', +}: CustomSelectProps) { + const [isOpen, setIsOpen] = useState(false) + const containerRef = useRef(null) + + // Find currently selected option + const selectedOption = options.find((opt) => opt.value === value) + + // Close on outside click + useEffect(() => { + const handleOutsideClick = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + if (isOpen) { + document.addEventListener('mousedown', handleOutsideClick) + } + return () => { + document.removeEventListener('mousedown', handleOutsideClick) + } + }, [isOpen]) + + // Close on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + setIsOpen(false) + } + } + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [isOpen]) + + const handleSelect = (optValue: T) => { + onChange(optValue) + setIsOpen(false) + } + + const isSmall = size === 'sm' + + return ( +
+ {/* Trigger Button */} + + + {/* Dropdown Popover */} + {isOpen && ( +
+
+ {options.map((option) => { + const isSelected = option.value === value + return ( + + ) + })} +
+
+ )} +
+ ) +} diff --git a/new-frontend/src/pages/TasksPage.tsx b/new-frontend/src/pages/TasksPage.tsx index fa7bae3..9f50884 100644 --- a/new-frontend/src/pages/TasksPage.tsx +++ b/new-frontend/src/pages/TasksPage.tsx @@ -22,6 +22,7 @@ import type { Task, TaskStatus, Project } from '../types/task' import { TaskKanbanView } from '../components/tasks/TaskKanbanView' import { TaskListView } from '../components/tasks/TaskListView' import { CreateTaskModal } from '../components/tasks/CreateTaskModal' +import { CustomSelect } from '../components/ui/Select' export const TasksPage: React.FC = () => { const { activeOrg } = useAuth() @@ -367,39 +368,37 @@ export const TasksPage: React.FC = () => { {/* Project Filter */} {projects.length > 0 && ( -
- - + onChange={(val) => setSelectedProjectId(val)} + leftIcon={} + options={[ + { value: 'ALL', label: 'Todos os Projetos' }, + ...projects.map((p) => ({ + value: p.id, + label: p.name, + color: p.color || '#0f766e', + })), + ]} + />
)} {/* Status Filter */} - +
+ setSelectedStatus(val as TaskStatus | 'ALL')} + options={[ + { value: 'ALL', label: 'Todos os Status' }, + { value: 'NOT_STARTED', label: 'A Fazer', color: '#71717a' }, + { value: 'IN_PROGRESS', label: 'Em Andamento', color: '#14b8a6' }, + { value: 'COMPLETED', label: 'Concluído', color: '#10b981' }, + { value: 'CANCELED', label: 'Cancelado', color: '#f43f5e' }, + ]} + /> +