diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/ProjectUseCaseConfig.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/ProjectUseCaseConfig.java new file mode 100644 index 0000000..3f966dd --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/config/ProjectUseCaseConfig.java @@ -0,0 +1,23 @@ +package net.beetechgroup.beetask.frameworks.config; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import net.beetechgroup.beetask.usecase.project.create.CreateProjectUseCase; +import net.beetechgroup.beetask.usecase.project.list.ListProjectsUseCase; +import net.beetechgroup.beetask.usecase.repository.OrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.ProjectRepository; + +@ApplicationScoped +public class ProjectUseCaseConfig { + + @Produces + public CreateProjectUseCase createProjectUseCase(ProjectRepository projectRepository, + OrganizationRepository organizationRepository) { + return new CreateProjectUseCase(projectRepository, organizationRepository); + } + + @Produces + public ListProjectsUseCase listProjectsUseCase(ProjectRepository projectRepository) { + return new ListProjectsUseCase(projectRepository); + } +} 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 58336f4..e199a46 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 @@ -10,6 +10,7 @@ public class ProjectEntityMapper { ProjectEntity entity = new ProjectEntity(); entity.setId(project.getId()); entity.setName(project.getName()); + entity.setOrganization(OrganizationEntityMapper.toEntity(project.getOrganization())); return entity; } @@ -19,6 +20,7 @@ public class ProjectEntityMapper { Project project = new Project(); project.setId(entity.getId()); project.setName(entity.getName()); + project.setOrganization(OrganizationEntityMapper.toDomain(entity.getOrganization())); return project; } } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/ProjectRepositoryImpl.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/ProjectRepositoryImpl.java new file mode 100644 index 0000000..f652aa3 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/repository/ProjectRepositoryImpl.java @@ -0,0 +1,41 @@ +package net.beetechgroup.beetask.frameworks.persistence.repository; + +import io.quarkus.hibernate.orm.panache.PanacheRepository; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; +import net.beetechgroup.beetask.entities.Project; +import net.beetechgroup.beetask.frameworks.persistence.entities.ProjectEntity; +import net.beetechgroup.beetask.frameworks.persistence.mapper.ProjectEntityMapper; +import net.beetechgroup.beetask.usecase.repository.ProjectRepository; + +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class ProjectRepositoryImpl implements ProjectRepository, PanacheRepository { + + @Override + @Transactional + public Project saveProject(Project project) { + ProjectEntity entity = ProjectEntityMapper.toEntity(project); + if (entity.getId() == null) { + persist(entity); + } else { + entity = getEntityManager().merge(entity); + } + return ProjectEntityMapper.toDomain(entity); + } + + @Override + public Optional findProjectById(Long id) { + return findByIdOptional(id) + .map(ProjectEntityMapper::toDomain); + } + + @Override + public List findByOrganizationId(Long organizationId) { + return find("organization.id", organizationId).stream() + .map(ProjectEntityMapper::toDomain) + .toList(); + } +} 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 new file mode 100644 index 0000000..a153bd5 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/interfaceadapters/controllers/project/ProjectController.java @@ -0,0 +1,49 @@ +package net.beetechgroup.beetask.interfaceadapters.controllers.project; + +import io.quarkus.security.Authenticated; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import net.beetechgroup.beetask.entities.Project; +import net.beetechgroup.beetask.usecase.project.create.CreateProjectUseCase; +import net.beetechgroup.beetask.usecase.project.list.ListProjectsUseCase; + +import java.util.List; + +@Path("/organizations/{orgId}/projects") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ProjectController { + + @Inject + CreateProjectUseCase createProjectUseCase; + + @Inject + ListProjectsUseCase listProjectsUseCase; + + @GET + @Authenticated + public List listProjects(@PathParam("orgId") Long orgId) { + return listProjectsUseCase.execute(orgId).stream() + .map(p -> new ProjectResponse(p.getId(), p.getName())) + .toList(); + } + + @POST + @Authenticated + public ProjectResponse createProject(@PathParam("orgId") Long orgId, CreateProjectRequest request) { + Project project = createProjectUseCase.execute(orgId, request.name()); + return new ProjectResponse(project.getId(), project.getName()); + } + + // TODO must be a independent file + public record CreateProjectRequest(String name) {} + + // TODO must be a independent file + public record ProjectResponse(Long id, String name) {} +} 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 new file mode 100644 index 0000000..f203a20 --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/project/create/CreateProjectUseCase.java @@ -0,0 +1,27 @@ +package net.beetechgroup.beetask.usecase.project.create; + +import net.beetechgroup.beetask.entities.Project; +import net.beetechgroup.beetask.entities.organization.Organization; +import net.beetechgroup.beetask.usecase.repository.OrganizationRepository; +import net.beetechgroup.beetask.usecase.repository.ProjectRepository; + +public class CreateProjectUseCase { + private final ProjectRepository projectRepository; + private final OrganizationRepository organizationRepository; + + public CreateProjectUseCase(ProjectRepository projectRepository, OrganizationRepository organizationRepository) { + this.projectRepository = projectRepository; + this.organizationRepository = organizationRepository; + } + + public Project execute(Long organizationId, String projectName) { + Organization organization = organizationRepository.findOrganizationById(organizationId) + .orElseThrow(() -> new RuntimeException("Organization not found")); + + Project project = new Project(); + project.setName(projectName); + project.setOrganization(organization); + + return projectRepository.saveProject(project); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/project/list/ListProjectsUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/project/list/ListProjectsUseCase.java new file mode 100644 index 0000000..236da7c --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/project/list/ListProjectsUseCase.java @@ -0,0 +1,18 @@ +package net.beetechgroup.beetask.usecase.project.list; + +import net.beetechgroup.beetask.entities.Project; +import net.beetechgroup.beetask.usecase.repository.ProjectRepository; + +import java.util.List; + +public class ListProjectsUseCase { + private final ProjectRepository projectRepository; + + public ListProjectsUseCase(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + public List execute(Long organizationId) { + return projectRepository.findByOrganizationId(organizationId); + } +} diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/ProjectRepository.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/ProjectRepository.java new file mode 100644 index 0000000..115f59b --- /dev/null +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/repository/ProjectRepository.java @@ -0,0 +1,11 @@ +package net.beetechgroup.beetask.usecase.repository; + +import net.beetechgroup.beetask.entities.Project; +import java.util.List; +import java.util.Optional; + +public interface ProjectRepository { + Project saveProject(Project project); + Optional findProjectById(Long id); + List findByOrganizationId(Long organizationId); +} diff --git a/frontend/src/components/AppShell.tsx b/frontend/src/components/AppShell.tsx index 204161f..8ab36a4 100644 --- a/frontend/src/components/AppShell.tsx +++ b/frontend/src/components/AppShell.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { Layout, ListTodo, PlusCircle, Columns, LogOut, Settings, Building2, Clock, ChevronDown, Shield } from 'lucide-react' +import { Layout, ListTodo, PlusCircle, Columns, LogOut, Settings, Building2, Clock, ChevronDown, Shield, FolderKanban } from 'lucide-react' import { NavLink, Outlet, Navigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' import { OnboardingModal } from './OnboardingModal' @@ -147,6 +147,10 @@ export function AppShell() { Quadro + + + Projetos + Solicitações @@ -215,6 +219,10 @@ export function AppShell() { Quadro + + + Projetos + Pedidos diff --git a/frontend/src/pages/ProjectsPage.tsx b/frontend/src/pages/ProjectsPage.tsx new file mode 100644 index 0000000..dbce260 --- /dev/null +++ b/frontend/src/pages/ProjectsPage.tsx @@ -0,0 +1,249 @@ +import React, { useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + FolderKanban, + Plus, + Search, + Building2, + Layout, + ArrowRight, + Loader2, + X, +} from "lucide-react"; +import { useAuth } from "../contexts/AuthContext"; +import { projectService, type Project } from "../services/projectService"; + +export const ProjectsPage: React.FC = () => { + const { activeOrg } = useAuth(); + const [projects, setProjects] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isCreating, setIsCreating] = useState(false); + const [newProjectName, setNewProjectName] = useState(""); + const [searchQuery, setSearchQuery] = useState(""); + const [error, setError] = useState(null); + + const fetchProjects = async () => { + if (!activeOrg) return; + setIsLoading(true); + try { + const data = await projectService.listByOrganization(activeOrg.id); + setProjects(data); + } catch (err) { + console.error("Failed to fetch projects", err); + setError("Não foi possível carregar os projetos."); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + fetchProjects(); + }, [activeOrg]); + + const handleCreateProject = async (e: React.FormEvent) => { + e.preventDefault(); + if (!activeOrg || !newProjectName.trim()) return; + + setIsCreating(true); + try { + const newProject = await projectService.create( + activeOrg.id, + newProjectName, + ); + setProjects((prev) => [...prev, newProject]); + setNewProjectName(""); + setIsCreating(false); + } catch (err) { + console.error("Failed to create project", err); + setError("Falha ao criar o projeto."); + setIsCreating(false); + } + }; + + const filteredProjects = projects.filter((p) => + p.name.toLowerCase().includes(searchQuery.toLowerCase()), + ); + + if (!activeOrg) { + return ( +
+
+ +
+

+ Nenhuma Organização Ativa +

+

+ Selecione uma organização no menu superior para gerenciar seus + projetos. +

+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ + Gerenciamento +
+

+ Projetos de {activeOrg.name} +

+

+ Organize suas tarefas em projetos específicos para melhor + acompanhamento. +

+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-12 pr-6 py-3.5 bg-surface border border-border-soft rounded-2xl outline-none focus:border-brand focus:ring-4 focus:ring-brand/5 transition-all w-full md:w-64 font-medium" + /> +
+
+
+ +
+ {/* New Project Form */} +
+ +
+ +
+

+ Novo Projeto +

+

+ Crie um novo contêiner para suas tarefas e objetivos. +

+ +
+
+ + setNewProjectName(e.target.value)} + className="w-full px-5 py-4 bg-app-bg border border-border-soft rounded-2xl outline-none focus:border-brand focus:ring-4 focus:ring-brand/5 transition-all font-bold" + /> +
+ + +
+ + {error && ( + + + {error} + + )} +
+
+ + {/* Projects List */} +
+ {isLoading ? ( +
+ {[1, 2, 4, 5, 6].map((i) => ( +
+ ))} +
+ ) : filteredProjects.length === 0 ? ( +
+
+ +
+

+ Nenhum projeto encontrado +

+

+ {searchQuery + ? "Nenhum projeto corresponde à sua busca." + : "Sua organização ainda não possui projetos."} +

+
+ ) : ( +
+ + {filteredProjects.map((project, index) => ( + +
+
+ {project.name.charAt(0).toUpperCase()} +
+
+

+ {project.name} +

+
+ + ID #{project.id} + +
+
+
+
+ +
+
+ ))} +
+
+ )} +
+
+
+ ); +}; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index ff23164..dc76e8b 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -10,6 +10,7 @@ import { CreateOrganizationPage } from "./pages/CreateOrganizationPage"; import { JoinOrganizationPage } from "./pages/JoinOrganizationPage"; import { OrganizationAdminPage } from "./pages/OrganizationAdminPage"; import { MyRequestsPage } from "./pages/MyRequestsPage"; +import { ProjectsPage } from "./pages/ProjectsPage"; export const router = createBrowserRouter([ { @@ -52,6 +53,10 @@ export const router = createBrowserRouter([ path: "requests", element: , }, + { + path: "projects", + element: , + }, ], }, ]); diff --git a/frontend/src/services/projectService.ts b/frontend/src/services/projectService.ts new file mode 100644 index 0000000..c167846 --- /dev/null +++ b/frontend/src/services/projectService.ts @@ -0,0 +1,19 @@ +import { apiFetch } from '../lib/api' + +export interface Project { + id: number + name: string +} + +export const projectService = { + listByOrganization: async (orgId: number): Promise => { + return apiFetch(`/organizations/${orgId}/projects`) + }, + + create: async (orgId: number, name: string): Promise => { + return apiFetch(`/organizations/${orgId}/projects`, { + method: 'POST', + body: { name }, + }) + }, +}