diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/Project.java b/backend/src/main/java/net/beetechgroup/beetask/entities/Project.java index 7d9b0cd..2a21b75 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/Project.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/Project.java @@ -8,6 +8,8 @@ import net.beetechgroup.beetask.entities.organization.Organization; public class Project { private Long id; private String name; + private String color; + private String icon; private Organization organization; private List users; @@ -27,6 +29,22 @@ public class Project { this.name = name; } + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + public Organization getOrganization() { return organization; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ProjectEntity.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ProjectEntity.java index 15f1076..a0fd6d0 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ProjectEntity.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ProjectEntity.java @@ -19,6 +19,8 @@ public class ProjectEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; + private String color; + private String icon; @ManyToOne @JoinColumn(name = "organization_id") private OrganizationEntity organization; @@ -43,6 +45,21 @@ public class ProjectEntity { this.name = name; } + public String getColor() { + return color; + } + + public void setColor(String color) { + this.color = color; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } public OrganizationEntity getOrganization() { return organization; diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ProjectEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ProjectEntityMapper.java index fce7f28..65ef237 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ProjectEntityMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ProjectEntityMapper.java @@ -11,6 +11,8 @@ public class ProjectEntityMapper { ProjectEntity entity = new ProjectEntity(); entity.setId(project.getId()); entity.setName(project.getName()); + entity.setColor(project.getColor()); + entity.setIcon(project.getIcon()); entity.setOrganization(OrganizationEntityMapper.toEntity(project.getOrganization())); return entity; } @@ -21,6 +23,8 @@ public class ProjectEntityMapper { Project project = new Project(); project.setId(entity.getId()); project.setName(entity.getName()); + project.setColor(entity.getColor()); + project.setIcon(entity.getIcon()); project.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization())); return project; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java index d9c2daf..7b16347 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/TaskRepositoryImpl.java @@ -53,17 +53,16 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository findAccessibleTasksFiltered(String email, Long organizationId, String text, List projectIds, + public List findAccessibleTasksFiltered(Long organizationId, String text, List projectIds, List statuses, List categoryIds, List userIds) { StringBuilder query = new StringBuilder( - "select distinct t from TaskEntity t join t.project p left join p.users pu left join t.user tu " + - "where p.organization.id = ?1 and (pu.email = ?2 or tu.email = ?2)" + "select distinct t from TaskEntity t join t.project p " + + "where p.organization.id = ?1" ); List params = new ArrayList<>(); params.add(organizationId); - params.add(email); - int idx = 3; + int idx = 2; if (Objects.nonNull(text) && !text.isBlank()) { query.append(" and (lower(t.title) like ?").append(idx).append(" or lower(t.description) like ?").append(idx + 1).append(")"); String pattern = "%" + text.toLowerCase() + "%"; @@ -92,8 +91,8 @@ public class TaskRepositoryImpl implements TaskRepository, PanacheRepository tasks = find(query.toString(), params.toArray()).list().stream().map(TaskEntityMapper::toDomain).toList(); - LOGGER.infof("Loaded %d accessible tasks for user %s in organization %d with filters (text=%s, projectIds=%s, statuses=%s, categoryIds=%s, userIds=%s)", - tasks.size(), email, organizationId, text, projectIds, statuses, categoryIds, userIds); + LOGGER.infof("Loaded %d accessible tasks in organization %d with filters (text=%s, projectIds=%s, statuses=%s, categoryIds=%s, userIds=%s)", + tasks.size(), organizationId, text, projectIds, statuses, categoryIds, userIds); return tasks; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/project/ProjectController.java b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/project/ProjectController.java index fa9e5b3..e6bb849 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/project/ProjectController.java +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/project/ProjectController.java @@ -32,7 +32,7 @@ public class ProjectController { @Authenticated public List listProjects(@PathParam("orgId") Long orgId) { List projects = listProjectsUseCase.execute(orgId).stream() - .map(p -> new ProjectResponse(p.getId(), p.getName())) + .map(p -> new ProjectResponse(p.getId(), p.getName(), p.getColor(), p.getIcon())) .toList(); LOGGER.infof("Listed %d projects for organization %d", projects.size(), orgId); return projects; @@ -42,14 +42,14 @@ public class ProjectController { @Authenticated public ProjectResponse createProject(@PathParam("orgId") Long orgId, CreateProjectRequest request) { LOGGER.infof("Project creation requested for organization %d with name '%s'", orgId, request.name()); - Project project = createProjectUseCase.execute(orgId, request.name()); + Project project = createProjectUseCase.execute(orgId, request.name(), request.color(), request.icon()); LOGGER.infof("Project %d created successfully for organization %d", project.getId(), orgId); - return new ProjectResponse(project.getId(), project.getName()); + return new ProjectResponse(project.getId(), project.getName(), project.getColor(), project.getIcon()); } // TODO must be a independent file - public record CreateProjectRequest(String name) {} + public record CreateProjectRequest(String name, String color, String icon) {} // TODO must be a independent file - public record ProjectResponse(Long id, String name) {} + public record ProjectResponse(Long id, String name, String color, String icon) {} } diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/project/create/CreateProjectUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/project/create/CreateProjectUseCase.java index 7b49f59..37bf0e3 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/project/create/CreateProjectUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/project/create/CreateProjectUseCase.java @@ -16,7 +16,7 @@ public class CreateProjectUseCase { this.organizationRepository = organizationRepository; } - public Project execute(Long organizationId, String projectName) { + public Project execute(Long organizationId, String projectName, String color, String icon) { Organization organization = organizationRepository.findOrganizationById(organizationId) .orElseThrow(() -> { LOGGER.warnf("Project creation failed because organization %d was not found", organizationId); @@ -26,6 +26,8 @@ public class CreateProjectUseCase { LOGGER.infof("Creating project '%s' in organization %d", projectName, organizationId); Project project = new Project(); project.setName(projectName); + project.setColor(color); + project.setIcon(icon); project.setOrganization(organization); Project savedProject = projectRepository.saveProject(project); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java index 58acb36..6bb36b2 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/TaskRepository.java @@ -10,7 +10,7 @@ public interface TaskRepository { Task saveTask(Task task); Task findTaskById(Long id); List findAllTasks(); - List findAccessibleTasksFiltered(String email, Long organizationId, String text, List projectIds, + List findAccessibleTasksFiltered(Long organizationId, String text, List projectIds, List statuses, List categoryIds, List userIds); List findTasksWorkedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end); List findTasksFinishedByUserInPeriod(String email, LocalDateTime start, LocalDateTime end); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java index 848d1f8..5087aa3 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/task/listall/ListAllTasksUseCase.java @@ -35,7 +35,6 @@ public class ListAllTasksUseCase { } List tasks = taskRepository.findAccessibleTasksFiltered( - input.userEmail(), input.organizationId(), input.text(), input.projectIds(), diff --git a/backend/src/main/resources/db/migration/V3__add_project_color_icon.sql b/backend/src/main/resources/db/migration/V3__add_project_color_icon.sql new file mode 100644 index 0000000..215b778 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__add_project_color_icon.sql @@ -0,0 +1,2 @@ +ALTER TABLE projects ADD COLUMN color VARCHAR(9); +ALTER TABLE projects ADD COLUMN icon VARCHAR(64); diff --git a/frontend/src/components/TaskFilterBar.tsx b/frontend/src/components/TaskFilterBar.tsx index df39cb6..7684be2 100644 --- a/frontend/src/components/TaskFilterBar.tsx +++ b/frontend/src/components/TaskFilterBar.tsx @@ -1,5 +1,5 @@ import { Filter, RefreshCw, Search } from 'lucide-react' -import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS } from '../types/task' +import { TASK_STATUS_LABELS, TASK_STATUS_OPTIONS, type TaskStatus } from '../types/task' import type { Project } from '../services/projectService' import type { Category } from '../types/category' import { MultiSelectChips } from './MultiSelectChips' @@ -9,6 +9,13 @@ import type { TaskFilters } from './taskFilters' import type { TaskAssignee } from '../types/task' import { UserAvatar } from './UserAvatar' +const STATUS_CHIP_COLORS: Record = { + NOT_STARTED: '#94a3b8', + IN_PROGRESS: '#f59e0b', + COMPLETED: '#10b981', + CANCELED: '#ef4444', +} + interface TaskFilterBarProps { filters: TaskFilters onFiltersChange: (filters: TaskFilters) => void @@ -59,26 +66,31 @@ export function TaskFilterBar({ )} -
- {projects.length > 0 && ( - ({ - value: project.id, - label: project.name, + {projects.length > 0 && ( +
+ + Projetos + ({ + value: p.id, + label: p.name, + color: p.color ?? undefined, + icon: p.icon ? : undefined, }))} selected={filters.projectIds} onChange={(projectIds) => update({ projectIds })} /> - )} +
+ )} - ({ - value: status, - label: TASK_STATUS_LABELS[status], +
+ + Status + ({ + value: s, + label: TASK_STATUS_LABELS[s], + color: STATUS_CHIP_COLORS[s], }))} selected={filters.statuses} onChange={(statuses) => update({ statuses })} diff --git a/frontend/src/pages/ProjectsPage.tsx b/frontend/src/pages/ProjectsPage.tsx index 988e3b9..ba70412 100644 --- a/frontend/src/pages/ProjectsPage.tsx +++ b/frontend/src/pages/ProjectsPage.tsx @@ -13,6 +13,7 @@ import { } from "lucide-react"; import { useAuth } from "../contexts/AuthContext"; import { projectService, type Project } from "../services/projectService"; +import { IconPicker } from "../components/IconPicker"; export const ProjectsPage: React.FC = () => { const { activeOrg } = useAuth(); @@ -21,6 +22,8 @@ export const ProjectsPage: React.FC = () => { const [isCreating, setIsCreating] = useState(false); const [showCreateForm, setShowCreateForm] = useState(false); const [newProjectName, setNewProjectName] = useState(""); + const [newProjectColor, setNewProjectColor] = useState("#6366f1"); + const [newProjectIcon, setNewProjectIcon] = useState("Folder"); const [searchQuery, setSearchQuery] = useState(""); const [error, setError] = useState(null); @@ -49,12 +52,15 @@ export const ProjectsPage: React.FC = () => { setIsCreating(true); setError(null); try { - const newProject = await projectService.create( - activeOrg.id, - newProjectName, - ); + const newProject = await projectService.create(activeOrg.id, { + name: newProjectName, + color: newProjectColor, + icon: newProjectIcon, + }); setProjects((prev) => [...prev, newProject]); setNewProjectName(""); + setNewProjectColor("#6366f1"); + setNewProjectIcon("Folder"); setShowCreateForm(false); } catch (err) { console.error("Failed to create project", err); @@ -311,6 +317,34 @@ export const ProjectsPage: React.FC = () => { />
+
+ +
+ setNewProjectColor(e.target.value)} + className="h-14 w-14 cursor-pointer rounded-xl border border-border-soft" + /> + setNewProjectColor(e.target.value)} + className="flex-1 px-4 py-3 bg-surface-muted/50 border border-border-soft rounded-xl text-text-main font-mono text-sm focus:outline-none focus:ring-4 focus:ring-brand/10 focus:border-brand transition-all" + placeholder="#6366f1" + /> +
+
+ +
+ + +
+ {error && (
@@ -320,7 +354,7 @@ export const ProjectsPage: React.FC = () => {