feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat: adicionado kpi-model - #37

Merged
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões
Aug 25, 2025

Hidden character warning

The head ref may contain hidden characters: "34-adicionar-caixa-de-sugest\u00f5es"
Merged

feat: adicionado kpi-model#37
rbxyz merged 1 commit into
mainfrom
34-adicionar-caixa-de-sugestões

Conversation

@rbxyz

@rbxyzrbxyz commented Aug 25, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Admins can manage KPIs for suggestions via a modal: search, create, select, and link/unlink KPIs. KPIs are displayed across suggestion views.
    • Suggestion submission now auto-fills your name and sector from your profile, showing the name as read-only with clearer visibility toggles.
  • Style

    • Updated the Suggestions card icon in the Admin area and made minor spacing adjustments.

@coderabbitai

coderabbitaiBot commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds relational KPI support: new Prisma models Kpi and SuggestionKpi; expands ClassificationType enum. Introduces TRPC kpi router with list/search/create/update/delete/link/unlink/getBySuggestionId. Wires KPI management into admin suggestions UI with a new KpiManagementModal and per-suggestion KPI fetching. Minor admin UI tweaks (icon change, suggestion card name/sector handling). Adds kpi route to API root.

Changes

Cohort / File(s)Summary
Prisma schema & relations
prisma/schema.prisma
Adds models Kpi and SuggestionKpi (many-to-many with Suggestion) with cascade relations, indexes, and uniqueness. Adds Suggestion.kpiLinks. Extends ClassificationType with CAPACITY and EFFORT.
API: KPI router
src/server/api/routers/kpi.ts
New TRPC router exposing listActive, search, create, update, delete (soft), getBySuggestionId, linkToSuggestion (replace links), unlinkFromSuggestion, with admin access and Zod validation.
API: root wiring
src/server/api/root.ts
Registers kpiRouter under appRouter.kpi.
Admin suggestions UI & flow
src/app/(authenticated)/admin/suggestions/page.tsx
Integrates KPI management: per-suggestion KPI fetching, state threading, modal orchestration, UI refactor to SuggestionItem, and refresh logic.
KPI management modal
src/components/admin/suggestion/kpi-management-modal.tsx
New component to search/create/select KPIs, link to suggestion, and delete KPIs; includes toasts and selection UX.
Suggestion submission card
src/components/admin/suggestion/suggestion-card.tsx
Makes submitted name read-only and auto-filled; adjusts effects and toggles; minor layout changes.
Admin dashboard icon
src/app/(authenticated)/admin/page.tsx
Changes Suggestions card icon from Utensils to Lightbulb.

Sequence Diagram(s)

sequenceDiagram
autonumber
actor Admin as Admin User
participant Page as Admin Suggestions Page
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Page: Open Suggestions
Page->>API: getBySuggestionId(suggestionId)
API->>DB: Query SuggestionKpi → Kpi (ordered)
DB-->>API: KPI list
API-->>Page: KPI list
Admin->>Page: Click "Gerenciar KPIs"
Page->>Modal: Open with selectedKpiIds
alt Searching KPIs
Modal->>API: search(query)
API->>DB: Find active KPIs (ilike)
DB-->>API: Results
API-->>Modal: Results
else Load active
Modal->>API: listActive()
API->>DB: Find active KPIs (ordered)
DB-->>API: KPI list
API-->>Modal: KPI list
end
Admin->>Modal: Toggle selections
opt Create KPI
Admin->>Modal: Enter name/desc, Create
Modal->>API: create({name, description})
API->>DB: Insert KPI (unique name)
DB-->>API: KPI
API-->>Modal: KPI
Modal->>API: listActive() (refetch)
end
Admin->>Modal: Save seleção
Modal->>API: linkToSuggestion({suggestionId, kpiIds})
API->>DB: Delete existing links
API->>DB: Create new links (batch)
DB-->>API: OK
API-->>Modal: {success:true}
Modal-->>Page: Close
Page->>API: getBySuggestionId(suggestionId) (refresh)
API-->>Page: KPI list (updated)
Loading
sequenceDiagram
autonumber
actor Admin as Admin User
participant Modal as KpiManagementModal
participant API as TRPC kpiRouter
participant DB as Prisma/DB
Admin->>Modal: Delete KPI
Modal->>API: delete({id})
API->>DB: Update KPI isActive=false
DB-->>API: OK
API-->>Modal: OK
Modal->>Modal: Remove from selection
Modal->>API: listActive() (refetch)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

  • 34 adicionar caixa de sugestões #36 — Earlier schema and admin suggestion UI changes; this PR builds on Suggestion/Classification structures and moves KPIs to dedicated models and API.

Poem

In burrows of code I hop with glee,
New KPIs sprout like clover free.
I link, I list, I softly delete—
A modal pops, selections complete.
With lightbulb bright above my nest,
I thump “merged!”—our metrics dressed. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 34-adicionar-caixa-de-sugestões

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@vercel

vercelBot commented Aug 25, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentPreviewCommentsUpdated (UTC)
eloReadyReadyPreviewCommentAug 25, 2025 2:10pm

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(authenticated)/admin/suggestions/page.tsx (1)

250-256: Bug: openClassificationModal ignora o tipo solicitado

Você sempre define type: 'impact', mesmo quando o usuário clica em Capacidade/Esforço. Isso faz o modal abrir na aba errada.

Aplique este diff:

- const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {- console.log('openKpiModal called with suggestionId:', suggestionId)- setSelectedSuggestionId(suggestionId)- // Os KPIs serão carregados automaticamente pela query quando selectedSuggestionId mudar- setKpiModalOpen(true)- }+ const openClassificationModal = (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => {+ setClassificationModal({+ isOpen: true,+ suggestionId,+ type+ })+ }

Observação: o openKpiModal permanece separado e focado em KPIs; este ajuste apenas corrige a abertura do modal de classificação.

🧹 Nitpick comments (19)
src/components/admin/suggestion/suggestion-card.tsx (1)

36-41: Setor enviado ignora o estado local; remova submittedSector para evitar fonte duplicada de verdade

Você preenche e mantém submittedSector, mas o payload usa sempre userData?.setor, e a UI também renderiza a partir de userSector. O estado submittedSector não tem efeito prático e adiciona complexidade desnecessária. Simplifique eliminando-o e a lógica associada no toggle do setor.

Aplicar este diff concentrado:

@@
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- const [submittedSector, setSubmittedSector] = useState("")
@@
- setSubmittedSector(userData.setor ?? "")
@@
- submittedSector: hideSector ? undefined : userData?.setor ?? undefined,+ submittedSector: hideSector ? undefined : userData?.setor ?? undefined,

E no onCheckedChange do setor (veja comentário abaixo) remova as atribuições ao estado removido.

Also applies to: 90-92

prisma/schema.prisma (3)

465-481: Unicidade de Kpi.name pode precisar ser case-insensitive

Hoje o schema garante unicidade case-sensitive em Postgres. Seu backend faz buscas case-insensitive para listagem, mas as validações de create/update usam equivalência direta (vide kpiRouter). Se o negócio exigir unicidade sem diferenciar caixa, considere:

  • Banco: usar @db.Citext em name ou criar unique index em lower(name).
  • App: reforçar validação com where: { name: { equals: input.name, mode: "insensitive" } } no create/update.

Posso preparar a migration e ajustes no router, se quiser.


482-497: Tabela de junção está correta; considere mapear nomes de tabela opcionalmente

@@unique([suggestionId, kpiId]) e onDelete: Cascade estão perfeitos. Se desejarem nomenclatura de tabela específica no DB, adicionem @@map("suggestions_kpis") (opcional, apenas para consistência de naming).


441-447: Risco de duas fontes de verdade para KPIs

O campo kpis Json? permanece em Suggestion ao mesmo tempo em que o m:m foi introduzido. Isso pode divergir com o tempo. Se não houver mais leitura/escrita neste JSON, planeje deprecar/remover e criar uma migration de dados para popular SuggestionKpi a partir do JSON legado.

Posso fornecer um script Prisma para migrar os dados e limpar o campo.

src/app/(authenticated)/admin/suggestions/page.tsx (6)

168-173: Remover logs de debug ou proteger por flag de ambiente

Há vários console.log espalhados (abertura do modal, carregamento de KPIs, fechamento do modal). Isso polui o console em produção.

Sugestão: remova-os ou encapsule em if (process.env.NODE_ENV !== 'production') console.log(...).

- console.log('openKpiModal called with suggestionId:', suggestionId)
@@
- console.log('Frontend: KPIs loaded for suggestion:', selectedSuggestionId, currentSuggestionKpis)
@@
- console.log('Frontend: Setting selected KPI IDs:', kpiIds)
@@
- console.log('Frontend: No KPIs data or invalid format')
@@
- console.log('Modal closed, reloading suggestion data...')

Also applies to: 199-209, 564-569


175-196: Tipagem fraca para kpiQuery.data

const kpiData = kpiQuery.data as unknown mascara problemas de tipo. Tipar corretamente melhora DX e evita checks redundantes.

Aplicar:

- const kpiQuery = api.kpi.getBySuggestionId.useQuery(+ const kpiQuery = api.kpi.getBySuggestionId.useQuery(
{ suggestionId: selectedSuggestionId ?? "" },
{
enabled: !!selectedSuggestionId,
}
)
- const kpiData = kpiQuery.data as unknown- const kpiError = kpiQuery.error+ const kpiData = kpiQuery.data as { id: string; name: string; description?: string | null }[] | undefined+ const kpiError = kpiQuery.error
const isLoadingKpis = kpiQuery.isLoading
@@
- const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {- if (kpiError) {+ const currentSuggestionKpis = useMemo((): { id: string; name: string; description?: string | null }[] => {+ if (kpiError) {
console.error('Error loading KPIs:', kpiError)
return []
}
- if (Array.isArray(kpiData)) {- return kpiData as { id: string; name: string; description?: string | null }[]- }- return []+ return Array.isArray(kpiData) ? kpiData : []
}, [kpiData, kpiError])

329-337: Inconsistência de rótulo: "Ajustes e incubar" vs. "Ajustar"

O priorityOrder inclui "Ajustes e incubar", mas os demais pontos do código usam "Ajustar". Alinhe a nomenclatura para evitar confusão em sorting e filtros.

- "Ajustes e incubar": 5,+ "Ajustar": 5,

E certifique-se de que quaisquer lugares que exibem esse rótulo usem exatamente o mesmo texto.


247-248: kpiPool não é utilizado

kpiPool é sempre [] e não alimenta a UI. Pode ser removido junto com as props associadas para simplificar.


617-669: Sequenciamento de atualização e notificação

Você muda status e depois dispara notificação por outra mutação, com um pequeno tempo de espera (sleep) embutido. Melhor concentrar essa operação em uma única mutação transacional no backend (atualiza status + envia email) para garantir consistência e simplificar o frontend.

Posso preparar uma mutação rejectWithReasonAndNotify no router de sugestões que faça ambos os passos de forma atômica.

Also applies to: 973-985, 990-1012


507-521: Remove redundant currentSuggestionKpis prop

currentSuggestionKpis is never consumed inside IdeasAccordion (it’s disabled via ESLint) and each SuggestionItem queries its own KPIs. You can safely remove this prop entirely.

Locations to update:

  • In src/app/(authenticated)/admin/suggestions/page.tsx, mobile view IdeasAccordion (around lines 508–516): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the same file, desktop view IdeasAccordion (around lines 525–533): drop currentSuggestionKpis={currentSuggestionKpis}.
  • In the IdeasAccordion signature/type (around lines 579–586): remove the destructured currentSuggestionKpis and its type, and delete the corresponding // eslint-disable-next-line @typescript-eslint/no-unused-vars comment.

Suggested diff:

--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -512,7 +512,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
@@ -532,7 +532,6 @@
kpiPool={kpiPool}
- currentSuggestionKpis={currentSuggestionKpis}
update={update}
currentUser={currentUser}
onOpenClassificationModal={openClassificationModal}
--- a/src/app/(authenticated)/admin/suggestions/page.tsx@@ -578,13 +578,10 @@
function IdeasAccordion({
sugestoes,
impactPool,
capacityPool,
effortPool,
kpiPool,
- // eslint-disable-next-line @typescript-eslint/no-unused-vars- currentSuggestionKpis,
update,
currentUser,
onOpenClassificationModal,
onOpenKpiModal,
getStatusFromScore,
}: {
sugestoes: SuggestionLocal[]
impactPool: ClassItem[]
capacityPool: ClassItem[]
effortPool: ClassItem[]
kpiPool: string[]
- currentSuggestionKpis: { id: string; name: string; description?: string | null }[]
update: (id: string, updates: Partial<SuggestionLocal>) => void
currentUser: RouterOutputs["user"]["me"] | undefined
onOpenClassificationModal: (suggestionId: string, type: 'impact' | 'capacity' | 'effort') => void
onOpenKpiModal: (suggestionId: string) => void
getStatusFromScore: (suggestion: SuggestionLocal) => string
})
src/components/admin/suggestion/kpi-management-modal.tsx (5)

86-93: Close the modal on success and invalidate per-suggestion KPI cache.

Ensure the UI reflects the latest links after save, and only then close the modal.

- const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({- onSuccess: () => {- toast.success("KPIs vinculados com sucesso!")- },- onError: (error) => {- toast.error(error.message)- }- })+ const linkToSuggestion = api.kpi.linkToSuggestion.useMutation({+ onSuccess: async () => {+ toast.success("KPIs vinculados com sucesso!")+ if (suggestionId) {+ await utils.kpi.getBySuggestionId.invalidate({ suggestionId })+ }+ onOpenChange(false)+ },+ onError: (error) => {+ toast.error(error.message)+ }+ })

Add this outside the selected range to support invalidation:

// near the other hooks/stateconstutils=api.useUtils()

49-61: Keep search results in sync after create/delete.

When a search is active, refetch the search query so the list reflects the mutation outcome.

 const createKpi = api.kpi.create.useMutation({
onSuccess: () => {
toast.success("KPI criado com sucesso!")
setNewKpiName("")
setNewKpiDescription("")
setIsCreatingNew(false)
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
},
 const deleteKpi = api.kpi.delete.useMutation({
onSuccess: (_, variables) => {
toast.success("KPI removido com sucesso!")
- void refetchKpis()+ void refetchKpis()+ if (searchQuery.length > 0) {+ void searchQuery_.refetch()+ }
// Remove da seleção se estiver selecionado
onKpiSelectionChange(selectedKpiIds.filter(id => id !== variables.id))
},

Also applies to: 74-84


266-271: Add accessible labels to icon-only buttons (X/Edit/Delete).

Improves a11y and UX with tooltips for icon-only actions.

- <button+ <button
onClick={() => handleKpiToggle(kpiId)}
className="ml-1 hover:bg-destructive/20 rounded-full p-0.5"
+ aria-label={`Remover ${kpi.name}`}+ title={`Remover ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
// TODO: Implementar edição inline
toast.info("Funcionalidade de edição será implementada em breve")
}}
+ aria-label={`Editar ${kpi.name}`}+ title={`Editar ${kpi.name}`}
>
- <Button+ <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
if (confirm(`Tem certeza que deseja remover o KPI "${kpi.name}"?`)) {
deleteKpi.mutate({ id: kpi.id })
}
}}
+ aria-label={`Excluir ${kpi.name}`}+ title={`Excluir ${kpi.name}`}
>

Also applies to: 334-356


41-44: Debounce the search to avoid request bursts while typing.

Reduce server chatter and flicker with a small debounce.

Example:

// add once (utils or inside this file)functionuseDebounce<T>(value: T,delay=200){const[v,setV]=useState(value)useEffect(()=>{constid=setTimeout(()=>setV(value),delay)return()=>clearTimeout(id)},[value,delay])returnv}// use itconstdebouncedQuery=useDebounce(searchQuery,250)constsearchQuery_=api.kpi.search.useQuery({query: debouncedQuery},{enabled: debouncedQuery.length>0})

63-72: Remove the unused update mutation or implement edit to avoid disabling lint globally.

Keeping dead code plus an eslint-disable is noisy. Either wire inline edit or drop the mutation for now.

-// eslint-disable-next-line @typescript-eslint/no-unused-vars-const updateKpi = api.kpi.update.useMutation({- onSuccess: () => {- toast.success("KPI atualizado com sucesso!")- void refetchKpis()- },- onError: (error) => {- toast.error(error.message)- }-})+// TODO: adicionar edição inline e reintroduzir update quando implementado
src/server/api/routers/kpi.ts (4)

73-75: Return proper RPC errors on duplicate names (409/CONFLICT).

Use TRPCError so clients can handle conflict states explicitly.

- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }
- if (existingKpi) {- throw new Error("Já existe um KPI com este nome")- }+ if (existingKpi) {+ throw new TRPCError({ code: "CONFLICT", message: "Já existe um KPI com este nome" })+ }

Also applies to: 109-111


62-66: Trim inputs server-side to avoid “space-only” names/descriptions.

Prevent subtle duplicates and validation gaps by trimming in Zod.

- .input(z.object({- name: z.string().min(1).max(100),- description: z.string().max(500).optional(),- order: z.number().int().default(0),- }))+ .input(z.object({+ name: z.string().trim().min(1).max(100),+ description: z.string().trim().max(500).optional(),+ order: z.number().int().default(0),+ }))
- .input(z.object({- id: z.string(),- name: z.string().min(1).max(100).optional(),- description: z.string().max(500).optional(),- isActive: z.boolean().optional(),- order: z.number().int().optional(),- }))+ .input(z.object({+ id: z.string(),+ name: z.string().trim().min(1).max(100).optional(),+ description: z.string().trim().max(500).optional(),+ isActive: z.boolean().optional(),+ order: z.number().int().optional(),+ }))

Also applies to: 90-96


186-193: Optionally return the number of unlinked records.

Helps the client confirm what changed.

- .mutation(async ({ ctx, input }) => {- await ctx.db.suggestionKpi.deleteMany({+ .mutation(async ({ ctx, input }) => {+ const result = await ctx.db.suggestionKpi.deleteMany({
where: {
suggestionId: input.suggestionId,
kpiId: { in: input.kpiIds },
},
})
+ return { count: result.count }
}),

1-5: Avoid blanket ESLint disables.

They hide real issues. Narrow the disables to specific lines or fix types where feasible.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb19d0e and 96fc7ae.

📒 Files selected for processing (7)
  • prisma/schema.prisma (1 hunks)
  • src/app/(authenticated)/admin/page.tsx (2 hunks)
  • src/app/(authenticated)/admin/suggestions/page.tsx (13 hunks)
  • src/components/admin/suggestion/kpi-management-modal.tsx (1 hunks)
  • src/components/admin/suggestion/suggestion-card.tsx (3 hunks)
  • src/server/api/root.ts (2 hunks)
  • src/server/api/routers/kpi.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/server/api/root.ts (1)
src/server/api/routers/kpi.ts (1)
  • kpiRouter (10-194)
src/server/api/routers/kpi.ts (1)
src/server/api/trpc.ts (2)
  • createTRPCRouter (80-80)
  • adminProcedure (126-141)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
src/trpc/react.tsx (1)
  • api (23-23)
src/app/(authenticated)/admin/suggestions/page.tsx (2)
src/trpc/react.tsx (2)
  • api (23-23)
  • RouterOutputs (37-37)
src/components/admin/suggestion/kpi-management-modal.tsx (1)
  • KpiManagementModal (23-388)
src/components/admin/suggestion/suggestion-card.tsx (2)
src/components/ui/card.tsx (1)
  • Card (79-79)
src/components/ui/label.tsx (1)
  • Label (26-26)
🔇 Additional comments (8)
src/components/admin/suggestion/suggestion-card.tsx (2)

36-41: Pré-preenchimento do nome está ok

A composição do nome com filtro de valores falsy e fallback para email está correta e simples.


114-114: Ajuste de layout (mt-4) é bem-vindo

O espaçamento superior melhora a separação visual da seção. Sem impactos colaterais.

src/app/(authenticated)/admin/page.tsx (1)

2-2: Troca de ícone para Lightbulb

A mudança do ícone de Sugestões para Lightbulb está consistente com a temática do recurso e não altera comportamento.

Also applies to: 55-55

prisma/schema.prisma (1)

455-457: Ligação m:m via campo kpiLinks é adequada

Adicionar kpiLinks SuggestionKpi[] em Suggestion estabelece corretamente o lado inverso do relacionamento com a tabela de junção. Boa escolha pelo relacionamento explícito (join table).

src/server/api/root.ts (1)

24-24: Exposição do kpiRouter no appRouter

Import e registro do kpi no roteador principal estão corretos. AppRouter refletirá o novo sub-roteador.

Also applies to: 54-55

src/components/admin/suggestion/kpi-management-modal.tsx (1)

256-275: Selected KPIs not present in allKpis won’t render (e.g., inactive KPIs).

Badges derive details from listActive; if getBySuggestionId returns inactive KPIs, badges won’t show and users can’t deselect them here. Either ensure the server returns only active KPIs for a suggestion, or fetch details for missing selected IDs on the client.

Do you want to filter inactive KPIs in getBySuggestionId on the server? I proposed a server-side fix in kpi.ts to avoid this inconsistency.

src/server/api/routers/kpi.ts (2)

19-23: Double-check the relation name used in _count.select.

_count.select.suggestions assumes a relation field “suggestions” on Kpi. Validate it matches the Prisma schema (could be “kpiLinks” or similar).

If it differs, adjust include/_count accordingly to avoid runtime errors.


69-71: Kpi.name uniqueness confirmed

The Prisma schema already declares name String @unique on the Kpi model (schema.prisma, line 468), so using findUnique by name is valid. No changes are needed here.

Comment on lines +557 to +574
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open) => {
setKpiModalOpen(open)
if (!open) {
// Recarregar dados da sugestão quando o modal for fechado
if (selectedSuggestionId) {
console.log('Modal closed, reloading suggestion data...')
void refetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fluxo de salvar KPIs não limpa todos os vínculos (não é possível salvar seleção vazia)

O KpiManagementModal (ver snippet relevante) só chama linkToSuggestion quando selectedKpiIds.length > 0. Se quiser remover todos os KPIs de uma sugestão, nenhuma chamada é feita e os vínculos permanecem. O backend está preparado para sobrescrever (apaga e recria), então deve aceitar array vazio.

Ajuste recomendado no modal (arquivo src/components/admin/suggestion/kpi-management-modal.tsx):

- if (suggestionId && selectedKpiIds.length > 0) {+ if (suggestionId) {
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })
}

Isso permitirá limpar todos os KPIs (enviando [], o router já executa deleteMany).

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<KpiManagementModal
isOpen={kpiModalOpen}
onOpenChange={(open)=>{
setKpiModalOpen(open)
if(!open){
// Recarregar dados da sugestão quando o modal for fechado
if(selectedSuggestionId){
console.log('Modal closed, reloading suggestion data...')
voidrefetch()
}
setSelectedSuggestionId(null)
setSelectedKpiIds([])
}
}}
selectedKpiIds={selectedKpiIds}
onKpiSelectionChange={setSelectedKpiIds}
suggestionId={selectedSuggestionId ?? undefined}
/>
// File: src/components/admin/suggestion/kpi-management-modal.tsx
// — inside the save/submit handler where KPIs are linked to a suggestion —
if(suggestionId){
linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
-}else{
-console.log('Skipping linkToSuggestion - missing data:',{
-suggestionId: !!suggestionId,
-selectedKpiIdsLength: selectedKpiIds.length,
-})
}
🤖 Prompt for AI Agents
In src/app/(authenticated)/admin/suggestions/page.tsx around lines 557 to 574,
the modal close handler only triggers linking when selectedKpiIds.length > 0
which prevents removing all KPI links; always call the function that persists
KPI links (e.g., linkToSuggestion or the prop handler that triggers the router
action) even when selectedKpiIds is an empty array so the backend can overwrite
links with an empty list; remove the conditional that skips the call on empty
selection (or explicitly pass [] to the same save function), ensure suggestionId
is passed through, and keep clearing local state (setSelectedSuggestionId(null),
setSelectedKpiIds([])) after the save completes or after refetch.

Comment on lines +128 to +152
const handleSaveSelection = () => {
console.log('handleSaveSelection called', {
suggestionId,
selectedKpiIds,
hasLinkToSuggestion: !!linkToSuggestion
})

if (suggestionId && selectedKpiIds.length > 0) {
console.log('Calling linkToSuggestion with:', {
suggestionId,
kpiIds: selectedKpiIds,
})

linkToSuggestion.mutate({
suggestionId,
kpiIds: selectedKpiIds,
})
} else {
console.log('Skipping linkToSuggestion - missing data:', {
suggestionId: !!suggestionId,
selectedKpiIdsLength: selectedKpiIds.length
})
}
onOpenChange(false)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t close the modal before the link mutation completes; also drop debug logs.

Closing immediately can hide failures and lose context. Let the modal close only after a successful link (or close immediately only when there’s nothing to link). Remove console logs in production code.

- const handleSaveSelection = () => {- console.log('handleSaveSelection called', {- suggestionId,- selectedKpiIds,- hasLinkToSuggestion: !!linkToSuggestion- })-- if (suggestionId && selectedKpiIds.length > 0) {- console.log('Calling linkToSuggestion with:', {- suggestionId,- kpiIds: selectedKpiIds,- })-- linkToSuggestion.mutate({- suggestionId,- kpiIds: selectedKpiIds,- })- } else {- console.log('Skipping linkToSuggestion - missing data:', {- suggestionId: !!suggestionId,- selectedKpiIdsLength: selectedKpiIds.length- })- }- onOpenChange(false)- }+ const handleSaveSelection = () => {+ if (suggestionId && selectedKpiIds.length > 0) {+ linkToSuggestion.mutate({+ suggestionId,+ kpiIds: selectedKpiIds,+ })+ } else {+ onOpenChange(false)+ }+ }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +135 to 146
<Label>Nome do colaborador</Label>
{!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
<span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>
</div>
)}
{hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/50">
<span className="text-sm text-muted-foreground italic">Nome será ocultado na sugestão</span>
</div>
)}
<div className="flex items-center space-x-2">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fallback do nome incorreto quando string vazia; e toggle de setor contém código inócuo

  • O display usa ?? para submittedName, mas o estado inicial é "" (string vazia). Com ?? o fallback não é aplicado e a UI pode exibir vazio até o useEffect rodar. Use || ou trim() com ||.
  • O bloco do toggle de setor atualiza submittedSector, porém esse estado não é consumido na UI nem no submit (ver comentário anterior). Remova o código morto.

Aplicar este diff no display do nome e limpar o toggle:

@@
- {!hideName && (+ {!hideName && (
<div className="flex items-center gap-2 p-3 border rounded-md bg-muted/30">
- <span className="text-sm font-medium">{submittedName ?? userData?.email ?? "Nome não disponível"}</span>+ <span className="text-sm font-medium">+ {submittedName.trim() || userData?.email || "Nome não disponível"}+ </span>
</div>
)}
@@
- onCheckedChange={(checked) => {- setHideSector(checked as boolean)- if (checked) {- setSubmittedSector("")- } else if (userData) {- setSubmittedSector(userData.setor ?? "")- }- }}+ onCheckedChange={(checked) => {+ setHideSector(checked as boolean)+ }}

Also applies to: 169-179

🤖 Prompt for AI Agents
In src/components/admin/suggestion/suggestion-card.tsx around lines 135-146 (and
also apply same fix at 169-179), the name fallback uses the nullish coalescing
operator (??) so an empty string shows as blank; replace with a truthy-check
that trims the value before falling back (e.g., use (submittedName &&
submittedName.trim()) || userData?.email || "Nome não disponível") so empty or
whitespace-only strings fall back immediately. Also remove the dead
toggle-sector code that only updates submittedSector but is never used in the UI
or submit path—delete the toggle block and any submittedSector state updates
that aren’t consumed to keep the component clean.

Comment on lines +139 to +150
const suggestionKpis = await ctx.db.suggestionKpi.findMany({
where: { suggestionId: input.suggestionId },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})

return suggestionKpis.map((sk) => sk.kpi)
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getBySuggestionId should exclude inactive KPIs to keep UI consistent.

Inactive KPIs currently leak into selections and won’t render in the modal list (which shows only active KPIs).

- const suggestionKpis = await ctx.db.suggestionKpi.findMany({- where: { suggestionId: input.suggestionId },+ const suggestionKpis = await ctx.db.suggestionKpi.findMany({+ where: { + suggestionId: input.suggestionId,+ kpi: { isActive: true },+ },
include: {
kpi: true,
},
orderBy: {
kpi: { order: "asc" },
},
})
📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {suggestionId: input.suggestionId},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
constsuggestionKpis=awaitctx.db.suggestionKpi.findMany({
where: {
suggestionId: input.suggestionId,
kpi: {isActive: true},
},
include: {
kpi: true,
},
orderBy: {
kpi: {order: "asc"},
},
})
returnsuggestionKpis.map((sk)=>sk.kpi)
}),
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 139 to 150, the getBySuggestionId
query returns KPIs regardless of active state so inactive KPIs appear in
selections; update the query to only return active KPIs by adding a condition to
the findMany (e.g. constrain by kpi.active === true via the where clause or
filter the result array by sk.kpi.active) and keep the existing include and
orderBy so only active KPIs are mapped and returned.

Comment on lines +158 to +178
.mutation(async ({ ctx, input }) => {
try {
await ctx.db.suggestionKpi.deleteMany({
where: { suggestionId: input.suggestionId },
})

if (input.kpiIds.length > 0) {
await ctx.db.suggestionKpi.createMany({
data: input.kpiIds.map((kpiId) => ({
suggestionId: input.suggestionId,
kpiId,
})),
})
}

return { success: true }
} catch (error) {
console.error('linkToSuggestion error:', error)
throw error
}
}),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make linkToSuggestion transactional and deduplicate IDs; return typed errors.

Delete-then-insert without a transaction risks losing links on mid-flight failures; duplicates in input can violate unique constraints.

- .mutation(async ({ ctx, input }) => {- try {- await ctx.db.suggestionKpi.deleteMany({- where: { suggestionId: input.suggestionId },- })-- if (input.kpiIds.length > 0) {- await ctx.db.suggestionKpi.createMany({- data: input.kpiIds.map((kpiId) => ({- suggestionId: input.suggestionId,- kpiId,- })),- })- }-- return { success: true }- } catch (error) {- console.error('linkToSuggestion error:', error)- throw error- }- }),+ .mutation(async ({ ctx, input }) => {+ const uniqueKpiIds = Array.from(new Set(input.kpiIds))+ try {+ await ctx.db.$transaction(async (trx) => {+ await trx.suggestionKpi.deleteMany({+ where: { suggestionId: input.suggestionId },+ })+ if (uniqueKpiIds.length > 0) {+ await trx.suggestionKpi.createMany({+ data: uniqueKpiIds.map((kpiId) => ({+ suggestionId: input.suggestionId,+ kpiId,+ })),+ })+ }+ })+ return { success: true }+ } catch (error) {+ console.error('linkToSuggestion error:', error)+ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Falha ao vincular KPIs" })+ }+ }),

Add the missing import at the top (outside the selected range):

import{TRPCError}from"@trpc/server"
🤖 Prompt for AI Agents
In src/server/api/routers/kpi.ts around lines 158 to 178, the linkToSuggestion
mutation currently does a deleteMany then createMany without a transaction,
allows duplicate kpiIds, and throws raw errors; wrap the delete+create in a
single database transaction (e.g. ctx.db.$transaction) so either both ops
succeed or none do, deduplicate input.kpiIds before creating (use a Set or
Array.from(new Set(...))), and replace the generic throw/console.error with a
typed TRPCError (import TRPCError from "@trpc/server") that returns a clear
error code/message on failure.

@rbxyz
rbxyz merged commit 396a227 into mainAug 25, 2025
6 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@rbxyz