`, after the `objetivo`/Badge header, replace the exercise count + buttons with conditional:
+
+```typescript
+{allowEditing && editingTreinoId === treino.id ? (
+
+ {
+ handleSave(data);
+ setEditingTreinoId(null);
+ }}
+ onCancel={() => setEditingTreinoId(null)}
+ />
+
+) : (
+ // all existing non-editing content:
+
+ {/* ... existing header, badges, exercise count ... */}
+
+ // ... existing buttons (Select, Play, Edit, Delete)
+)}
+```
+
+- [ ] **Step 3: Move "Criar Manual" button above lists, use WorkoutEditor inline**
+
+Remove lines 214-241 (bottom editor + button). Replace with:
+
+```typescript
+{/* Above renderWorkoutList calls, before the lists: */}
+
+ {renderWorkoutList(
+ treinosDoAluno,
+ 'Meus Treinos Pessoais',
+ 'Treinos que voce criou manualmente ou gerou com a IA.',
+ ,
+ true
+ )}
+
+
+
+
{
+ setEditingTreinoId('__new__');
+ }}
+ variant="outline"
+ size="lg"
+ >
+
+ Criar Novo Treino Manualmente
+
+
+
+{editingTreinoId === '__new__' && (
+
+ {
+ handleSave(data);
+ setEditingTreinoId(null);
+ }}
+ onCancel={() => setEditingTreinoId(null)}
+ />
+
+)}
+```
+
+- [ ] **Step 4: Remove window.scrollTo from use-workout-crud.ts**
+
+```typescript
+// In use-workout-crud.ts line 60, delete:
+// window.scrollTo({ top: 0, behavior: 'smooth' });
+// Replace with nothing (remove the line).
+```
+
+- [ ] **Step 5: Run typecheck + lint**
+
+```
+npx tsc --noEmit
+npx eslint src/app/aluno/meus-treinos/meus-treinos-client.tsx src/hooks/use-workout-crud.ts
+```
+Expected: both pass.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/app/aluno/meus-treinos/meus-treinos-client.tsx src/hooks/use-workout-crud.ts
+git commit -m "feat(ux): inline editor inside treino cards, remove bottom editor"
+```
+
+---
+
+### Task 4: Visual grouper banner + after-gen scroll
+
+**Files:**
+- Modify: `src/app/aluno/meus-treinos/meus-treinos-client.tsx`
+
+**Interfaces:**
+- Consumes: `planName` from Task 2
+- Produces: banner above treino list (auto-clears 30s), smooth scroll after gen
+
+- [ ] **Step 1: Add banner state + auto-clear effect**
+
+```typescript
+// After useWorkoutGeneration call (line ~70), add:
+ const [showPlanBanner, setShowPlanBanner] = useState(false);
+ const [bannerPlanName, setBannerPlanName] = useState
(null);
+
+ // Capture planName changes into banner state
+ useEffect(() => {
+ if (planName) {
+ setBannerPlanName(planName);
+ setShowPlanBanner(true);
+ const timer = setTimeout(() => setShowPlanBanner(false), 30000);
+ return () => clearTimeout(timer);
+ }
+ }, [planName]);
+```
+
+- [ ] **Step 2: Update onSuccess with scroll**
+
+```typescript
+// In useWorkoutGeneration options:
+ onSuccess: () => {
+ router.refresh();
+ setTimeout(() => {
+ document.getElementById('treinos-pessoais')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }, 300);
+ },
+```
+
+- [ ] **Step 3: Render banner above treino list**
+
+```typescript
+// Import Sparkles from lucide-react (line 16, already imported)
+// Before , add:
+{showPlanBanner && bannerPlanName && (
+
+
+
+
+ Plano Semanal: {bannerPlanName}
+
+
+
+)}
+```
+
+- [ ] **Step 4: Run typecheck + lint**
+
+```
+npx tsc --noEmit
+npx eslint src/app/aluno/meus-treinos/meus-treinos-client.tsx
+```
+Expected: both pass.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/app/aluno/meus-treinos/meus-treinos-client.tsx
+git commit -m "feat(ux): visual grouper banner + after-gen scroll anchor"
+```
+
+---
+
+### Task 5: Update tests
+
+**Files:**
+- Modify: `src/app/aluno/meus-treinos/meus-treinos-client.test.tsx`
+
+- [ ] **Step 1: Add planName to mock**
+
+```typescript
+// Lines 57-62, add planName to hook mock return:
+vi.mock('@/hooks/use-workout-generation', () => ({
+ useWorkoutGeneration: () => ({
+ isGenerating: false,
+ handleGenerate: vi.fn(),
+ planName: null,
+ }),
+}));
+```
+
+- [ ] **Step 2: Import useWorkoutGeneration for dynamic mocking**
+
+```typescript
+// Add at top after existing imports:
+import { useWorkoutGeneration } from '@/hooks/use-workout-generation';
+```
+
+- [ ] **Step 3: Add banner test**
+
+```typescript
+// After last test in meus-treinos-client.test.tsx:
+ it('renders plan banner when planName is set', () => {
+ (useWorkoutGeneration as ReturnType
).mockReturnValue({
+ isGenerating: false,
+ handleGenerate: vi.fn(),
+ planName: 'Hipertrofia 3x',
+ });
+ render( );
+ expect(screen.getByText(/Plano Semanal/)).toBeTruthy();
+ expect(screen.getByText(/Hipertrofia 3x/)).toBeTruthy();
+ });
+```
+
+- [ ] **Step 4: Run tests**
+
+```
+npx vitest run --reporter verbose src/app/aluno/meus-treinos/
+```
+Expected: all existing tests pass + new banner test.
+
+- [ ] **Step 5: Run all quality gates**
+
+```
+npx prettier --write src/app/aluno/meus-treinos/meus-treinos-client.tsx src/components/dashboard/aluno/workout-editor.tsx src/hooks/use-workout-generation.ts src/hooks/use-workout-crud.ts
+npx eslint src
+npx tsc --noEmit
+npx vitest run
+```
+Expected: all gates pass.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/app/aluno/meus-treinos/meus-treinos-client.test.tsx
+git commit -m "test(ux): update tests for inline editor + banner"
+```
+
+---
+
+### Task 6: Push + PR
+
+- [ ] **Step 1: Push**
+
+```bash
+git push origin fix/meus-treinos-ux-improvements
+```
+
+- [ ] **Step 2: Open PR**
+
+```bash
+gh pr create --repo EmiyaKiritsugu3/PWeb_Project \
+ --base main \
+ --head fix/meus-treinos-ux-improvements \
+ --title "feat(ux): inline editor + visual grouper for meus treinos" \
+ --body "## Changes
+
+- WorkoutEditor renders inline inside treino cards (no scroll jump)
+- Visual grouper banner after AI weekly plan generation (auto-hides 30s)
+- After-gen smooth scroll to treino list
+- Create button moved above treino list, opens inline editor
+- Removed bottom-of-page editor
+
+## Files
+
+- \`workout-editor.tsx\` — \`compact\` prop for inline rendering
+- \`use-workout-generation.ts\` — exposes \`planName\`
+- \`meus-treinos-client.tsx\` — inline editor, banner, scroll, repositioned create button
+- \`use-workout-crud.ts\` — removed scrollTo call
+
+Closes UX issues from design spec 2026-07-05.
+
+🤖 Generated with [Claude Code](https://claude.com/claude-code)"
+
+- [ ] **Step 3: Wait for CI, merge**
+
+Monitor checks, merge when green.
diff --git a/docs/superpowers/specs/2026-07-05-meus-treinos-ux-improvements-design.md b/docs/superpowers/specs/2026-07-05-meus-treinos-ux-improvements-design.md
new file mode 100644
index 00000000..00b3f11b
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-05-meus-treinos-ux-improvements-design.md
@@ -0,0 +1,88 @@
+# Design: Meus Treinos UX Improvements
+
+**Date:** 2026-07-05
+**Branch:** `fix/meus-treinos-ux-improvements`
+**Scope:** UX only — zero schema/API changes
+
+## Problem
+
+1. **Scroll paradox** — clicking "Editar" on a treino card runs `window.scrollTo({top:0})` but the editor appears at page bottom. User loses context, must scroll down manually.
+2. **No visual grouping** — AI generates 3-day split (e.g. Peito/Triceps + Costas/Biceps + Pernas/Ombros). Three independent cards with no visual connection.
+3. **After-gen feedback gap** — `router.refresh()` fixed (commit 1118d92), but no visual anchor after generation completes.
+
+## Design
+
+### 1. Inline Editor
+
+Clicking "Editar" on a card renders `WorkoutEditor` **inside the card**, replacing exercise list. No scroll. Cancel/Save collapses back to normal view.
+
+**Card states:**
+
+```
+NORMAL: EDITING:
+┌─────────────────────────────┐ ┌──────────────────────────────────┐
+│ Peito/Triceps [Seg] [▶][✏][🗑]│ │ ✏ Editando: Peito/Triceps │
+│ 5 exercicios │ │ ┌──────────────────────────────┐ │
+│ │ │ │ WorkoutEditor (exercises) │ │
+│ Dropdown agendamento │ │ └──────────────────────────────┘ │
+└─────────────────────────────┘ │ [Cancelar] [Salvar] │
+ └──────────────────────────────────┘
+```
+
+**Files:**
+- `card-treino.tsx` — add `isEditing` state + inline render of WorkoutEditor
+- `workout-editor.tsx` — add `compact?: boolean` prop to suppress outer Card wrapper
+- `meus-treinos-client.tsx` — remove bottom-of-page editor, wire inline mode
+
+### 2. Visual Grouper — Plano Semanal
+
+After AI generation, banner appears above "Meus Treinos Pessoais" for 30s.
+
+**Detection:** `useWorkoutGeneration` exposes `planName` from `streamWorkoutPlan` return. Client stores in state, renders banner, auto-clears after 30s.
+
+```tsx
+
+
+
+
+ Plano Semanal: {planName}
+
+ {diaLabels.join(' → ')}
+
+
+```
+
+Cards part of plan get `border-l-2 border-primary/30 ml-2 pl-4` timeline visual.
+
+### 3. After-gen Scroll
+
+Scroll smooth to `#treinos-pessoais` after `router.refresh()`.
+
+### 4. Remove Bottom Editor
+
+"Criar Novo Treino Manualmente" button moves **above** treino list. When clicked, renders `WorkoutEditor` inline above first card. Bottom conditional editor removed.
+
+## Files Changed
+
+| File | Change | Risk |
+|---|---|---|
+| `card-treino.tsx` | +isEditing state, inline WorkoutEditor render | Low |
+| `workout-editor.tsx` | +`compact?: boolean` prop | Low |
+| `meus-treinos-client.tsx` | -bottom editor, +banner, +planName, +scroll anchor | Medium |
+| `use-workout-generation.ts` | +expose planName from hook return | Low |
+| `card-treino.test.tsx` | +inline edit tests | Low |
+| `meus-treinos-client.test.tsx` | +banner tests | Low |
+
+## Out of Scope
+
+- PlanoSemanal Prisma model
+- Plan-wide progress tracking
+- 1-day = 1-card data model change
+
+## Success Criteria
+
+1. Click "Editar" → editor appears inside same card, zero page jump
+2. "Cancelar" / "Salvar" → card returns to normal
+3. AI gen: banner 30s + day sequence + scroll to treino list
+4. "Criar Manual" above list, opens inline editor
+5. All quality gates pass
diff --git a/src/app/aluno/meus-treinos/meus-treinos-client.test.tsx b/src/app/aluno/meus-treinos/meus-treinos-client.test.tsx
index 33d733a2..38c21013 100644
--- a/src/app/aluno/meus-treinos/meus-treinos-client.test.tsx
+++ b/src/app/aluno/meus-treinos/meus-treinos-client.test.tsx
@@ -3,6 +3,7 @@ import { render, screen } from '@testing-library/react';
import MeusTreinosClient from './meus-treinos-client';
import type { Treino } from '@/lib/definitions';
import type { ReactNode } from 'react';
+import { useWorkoutGeneration } from '@/hooks/use-workout-generation';
vi.mock('next/navigation', () => ({
useRouter: () => ({
@@ -55,10 +56,11 @@ vi.mock('@/hooks/use-workout-crud', () => ({
}));
vi.mock('@/hooks/use-workout-generation', () => ({
- useWorkoutGeneration: () => ({
+ useWorkoutGeneration: vi.fn(() => ({
isGenerating: false,
handleGenerate: vi.fn(),
- }),
+ planName: null,
+ })),
}));
vi.mock('@/lib/actions/treinos', () => ({
@@ -194,4 +196,15 @@ describe('MeusTreinosClient', () => {
render( );
expect(screen.getByText('Criar Novo Treino Manualmente')).toBeTruthy();
});
+
+ it('renders plan banner when planName is set', () => {
+ vi.mocked(useWorkoutGeneration).mockReturnValue({
+ isGenerating: false,
+ handleGenerate: vi.fn(),
+ planName: 'Hipertrofia 3x',
+ });
+ render( );
+ expect(screen.getByText(/Plano Semanal/)).toBeTruthy();
+ expect(screen.getByText(/Hipertrofia 3x/)).toBeTruthy();
+ });
});
diff --git a/src/app/aluno/meus-treinos/meus-treinos-client.tsx b/src/app/aluno/meus-treinos/meus-treinos-client.tsx
index 8ac5dab3..f6f5ee44 100644
--- a/src/app/aluno/meus-treinos/meus-treinos-client.tsx
+++ b/src/app/aluno/meus-treinos/meus-treinos-client.tsx
@@ -1,6 +1,6 @@
'use client';
-import React, { useState, useMemo } from 'react';
+import React, { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { PageHeader } from '@/components/page-header';
import {
@@ -13,7 +13,7 @@ import {
import { DIAS_DA_SEMANA } from '@/lib/constants';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
-import { PlusCircle, Trash2, Pencil, FileSignature, User, Play } from 'lucide-react';
+import { PlusCircle, Trash2, Pencil, FileSignature, User, Play, Sparkles } from 'lucide-react';
import type { Treino, HistoricoTreino } from '@/lib/definitions';
import { useAppNotification } from '@/hooks/use-app-notification';
import { WorkoutSession } from '@/components/WorkoutSession';
@@ -48,28 +48,55 @@ export default function MeusTreinosClient({
const {
meusTreinos,
- isFormVisible,
- setIsFormVisible,
- editingTreino,
- setEditingTreino,
isAlertOpen,
setIsAlertOpen,
deletingTreino,
handleSave,
- handleEdit,
handleDayChange,
openDeleteAlert,
handleDelete,
} = useWorkoutCRUD({ initialTreinos, userId, router, notify });
- const { isGenerating, handleGenerate } = useWorkoutGeneration({
+ const { isGenerating, handleGenerate, planName } = useWorkoutGeneration({
userId,
meusTreinos,
notify,
- onSuccess: () => router.refresh(),
+ onSuccess: () => {
+ router.refresh();
+ let scrollAttempts = 0;
+ const MAX_SCROLL_ATTEMPTS = 20;
+ const scrollToTreinos = () => {
+ const el = document.getElementById('treinos-pessoais');
+ if (el) {
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ return;
+ }
+ if (++scrollAttempts < MAX_SCROLL_ATTEMPTS) {
+ requestAnimationFrame(scrollToTreinos);
+ }
+ };
+ requestAnimationFrame(scrollToTreinos);
+ },
});
+ const [showPlanBanner, setShowPlanBanner] = useState(false);
+ const [bannerPlanName, setBannerPlanName] = useState(null);
+
+ useEffect(() => {
+ if (planName) {
+ setBannerPlanName(planName);
+ setShowPlanBanner(true);
+ const timer = setTimeout(() => setShowPlanBanner(false), 30000);
+ return () => clearTimeout(timer);
+ }
+ }, [planName]);
+
const [treinoEmSessao, setTreinoEmSessao] = useState(null);
+ const [editingTreinoId, setEditingTreinoId] = useState(null);
+
+ const handleEditLocal = useCallback((treino: Treino) => {
+ setEditingTreinoId(treino.id);
+ }, []);
const { treinosDoPersonal, treinosDoAluno } = useMemo(() => {
const treinosDoPersonal = meusTreinos.filter((t) => t.instrutorId && t.instrutorId !== userId);
@@ -105,50 +132,72 @@ export default function MeusTreinosClient({
treino.diaSemana !== null && 'bg-accent/10 border-accent'
)}
>
-
-
-
{treino.objetivo}
- {treino.diaSemana !== null &&
{getDiaLabel(treino.diaSemana)} }
- {treino.instrutorId && treino.instrutorId !== userId && (
-
Do Personal
- )}
+ {allowEditing && editingTreinoId === treino.id ? (
+
+ {
+ const ok = await handleSave(data, treino.id);
+ if (ok) setEditingTreinoId(null);
+ }}
+ onCancel={() => setEditingTreinoId(null)}
+ />
-
{treino.exercicios.length} exercícios
-
-
-
handleDayChange(treino.id, value)}
- >
-
-
-
-
- Nenhum (Desativado)
- {DIAS_DA_SEMANA.map((dia) => (
-
- {dia.label}
-
- ))}
-
-
-
setTreinoEmSessao(treino)}>
-
- Iniciar
-
- {allowEditing && (
- <>
-
handleEdit(treino)}>
-
- Editar
+ ) : (
+ <>
+
+
+
{treino.objetivo}
+ {treino.diaSemana !== null && {getDiaLabel(treino.diaSemana)} }
+ {treino.instrutorId && treino.instrutorId !== userId && (
+ Do Personal
+ )}
+
+
+ {treino.exercicios.length} exercícios
+
+
+
+
handleDayChange(treino.id, value)}
+ >
+
+
+
+
+ Nenhum (Desativado)
+ {DIAS_DA_SEMANA.map((dia) => (
+
+ {dia.label}
+
+ ))}
+
+
+
setTreinoEmSessao(treino)}>
+
+ Iniciar
-
openDeleteAlert(treino)}>
-
- Excluir
-
- >
- )}
-
+ {allowEditing && (
+ <>
+ handleEditLocal(treino)}>
+
+ Editar
+
+ openDeleteAlert(treino)}
+ >
+
+ Excluir
+
+ >
+ )}
+
+ >
+ )}
))}
{treinos.length === 0 && (
@@ -201,44 +250,57 @@ export default function MeusTreinosClient({
false
)}
- {renderWorkoutList(
- treinosDoAluno,
- 'Meus Treinos Pessoais',
- 'Treinos que você criou manualmente ou gerou com a IA.',
- ,
- true
+
+
+ {showPlanBanner && bannerPlanName && (
+
+
+
+
+ Plano Semanal: {bannerPlanName}
+
+
+ Seu plano semanal foi gerado! Confira os treinos abaixo.
+
+
+
)}
+
+ {renderWorkoutList(
+ treinosDoAluno,
+ 'Meus Treinos Pessoais',
+ 'Treinos que voce criou manualmente ou gerou com a IA.',
+ ,
+ true
+ )}
+
-
+
+
{
+ setEditingTreinoId('__new__');
+ }}
+ variant="outline"
+ size="lg"
+ >
+
+ Criar Novo Treino Manualmente
+
+
- {isFormVisible && (
-
+ {editingTreinoId === '__new__' && (
+
{
- setIsFormVisible(false);
- setEditingTreino(null);
+ compact
+ treinoToEdit={null}
+ onSave={(data) => {
+ handleSave(data);
+ setEditingTreinoId(null);
}}
+ onCancel={() => setEditingTreinoId(null)}
/>
)}
-
- {!isFormVisible && (
-
-
{
- setEditingTreino(null);
- setIsFormVisible(true);
- }}
- variant="outline"
- size="lg"
- >
-
- Criar Novo Treino Manualmente
-
-
- )}
diff --git a/src/components/dashboard/aluno/workout-editor.test.tsx b/src/components/dashboard/aluno/workout-editor.test.tsx
index 13cea808..b55e0b24 100644
--- a/src/components/dashboard/aluno/workout-editor.test.tsx
+++ b/src/components/dashboard/aluno/workout-editor.test.tsx
@@ -200,6 +200,20 @@ describe('WorkoutEditor', () => {
expect(mockOnSave).not.toHaveBeenCalled();
});
+ describe('compact mode', () => {
+ it('does not render outer Card when compact', () => {
+ render(
+
+ );
+ expect(screen.queryByTestId('card')).toBeNull();
+ });
+
+ it('renders outer Card by default', () => {
+ render( );
+ expect(screen.getByTestId('card')).toBeTruthy();
+ });
+ });
+
it('renders exercise rows when exercises exist', () => {
const exercises = [
{ id: 'ex-1', nomeExercicio: 'Supino', series: 3, repeticoes: '10-12' },
diff --git a/src/components/dashboard/aluno/workout-editor.tsx b/src/components/dashboard/aluno/workout-editor.tsx
index 86158e6e..cd5d51cc 100644
--- a/src/components/dashboard/aluno/workout-editor.tsx
+++ b/src/components/dashboard/aluno/workout-editor.tsx
@@ -19,10 +19,12 @@ export function WorkoutEditor({
onSave,
treinoToEdit,
onCancel,
+ compact = false,
}: Readonly<{
onSave: (treino: Omit) => void;
treinoToEdit: Treino | null;
onCancel: () => void;
+ compact?: boolean;
}>) {
const { toast } = useToast();
const {
@@ -54,60 +56,68 @@ export function WorkoutEditor({
onSave(novoTreino);
};
- return (
-
-
-
-
-
- {treinoToEdit ? 'Editar Treino Pessoal' : 'Criar Novo Treino Pessoal'}
-
-
- Crie um plano de treino do zero ou ajuste um existente que você criou.
-
-
-
-
- Nome/Objetivo do Treino
- setObjetivo(e.target.value)}
- />
-
+ const content = (
+ <>
+
+
+
+ {treinoToEdit ? 'Editar Treino Pessoal' : 'Criar Novo Treino Pessoal'}
+
+
+ Crie um plano de treino do zero ou ajuste um existente que você criou.
+
+
+
+
+ Nome/Objetivo do Treino
+ setObjetivo(e.target.value)}
+ />
+
-
-
Exercícios
- {exercicios.map((exercicio, index) => (
-
-
-
- ))}
-
-
- Adicionar Exercício Manualmente
-
-
-
-
-
-
- {treinoToEdit ? 'Salvar Alterações' : 'Salvar Novo Treino'}
+
+
Exercícios
+ {exercicios.map((exercicio, index) => (
+
+
+
+ ))}
+
+
+ Adicionar Exercício Manualmente
-
- Cancelar
-
-
-
+
+
+
+
+
+ {treinoToEdit ? 'Salvar Alterações' : 'Salvar Novo Treino'}
+
+
+ Cancelar
+
+
+ >
+ );
+
+ if (compact) {
+ return {content}
;
+ }
+
+ return (
+
+ {content}
);
}
diff --git a/src/hooks/use-workout-crud.test.ts b/src/hooks/use-workout-crud.test.ts
index 580dc59d..c89319d4 100644
--- a/src/hooks/use-workout-crud.test.ts
+++ b/src/hooks/use-workout-crud.test.ts
@@ -128,11 +128,14 @@ describe('useWorkoutCRUD', () => {
act(() => result.current.handleEdit(createMockTreino('t-1', 1)));
await act(async () => {
- await result.current.handleSave({
- objetivo: 'Updated',
- diaSemana: 1,
- exercicios: [],
- });
+ await result.current.handleSave(
+ {
+ objetivo: 'Updated',
+ diaSemana: 1,
+ exercicios: [],
+ },
+ 't-1'
+ );
});
expect(upsertTreinoAction).toHaveBeenCalledWith(
@@ -153,14 +156,6 @@ describe('useWorkoutCRUD', () => {
expect(result.current.editingTreino).toEqual(treino);
expect(result.current.isFormVisible).toBe(true);
});
-
- it('should scroll to top', () => {
- const { result } = renderHook(() => useWorkoutCRUD(defaultOptions()));
-
- act(() => result.current.handleEdit(createMockTreino('t-1')));
-
- expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
- });
});
describe('handleDayChange', () => {
diff --git a/src/hooks/use-workout-crud.ts b/src/hooks/use-workout-crud.ts
index 078d4c0c..124f5573 100644
--- a/src/hooks/use-workout-crud.ts
+++ b/src/hooks/use-workout-crud.ts
@@ -25,25 +25,27 @@ export function useWorkoutCRUD({ initialTreinos, userId, router, notify }: UseWo
const [deletingTreino, setDeletingTreino] = useState(null);
const handleSave = useCallback(
- async (treinoData: Omit) => {
+ async (treinoData: Omit, treinoId?: string) => {
+ const isEdit = Boolean(treinoId);
try {
const res = await upsertTreinoAction({
...treinoData,
- ...(editingTreino ? { id: editingTreino.id } : {}),
+ ...(isEdit ? { id: treinoId } : {}),
alunoId: userId,
});
if (res.success) {
- notify.success(editingTreino ? 'Treino atualizado!' : 'Novo treino salvo!');
- if (editingTreino) {
+ notify.success(isEdit ? 'Treino atualizado!' : 'Novo treino salvo!');
+ if (isEdit) {
setMeusTreinos((prev) =>
- prev.map((t) => (t.id === editingTreino.id ? { ...t, ...treinoData } : t))
+ prev.map((t) => (t.id === treinoId ? { ...t, ...treinoData } : t))
);
} else {
router.refresh();
}
setIsFormVisible(false);
setEditingTreino(null);
+ return true;
} else {
throw new Error(res.error);
}
@@ -51,13 +53,12 @@ export function useWorkoutCRUD({ initialTreinos, userId, router, notify }: UseWo
notify.error('Erro ao salvar', undefined, error);
}
},
- [editingTreino, userId, router, notify]
+ [userId, router, notify]
);
const handleEdit = useCallback((treino: Treino) => {
setEditingTreino(treino);
setIsFormVisible(true);
- window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
const handleDayChange = useCallback(
@@ -79,6 +80,8 @@ export function useWorkoutCRUD({ initialTreinos, userId, router, notify }: UseWo
setMeusTreinos((prev) =>
prev.map((t) => (t.id === treinoId ? { ...t, diaSemana: novoDia } : t))
);
+ } else {
+ notify.error('Erro ao atualizar agenda', res.error || 'Tente novamente.');
}
} catch (error) {
notify.error('Erro ao atualizar agenda', undefined, error);
@@ -100,6 +103,8 @@ export function useWorkoutCRUD({ initialTreinos, userId, router, notify }: UseWo
if (res.success) {
notify.success('Treino excluído!');
setMeusTreinos((prev) => prev.filter((t) => t.id !== deletingTreino.id));
+ } else {
+ notify.error('Erro ao excluir', res.error || 'Tente novamente.');
}
} catch (error) {
notify.error('Erro ao excluir', undefined, error);
diff --git a/src/hooks/use-workout-generation.ts b/src/hooks/use-workout-generation.ts
index 18a723c1..5b8ebf74 100644
--- a/src/hooks/use-workout-generation.ts
+++ b/src/hooks/use-workout-generation.ts
@@ -15,7 +15,7 @@ interface UseWorkoutGenerationOptions {
success: (title: string, description?: string) => void;
error: (title: string, description?: string, error?: unknown) => void;
};
- onSuccess: () => void;
+ onSuccess?: () => void;
}
export function useWorkoutGeneration({
@@ -25,10 +25,12 @@ export function useWorkoutGeneration({
onSuccess,
}: UseWorkoutGenerationOptions) {
const [isGenerating, setIsGenerating] = useState(false);
+ const [planName, setPlanName] = useState(null);
const handleGenerate = useCallback(
async (data: WorkoutGeneratorInput) => {
setIsGenerating(true);
+ setPlanName(null);
try {
Logger.info('Chamando a IA com dados:', data);
const result = await streamWorkoutPlan(data);
@@ -76,8 +78,9 @@ export function useWorkoutGeneration({
}
}
+ setPlanName(result.planName);
notify.success('Plano Pessoal Gerado!', `${result.planName} foi criado com sucesso.`);
- onSuccess();
+ onSuccess?.();
} else {
Logger.error('Resultado inesperado da IA:', result);
throw new Error('Formato de retorno inválido.');
@@ -89,8 +92,8 @@ export function useWorkoutGeneration({
setIsGenerating(false);
}
},
- [userId, meusTreinos, notify]
+ [userId, meusTreinos, notify, onSuccess]
);
- return { isGenerating, handleGenerate };
+ return { isGenerating, handleGenerate, planName };
}
diff --git a/src/lib/actions/treinos.ts b/src/lib/actions/treinos.ts
index c17a5c2c..4f7986fd 100644
--- a/src/lib/actions/treinos.ts
+++ b/src/lib/actions/treinos.ts
@@ -243,10 +243,12 @@ export async function updateTreinoDayAction(treinoId: string, diaSemana: number
select: { instrutorId: true, alunoId: true },
});
+ const alunoAuthId = await resolveAlunoId(user);
+
if (
funcData?.role !== 'GERENTE' &&
treino?.instrutorId !== user.id &&
- treino?.alunoId !== user.id
+ treino?.alunoId !== alunoAuthId
) {
return { success: false, error: 'Acesso não autorizado' };
}
@@ -286,10 +288,12 @@ export async function deleteTreinoAction(treinoId: string) {
select: { instrutorId: true, alunoId: true },
});
+ const alunoAuthId = await resolveAlunoId(user);
+
if (
funcData?.role !== 'GERENTE' &&
treino?.instrutorId !== user.id &&
- treino?.alunoId !== user.id
+ treino?.alunoId !== alunoAuthId
) {
return { success: false, error: 'Acesso não autorizado' };
}