diff --git a/CONTRIBUTING.en.md b/CONTRIBUTING.en.md index fd03b647..479e1037 100644 --- a/CONTRIBUTING.en.md +++ b/CONTRIBUTING.en.md @@ -208,9 +208,16 @@ entry file and follows it to the full instructions. **3. Tell it what you want to do** -Start with something simple, like: +Start with something simple. Use **the number of the issue assigned to you**: -> I want to pick up issue #123. Where do I start? +> I want to pick up issue #NUMBER. Where do I start? + +> ⚠️ `#NUMBER` is a placeholder, not a command. Replace it with the real number +> — for example, `#487`. If you paste the text as-is, the AI will go looking for +> an issue that doesn't exist. +> +> No issue assigned yet? Just say so: *"I haven't picked up a task yet, can you +> help me choose one?"* The AI will ask you two things: **which language you'd like to talk in** and **how much experience** you have contributing to open source. From there it @@ -498,14 +505,26 @@ git commit --no-verify Automatically: **build**, **lint**, **tests**, **coverage**, **dependency audit**, and then the **SonarCloud** analysis. -Two things that commonly cause confusion: +### 🟥 Seeing red on your PR? It's probably not your fault + +This is the part that scares most people opening their first pull request. Some +jobs can fail for reasons **unrelated to your code**: + +- **Your PR comes from a fork.** For security, GitHub **does not hand repository + secrets to pull requests from forks** — that's what stops someone from opening + a malicious PR just to capture keys. Jobs that depend on those values may fail + or be skipped, and that's expected. +- **The dependency audit job** may be red because of vulnerabilities in + development tooling that have no published fix yet. That predates your PR. +- **SonarCloud runs in a separate workflow**, triggered after CI — precisely to + work around the fork/secrets limitation. If it takes a while to show up, wait. + +**What to do:** don't try to "fix" those failures. Check that the **build**, +**lint** and **test** jobs passed — those do depend on your code. If one of them +fails, that's worth investigating. -- **SonarCloud runs in a separate workflow**, triggered after CI. This is - necessary because PRs from forks don't receive secrets — without the split, it - would fail every time. -- The **dependency audit** job may show red because of vulnerabilities in - development tooling that have no published fix yet. If your PR didn't touch - dependencies, that's **not** your fault. +When in doubt, **ask in the PR**. Nobody will mind, and the answer is very likely +"you can ignore that one, it's on our side". Full details in [`docs/04 - processo/ci-e-validacao.md`](docs/04%20-%20processo/ci-e-validacao.md) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 804a7c58..7b7d139d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -196,9 +196,16 @@ de porta e é direcionada para as instruções completas. **3. Diga o que você quer fazer** -Comece por algo simples, como: +Comece por algo simples. Use **o número da issue que foi atribuída a você**: -> Quero pegar a issue #123. Por onde começo? +> Quero pegar a issue #NÚMERO. Por onde começo? + +> ⚠️ `#NÚMERO` é um espaço a preencher, não um comando. Troque pelo número real +> — por exemplo, `#487`. Se você colar o texto como está, a IA vai procurar uma +> issue que não existe. +> +> Ainda não tem issue atribuída? Diga isso mesmo: *"ainda não peguei nenhuma +> tarefa, pode me ajudar a escolher?"* A IA vai perguntar duas coisas: **em qual idioma você prefere conversar** e **qual é a sua experiência** com contribuição em open source. A partir daí ela @@ -476,13 +483,28 @@ git commit --no-verify Automaticamente: **build**, **lint**, **testes**, **cobertura**, **auditoria de dependências** e, em seguida, a análise do **SonarCloud**. -Pontos que costumam gerar dúvida: +### 🟥 Viu vermelho no seu PR? Provavelmente não é culpa sua + +Esta é a parte que mais assusta quem abre o primeiro pull request. Alguns jobs +podem falhar por motivos **que não têm relação com o seu código**: + +- **Seu PR vem de um fork.** Por segurança, o GitHub **não entrega os secrets do + repositório** para pull requests vindos de forks — é o que impede que alguém + abra um PR malicioso só para capturar chaves. Jobs que dependem desses valores + podem falhar ou ser pulados, e isso é esperado. +- **O job de auditoria de dependências** pode estar vermelho por vulnerabilidades + em ferramentas de desenvolvimento que ainda não têm correção publicada. É + anterior ao seu PR. +- **O SonarCloud roda num workflow separado**, disparado depois da CI — + justamente para contornar a limitação de secrets em forks. Se ele demorar a + aparecer, aguarde. + +**O que fazer:** não tente "consertar" esses erros. Confira se os jobs de +**build**, **lint** e **testes** passaram — esses sim dependem do seu código. Se +algum deles falhar, aí vale investigar. -- O **SonarCloud roda num workflow separado**, disparado após a CI. É necessário - porque PRs vindos de forks não recebem secrets — sem isso, ele falharia sempre. -- O job de **auditoria de dependências** pode aparecer vermelho por - vulnerabilidades em ferramentas de desenvolvimento que ainda não têm correção - publicada. Se o seu PR não mexeu em dependências, isso **não** é culpa dele. +Na dúvida, **comente no PR perguntando**. Ninguém vai achar ruim, e é bem +provável que a resposta seja "pode ignorar, é do nosso lado". O detalhamento está em [`docs/04 - processo/ci-e-validacao.md`](docs/04%20-%20processo/ci-e-validacao.md). diff --git a/docs/05 - contribuicao/IA-GUIA.md b/docs/05 - contribuicao/IA-GUIA.md index b477ab60..bcccf3ea 100644 --- a/docs/05 - contribuicao/IA-GUIA.md +++ b/docs/05 - contribuicao/IA-GUIA.md @@ -98,6 +98,26 @@ Depois de identificar a issue, **leia o conteúdo dela**. Se a ferramenta tiver acesso à internet ou ao `gh`, busque direto. Se não tiver, peça para a pessoa colar a descrição. +### ⚠️ Quando o número da issue não resolve + +Se a busca não encontrar a issue, ou se o número parecer um exemplo copiado da +documentação — `#123`, `#NÚMERO`, `#NUMBER`, `#000` —, **não insista na busca e +não peça a descrição como se fosse problema de acesso.** Diga com clareza o que +aconteceu: + +> Não encontrei a issue #123 no repositório. Esse número aparece como exemplo no +> guia de contribuição — se você copiou de lá, me diga o número real da issue que +> te atribuíram. +> +> Se ainda não tem uma tarefa atribuída, posso te ajudar a escolher. + +Isso poupa a pessoa de achar que ela errou alguma configuração. O caso mais comum +é justamente esse: alguém colando o exemplo da documentação literalmente. + +> 💡 O mesmo vale para qualquer valor de exemplo que apareça na documentação — +> `SEU-USUARIO`, `feat/nome-da-sua-feature`, ``. São espaços a preencher. +> Ao encontrar um deles literalmente, pergunte o valor real em vez de tentar usar. + > ⚠️ Várias issues deste projeto descrevem um estado que já mudou. Antes de > agir, **confira contra o código atual**. Se a issue disser que algo não existe > e você encontrar que existe, avise a pessoa. diff --git a/src/app/(private)/dashboard/team-projects/[id]/edit/page.tsx b/src/app/(private)/dashboard/team-projects/[id]/edit/page.tsx index dd060536..d2c3fe4d 100644 --- a/src/app/(private)/dashboard/team-projects/[id]/edit/page.tsx +++ b/src/app/(private)/dashboard/team-projects/[id]/edit/page.tsx @@ -1,9 +1,11 @@ import { ProjectForm } from '@/components/Dashboard/TeamProject'; -export default function ProjectEditPage({ +export default async function ProjectEditPage({ params, }: { - params: { id: string }; + params: Promise<{ id: string }>; }) { - return ; + const { id } = await params; + + return ; } diff --git a/src/app/(private)/dashboard/team-projects/[id]/page.tsx b/src/app/(private)/dashboard/team-projects/[id]/page.tsx index 29568270..7dbe723a 100644 --- a/src/app/(private)/dashboard/team-projects/[id]/page.tsx +++ b/src/app/(private)/dashboard/team-projects/[id]/page.tsx @@ -1,9 +1,11 @@ import { ProjectDetails } from '@/components/Dashboard/TeamProject'; -export default function ProjectDetailsPage({ +export default async function ProjectDetailsPage({ params, }: { - params: { id: string }; + params: Promise<{ id: string }>; }) { - return ; + const { id } = await params; + + return ; } diff --git a/src/app/api/feedback/[id]/route.ts b/src/app/api/feedback/[id]/route.ts index a0761095..fda0d6a6 100644 --- a/src/app/api/feedback/[id]/route.ts +++ b/src/app/api/feedback/[id]/route.ts @@ -7,8 +7,9 @@ import { logger } from '@/lib/logger'; export async function GET( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); diff --git a/src/app/api/project-skill/[id]/route.ts b/src/app/api/project-skill/[id]/route.ts index 81d56173..1518597d 100644 --- a/src/app/api/project-skill/[id]/route.ts +++ b/src/app/api/project-skill/[id]/route.ts @@ -9,12 +9,13 @@ const idSchema = z.string().min(25, 'ID inválido').max(36, 'ID inválido'); export async function GET( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const auth = await checkAuth(); if (!auth.authorized) return auth.response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ success: false, @@ -54,8 +55,9 @@ export async function GET( export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const auth = await checkAuth(); if (!auth.authorized) return auth.response; diff --git a/src/app/api/project-stack/[id]/route.ts b/src/app/api/project-stack/[id]/route.ts index 436f0a35..e7b8f5d2 100644 --- a/src/app/api/project-stack/[id]/route.ts +++ b/src/app/api/project-stack/[id]/route.ts @@ -12,8 +12,9 @@ const updatePercentageSchema = z.object({ export async function GET( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const auth = await checkAuth(); if (!auth.authorized) return auth.response; @@ -52,8 +53,9 @@ export async function GET( export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const auth = await checkAuth(); if (!auth.authorized) return auth.response; @@ -121,8 +123,9 @@ export async function DELETE( export async function PATCH( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const auth = await checkAuth(); if (!auth.authorized) return auth.response; diff --git a/src/app/api/stack-taken/[id]/route.ts b/src/app/api/stack-taken/[id]/route.ts index f20665d5..924aa621 100644 --- a/src/app/api/stack-taken/[id]/route.ts +++ b/src/app/api/stack-taken/[id]/route.ts @@ -16,8 +16,9 @@ const idSchema = z.string().min(1, 'ID inválido.'); export async function GET( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); @@ -69,8 +70,9 @@ export async function GET( export async function PUT( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); @@ -133,8 +135,9 @@ export async function PUT( export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); diff --git a/src/app/api/team-project/[id]/route.ts b/src/app/api/team-project/[id]/route.ts index 465fea65..90cd9dcf 100644 --- a/src/app/api/team-project/[id]/route.ts +++ b/src/app/api/team-project/[id]/route.ts @@ -132,14 +132,15 @@ async function applyProjectStackChanges( export async function GET( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const { id } = context.params; + const { id } = params; const idParse = idSchema.safeParse(id); @@ -228,14 +229,15 @@ export async function GET( export async function PUT( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ @@ -296,7 +298,28 @@ export async function PUT( github, } = parse.data; + // ------------------------------------------------------------------ + // Bloqueio de edição estrutural — TEMPORARIAMENTE DESATIVADO + // + // A regra impedia alterar o projeto depois que alguém assumisse uma stack. + // Ela está desligada até a plataforma entrar em operação de fato, por dois + // motivos: + // + // 1. O escopo estava largo demais: `hasStructuralProjectChanges` considera + // nome, prazo e valor total como "estrutura", travando edições que não + // têm relação com a composição da equipe. + // + // 2. A comparação de prazo gera falso positivo. O formulário carrega a data + // como `deadline.split('T')[0]`, perdendo a hora; o banco guarda o + // timestamp completo. Os dois nunca coincidem, então abrir a tela e + // salvar sem mudar nada já disparava o bloqueio. + // + // Ao reativar, corrija os dois pontos antes. + // ------------------------------------------------------------------ + const BLOQUEAR_EDICAO_APOS_FORMACAO_DE_EQUIPE = false; + if ( + BLOQUEAR_EDICAO_APOS_FORMACAO_DE_EQUIPE && existing.stacksTaken.length > 0 && hasStructuralProjectChanges(existing, parse.data) ) { @@ -350,14 +373,15 @@ export async function PUT( export async function DELETE( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ diff --git a/src/app/api/user-admin/[id]/route.ts b/src/app/api/user-admin/[id]/route.ts index 0d2ca03b..9e34011c 100644 --- a/src/app/api/user-admin/[id]/route.ts +++ b/src/app/api/user-admin/[id]/route.ts @@ -21,13 +21,12 @@ const updateUserSchema = z.object({ export async function GET( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response } = await checkAuth({ requireAdmin: true }); if (!authorized) return response; - const params = await context.params; - const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ @@ -72,13 +71,12 @@ export async function GET( export async function PUT( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response } = await checkAuth({ requireAdmin: true }); if (!authorized) return response; - const params = await context.params; - const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ @@ -182,13 +180,12 @@ export async function PUT( export async function DELETE( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response } = await checkAuth({ requireAdmin: true }); if (!authorized) return response; - const params = await context.params; - const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ diff --git a/src/app/api/user-availability/[id]/route.ts b/src/app/api/user-availability/[id]/route.ts index 02deeeae..8aaa45a9 100644 --- a/src/app/api/user-availability/[id]/route.ts +++ b/src/app/api/user-availability/[id]/route.ts @@ -23,14 +23,15 @@ const userAvailabilityUpdateSchema = z.object({ export async function GET( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized) return response; - const { id } = context.params; + const { id } = params; const idParse = idSchema.safeParse(id); @@ -103,14 +104,15 @@ export async function GET( export async function PUT( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ @@ -205,15 +207,16 @@ export async function PUT( } export async function DELETE( - // request: NextRequest, - context: { params: { id: string } } + request: NextRequest, + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ diff --git a/src/app/api/user-certificate/[id]/route.ts b/src/app/api/user-certificate/[id]/route.ts index 0ede3f06..87902635 100644 --- a/src/app/api/user-certificate/[id]/route.ts +++ b/src/app/api/user-certificate/[id]/route.ts @@ -15,8 +15,9 @@ const updateSchema = z.object({ export async function GET( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); @@ -48,8 +49,9 @@ export async function GET( export async function PUT( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); @@ -102,8 +104,9 @@ export async function PUT( export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); diff --git a/src/app/api/user-skill/[id]/route.ts b/src/app/api/user-skill/[id]/route.ts index ea7566bd..15128a0e 100644 --- a/src/app/api/user-skill/[id]/route.ts +++ b/src/app/api/user-skill/[id]/route.ts @@ -49,8 +49,9 @@ export async function GET(request: NextRequest) { export async function DELETE( request: NextRequest, - { params }: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const auth = await checkAuth(); if (!auth.authorized) return auth.response; diff --git a/src/app/api/user/[id]/route.ts b/src/app/api/user/[id]/route.ts index 66c5aebf..aabefd9f 100644 --- a/src/app/api/user/[id]/route.ts +++ b/src/app/api/user/[id]/route.ts @@ -25,14 +25,15 @@ const updateUserSchema = z.object({ export async function GET( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ success: false, @@ -76,14 +77,15 @@ export async function GET( export async function PUT( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ success: false, @@ -185,14 +187,15 @@ export async function PUT( export async function DELETE( request: NextRequest, - context: { params: { id: string } } + context: { params: Promise<{ id: string }> } ) { + const params = await context.params; const { authorized, response, session } = await checkAuth({ allowedRoles: ROLE_GROUPS.ALL, }); if (!authorized || !session) return response; - const idParse = idSchema.safeParse(context.params.id); + const idParse = idSchema.safeParse(params.id); if (!idParse.success) { return buildResponse({ success: false, diff --git a/src/components/Dashboard/TeamProject/ProjectDetails.tsx b/src/components/Dashboard/TeamProject/ProjectDetails.tsx index b461aa4e..0bf975a3 100644 --- a/src/components/Dashboard/TeamProject/ProjectDetails.tsx +++ b/src/components/Dashboard/TeamProject/ProjectDetails.tsx @@ -66,6 +66,24 @@ export function ProjectDetails({ projectId }: Readonly<{ projectId: string }>) { } }; + const handleCompleteProject = async () => { + try { + const response = await apiTryCatch.patch('/team-project/user', { + id: project.id, + status: 'CONCLUIDO', + }); + toast.success( + response?.data?.message || 'Projeto marcado como concluído!' + ); + fetchDetailsProject(); + } catch { + toast.error('Erro ao concluir projeto'); + } + }; + + const canComplete = + user?.id === project.owner.id && project.status === 'EM_ANDAMENTO'; + return (
@@ -116,6 +134,11 @@ export function ProjectDetails({ projectId }: Readonly<{ projectId: string }>) { Editar )} + {canComplete && ( + + )}
diff --git a/src/components/Dashboard/TeamProject/ProjectForm.tsx b/src/components/Dashboard/TeamProject/ProjectForm.tsx index 3aecea90..92f65cca 100644 --- a/src/components/Dashboard/TeamProject/ProjectForm.tsx +++ b/src/components/Dashboard/TeamProject/ProjectForm.tsx @@ -3,7 +3,7 @@ import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import type { ProjectDetailsType } from '@/types/interface/team-project'; -import { useForm, FormProvider } from 'react-hook-form'; +import { useForm, FormProvider, type FieldErrors } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; @@ -103,8 +103,6 @@ export function ProjectForm({ editIdProp }: { editIdProp?: string } = {}) { stacks: data.stacks, }; - console.log('Payload enviado:', payload); - let res: Response; if (editId) { res = await fetch(`/api/team-project/${editId}`, { @@ -121,11 +119,27 @@ export function ProjectForm({ editIdProp }: { editIdProp?: string } = {}) { } if (!res.ok) { - const error = await res.json().catch(() => null); - console.error('Erro da API:', error); - throw new Error( - editId ? 'Erro ao atualizar projeto' : 'Erro ao criar projeto' - ); + // A API responde no formato { success, message, data, errors }. + // Aproveitamos a mensagem dela em vez de mostrar um erro genérico — + // quase sempre ela explica exatamente o que impediu o salvamento. + const corpo = await res.text(); + let mensagem = editId + ? 'Erro ao atualizar projeto' + : 'Erro ao criar projeto'; + + try { + const dados = JSON.parse(corpo); + const detalhe = Array.isArray(dados?.errors) + ? dados.errors.join(' · ') + : ''; + mensagem = + [dados?.message, detalhe].filter(Boolean).join(' — ') || mensagem; + } catch { + // resposta sem JSON — mantém a mensagem padrão + } + + console.error(`Erro da API (${res.status}):`, corpo); + throw new Error(mensagem); } router.push( @@ -135,7 +149,11 @@ export function ProjectForm({ editIdProp }: { editIdProp?: string } = {}) { ); } catch (err) { console.error(err); - toast.error('Erro ao salvar projeto'); + toast.error( + err instanceof Error && err.message + ? err.message + : 'Erro ao salvar projeto' + ); } }; @@ -176,10 +194,39 @@ export function ProjectForm({ editIdProp }: { editIdProp?: string } = {}) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [editId]); + // Sem isto, uma validação que falha num campo fora da área visível não dá + // retorno nenhum: a pessoa clica em "Salvar" e nada acontece. + const onInvalid = (validationErrors: FieldErrors) => { + const campos: Record = { + name: 'Nome', + description: 'Descrição', + deadline: 'Prazo', + totalValue: 'Valor total', + github: 'GitHub', + stacks: 'Stacks', + skills: 'Skills', + }; + + const pendencias = Object.entries(validationErrors).map(([campo, erro]) => { + const rotulo = campos[campo] ?? campo; + const mensagem = + erro && typeof erro === 'object' && 'message' in erro + ? String(erro.message ?? '') + : ''; + return mensagem ? `${rotulo}: ${mensagem}` : rotulo; + }); + + toast.error( + pendencias.length === 1 + ? pendencias[0] + : `Verifique os campos: ${pendencias.join(' · ')}` + ); + }; + return (
{/* Coluna Esquerda */} diff --git a/src/tests/unit/api/team-project/team-project-id-route.test.ts b/src/tests/unit/api/team-project/team-project-id-route.test.ts index ca47e4de..f41d1908 100644 --- a/src/tests/unit/api/team-project/team-project-id-route.test.ts +++ b/src/tests/unit/api/team-project/team-project-id-route.test.ts @@ -83,7 +83,7 @@ const createRequest = (body: unknown) => }) as NextRequest; const createContext = () => ({ - params: { id: projectId }, + params: Promise.resolve({ id: projectId }), }); const authorizeOwner = () => { @@ -148,7 +148,14 @@ describe('PUT /api/team-project/[id]', () => { }); }); - it('bloqueia alteração de name após formação da equipe', async () => { + // Estes três testes cobrem o bloqueio de edição estrutural após a formação da + // equipe, que está TEMPORARIAMENTE DESATIVADO na rota — veja a constante + // BLOQUEAR_EDICAO_APOS_FORMACAO_DE_EQUIPE em + // src/app/api/team-project/[id]/route.ts. + // + // Ficam como `skip` de propósito: descrevem o comportamento esperado quando a + // regra voltar, e servem de ponto de partida para quem for reativá-la. + it.skip('bloqueia alteração de name após formação da equipe', async () => { (prisma.project.findUnique as jest.Mock).mockResolvedValue( existingProject({ stacksTaken: [{ id: 'stack-taken-1' }], @@ -172,7 +179,7 @@ describe('PUT /api/team-project/[id]', () => { expect(prisma.project.update).not.toHaveBeenCalled(); }); - it('bloqueia alteração de skills após formação da equipe', async () => { + it.skip('bloqueia alteração de skills após formação da equipe', async () => { (prisma.project.findUnique as jest.Mock).mockResolvedValue( existingProject({ stacksTaken: [{ id: 'stack-taken-1' }], @@ -188,7 +195,7 @@ describe('PUT /api/team-project/[id]', () => { expect(prisma.project.update).not.toHaveBeenCalled(); }); - it('bloqueia alteração de stacks após formação da equipe', async () => { + it.skip('bloqueia alteração de stacks após formação da equipe', async () => { (prisma.project.findUnique as jest.Mock).mockResolvedValue( existingProject({ stacksTaken: [{ id: 'stack-taken-1' }], diff --git a/src/tests/unit/api/user/user-id-route.test.ts b/src/tests/unit/api/user/user-id-route.test.ts index 5745880a..a2519c33 100644 --- a/src/tests/unit/api/user/user-id-route.test.ts +++ b/src/tests/unit/api/user/user-id-route.test.ts @@ -54,7 +54,7 @@ const createRequest = (body?: unknown) => }) as MockRequest as NextRequest; const createContext = (id = userId) => ({ - params: { id }, + params: Promise.resolve({ id }), }); const authorizeUser = (id = userId, role = 'USER') => {