Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 4 additions & 27 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,13 @@
import ChartSwitcher from './components/ChartSwitcher';
import LatestExpenses from './components/LatestExpenses';
import SummaryCards from './components/SummaryCards';
import { getMe } from './services/me';
import { useLoaderData, useNavigate } from 'react-router';
import { REACTQUERY_KEYS } from './libs/ReactQuery/keys';
import { useQuery } from '@tanstack/react-query';
import { useAuthStore } from './context/stores/auth';

function App() {
/* const store = useAuthStore();

const loaderData = useLoaderData(); */
/* const { data } = useQuery({
queryKey: REACTQUERY_KEYS.USER.ME,
enabled: !storeUser.user,

initialData: () => {
const { user } = storeUser;
if (Object.entries(user).length > 0) {
storeUser.setUser(user);
return user;
} else {
logout();
}
},
queryFn: getMe,
}); */

/* useEffect(() => {
console.log(store);
}, []); */

const user = useAuthStore(store => store.user);
useEffect(() => {
console.log({ user });
}, []);

Check warning on line 11 in frontend/src/App.jsx

View workflow job for this annotation

GitHub Actions / Frontend (React)

React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array
return (
<div>
<SummaryCards />
Expand Down
43 changes: 0 additions & 43 deletions frontend/src/actions/groupAction.js
Original file line number Diff line number Diff line change
@@ -1,43 +0,0 @@
// src/routes/loginAction.js
import { createGroupFormSchema } from '../schemas/createGroup/form';
import { z } from 'zod';
import { createGroup } from '../services/groups';
import { customToast } from '../components/CustomToast';
import { queryClient } from '../libs/ReactQuery/queryClient';
import { REACTQUERY_KEYS } from '../libs/ReactQuery/keys';

export const groupAction = async ({ request }) => {
const form = await request.formData();
const formData = Object.fromEntries(form);
const { success, error, data } = createGroupFormSchema.safeParse(formData);

if (!success) {
const errors = {};
if (error instanceof z.ZodError) {
error.issues.forEach(issue => {
const fieldName = issue.path[0];
const title =
createGroupFormSchema.shape[fieldName]?.description || fieldName;

return (errors[fieldName] = {
title,
message: issue.message,
});
});
} else {
errors.global = 'Ocorreu um erro inesperado.';
}

return { success, errors, value: data };
}

try {
await createGroup(data);
queryClient.invalidateQueries([REACTQUERY_KEYS.GROUPS.ALL]);
customToast('Grupo criado', 'Grupo criado com sucesso', 'success');
return { modalClose: true };
} catch (error) {
customToast('Error', error.message, 'error');
return { modalClose: false };
}
};
2 changes: 1 addition & 1 deletion frontend/src/components/ChartSwitcher/ExpenseChart.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
import { avatarBackgroundColors } from '../../mockData/colorsPallete/colors';
import { useMeDashboardQuery } from '../../hooks/ReactQuery/Queries/useMeDashboardStatsQuery';

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/LatestExpenses/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default function LatestExpenses() {
<li key={expense.expenseId}>
<MenuItem
as="a"
href={`/groups/${expense.groupId}`}
href={`/groups/${expense.groupId}/expenses/${expense.expenseId}`}
aria-label={`Despesa: ${expense.title}`}
>
<div className="text-text bg-neutral border-surface rounded-lg p-4 border hover:shadow-sm transition-shadow cursor-pointer">
Expand Down
4 changes: 1 addition & 3 deletions frontend/src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useAuthStore } from '../../context/stores/auth';
import { useLogout } from '../../hooks/useLogout';
import LogoImage from '../Logo';
import DropdownMenu from './DropdownMenu';
import { Menu, MenuItem } from '@headlessui/react';

export default function Header() {
const logout = useLogout();
const isAuthenticated = useAuthStore(state => !!state.token);

return (
Expand All @@ -15,7 +13,7 @@ export default function Header() {
<LogoImage className="w-30 h-auto object-contain" />
</MenuItem>
</Menu>
<button onClick={() => logout()}>Sair</button>

{isAuthenticated && <DropdownMenu />}
</header>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Layout/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function Layout() {
<main className="flex-1 overflow-y-auto bg-neutral ">
<Header />
<div className="w-full flex justify-center px-4 sm:px-6 lg:px-8">
<div className="w-full max-w-4xl pt-10">
<div className="w-full max-w-4xl py-10">
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
Expand Down
48 changes: 25 additions & 23 deletions frontend/src/components/Modal/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,43 @@
TransitionChild,
} from '@headlessui/react';
import { Fragment, useEffect, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import ButtonUI from '../ui/Button';
import { useActionData, useRevalidator } from 'react-router';
import { customToast } from '../CustomToast';

export default function Modal({ children, isOpen, setIsOpen, fnClose }) {
export default function Modal({
children,
isOpen,
setIsOpen,
fnClose,
autoCloseOnSuccess = true, // 👈 opcional
}) {
const [showConfirm, setShowConfirm] = useState(false);
const actionData = useActionData();
const revalidator = useRevalidator();
const queryClient = useQueryClient();

// 🔍 Observa mutations do React Query
useEffect(() => {
if (actionData?.modalClose) {
setIsOpen(false);
return;
}
if (actionData?.errors) {
const nameField = actionData.errors?.name;
customToast(nameField.title, nameField.message, 'error');
} else if (actionData?.success) {
const { data: notification } = actionData;
customToast(notification.title, notification.message, 'success');
setIsOpen(false);
}
revalidator.revalidate();
}, [actionData]);
if (!autoCloseOnSuccess || !isOpen) return;

const unsubscribe = queryClient.getMutationCache().subscribe(mutation => {
if (mutation?.action?.type === 'success') {
setIsOpen(false);
} else if (mutation?.action?.type === 'error') {
const err = mutation.state.error;
customToast('Erro', err?.message || 'Algo deu errado', 'error');
}
});

return () => unsubscribe?.();
}, [autoCloseOnSuccess, isOpen, queryClient]);

Check warning on line 36 in frontend/src/components/Modal/index.jsx

View workflow job for this annotation

GitHub Actions / Frontend (React)

React Hook useEffect has a missing dependency: 'setIsOpen'. Either include it or remove the dependency array. If 'setIsOpen' changes too often, find the parent component that defines it and wrap that definition in useCallback

// 🚫 não fecha o modal direto — só abre o de confirmação
function handleCloseAttempt() {
setShowConfirm(true);
}

// ✅ agora sim, fecha de fato
function handleConfirmClose() {
setShowConfirm(false);
fnClose(); // fecha o modal principal
fnClose?.();
}

function handleCancelClose() {
Expand All @@ -47,7 +50,6 @@

return (
<>
{/* Modal principal */}
<Transition show={isOpen} as={Fragment}>
<Dialog onClose={handleCloseAttempt} className="fixed z-50 inset-0">
<div className="fixed inset-0 flex items-center justify-center p-4">
Expand Down Expand Up @@ -90,14 +92,14 @@
>
<span>x</span>
</ButtonUI>

<div className="p-6 sm:p-8">{children}</div>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</Transition>

{/* Modal de confirmação reutilizável */}
<ConfirmModal
isOpen={showConfirm}
onConfirm={handleConfirmClose}
Expand Down
65 changes: 35 additions & 30 deletions frontend/src/components/forms/ExpenseForm/ListMembers/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,38 @@

export default function ListMembers({
isLoading,
members: { filteredMembers, totalPages },
members: { groupMembers, totalPages },
onConfirm,
search: { search, setSearch },
}) {
const [inputValue, setInputValue] = useState(''); // valor do input enquanto digita
const [inputValue, setInputValue] = useState('');
const [page, setPage] = useState(0);

const [checkedMembers, setCheckedMembers] = useState([]);
// Alterna seleção de membros

const toggleMember = id => {
setCheckedMembers(prev =>
prev.includes(id) ? prev.filter(m => m !== id) : [...prev, id]
);
};

// Debounce para busca
useEffect(() => {
const handler = setTimeout(() => {
setSearch(inputValue.trim());
setPage(0);
}, 500);

return () => clearTimeout(handler);
}, [inputValue]); // 👈 apenas inputValue aqui
}, [inputValue]);

Check warning on line 29 in frontend/src/components/forms/ExpenseForm/ListMembers/index.jsx

View workflow job for this annotation

GitHub Actions / Frontend (React)

React Hook useEffect has a missing dependency: 'setSearch'. Either include it or remove the dependency array. If 'setSearch' changes too often, find the parent component that defines it and wrap that definition in useCallback

// Reseta seleção ao mudar de página ou busca
useEffect(() => {
setCheckedMembers([]);
}, [page, search]);

// 🧠 Ordena a lista: OWNER → ADMIN → USER
const sortedMembers = [...(groupMembers || [])].sort((a, b) => {
const order = { OWNER: 0, ADMIN: 1, USER: 2 };
return order[a.authority] - order[b.authority];
});

return (
<div className="flex flex-col gap-4 w-full">
{/* Campo de busca */}
Expand All @@ -59,25 +61,19 @@
<p className="text-sm text-muted text-center py-4 animate-pulse">
Carregando membros...
</p>
) : filteredMembers.length > 0 ? (
filteredMembers.map(member => {
) : sortedMembers.length > 0 ? (
sortedMembers.map(member => {
const isSelected = checkedMembers.includes(member.id);

return (
<div
key={member.id}
onClick={() => toggleMember(member.id)}
className={`
group flex items-center justify-between gap-3 p-3 rounded-xl cursor-pointer
transition-all duration-200 border
${
isSelected
? 'bg-primary/10 border-primary/40 shadow-sm'
: 'border-transparent hover:bg-primary/5 hover:border-primary/20'
}
`}
className={`group flex items-center justify-between gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 border ${
isSelected
? 'bg-primary/10 border-primary/40 shadow-sm'
: 'border-transparent hover:bg-primary/5 hover:border-primary/20'
}`}
>
{/* Avatar + nome */}
<div className="flex items-center gap-3 flex-1">
<div
className="w-10 h-10 rounded-full flex items-center justify-center font-semibold text-white text-sm shadow-sm group-hover:scale-105 transition-transform duration-200"
Expand All @@ -89,15 +85,26 @@
<span className="text-text font-medium leading-tight truncate">
{member.name}
</span>
{member.role && (
<span className="text-xs text-muted capitalize">
{member.role}
{member.authority && (
<span
className={`text-xs capitalize ${
member.authority === 'OWNER'
? 'text-amber-600 font-semibold'
: member.authority === 'ADMIN'
? 'text-blue-500'
: 'text-muted'
}`}
>
{member.authority === 'OWNER'
? 'Dono'
: member.authority === 'ADMIN'
? 'Administrador'
: 'Membro'}
</span>
)}
</div>
</div>

{/* Rótulo de seleção */}
{isSelected && (
<span className="text-xs font-medium text-primary bg-primary/10 px-2 py-1 rounded-full animate-fade-in">
Selecionado
Expand All @@ -113,7 +120,7 @@
)}
</div>

{/* Paginação simples */}
{/* Paginação */}
{totalPages > 1 && (
<Pagination
onNext={() => setPage(prev => Math.min(prev + 1, totalPages - 1))}
Expand All @@ -123,16 +130,14 @@
/>
)}

{/* Botões de ação */}
{/* Botões */}
<div className="flex justify-end gap-3 mt-4">
<ButtonUI
type="button"
disabled={checkedMembers.length === 0}
aria-disabled={checkedMembers.length === 0}
onClick={() =>
onConfirm(
filteredMembers.filter(m => checkedMembers.includes(m.id))
)
onConfirm(sortedMembers.filter(m => checkedMembers.includes(m.id)))
}
className={`px-6 py-2 rounded-lg font-semibold shadow-sm text-white transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-surface dark:focus:ring-offset-surface-dark bg-primary hover:bg-primary/90 active:bg-primary-dark cursor-pointer ${
checkedMembers.length === 0
Expand Down
Loading
Loading