Skip to content

Commit f254245

Browse files
Ajustes nos grupos
1 parent b8de5b2 commit f254245

File tree

8 files changed

+451
-194
lines changed

8 files changed

+451
-194
lines changed

codigo-fonte/cultivo/apps/web/src/routes/groups/$groupId.tsx

Lines changed: 115 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,32 @@ function GroupDetails() {
2121
const { groupId } = Route.useParams();
2222
const group = useQuery(api.group.getById, { id: groupId as Id<"groups"> });
2323
const removeParticipant = useMutation(api.group.removeParticipant);
24+
const leaveGroup = useMutation(api.group.leaveGroup);
2425
const [inviteCopied, setInviteCopied] = useState(false);
2526
const [removingUserId, setRemovingUserId] = useState<string | null>(null);
27+
const [isLeavingGroup, setIsLeavingGroup] = useState(false);
2628
const userId = useMemo(() => {
2729
if (typeof window === "undefined") return null;
2830
const user = localStorage.getItem("user");
2931
return user ? JSON.parse(user)._id : null;
3032
}, []);
31-
const isOwner = useMemo(() => userId && group && String(group.createdBy) === String(userId), [userId, group]);
32-
const inviteLink = useMemo(() => group ? `${window.location.origin}/groups/join?groupId=${group._id}` : "", [group]);
33+
const isOwner = useMemo(
34+
() => userId && group && String(group.createdBy) === String(userId),
35+
[userId, group]
36+
);
37+
const inviteLink = useMemo(
38+
() =>
39+
group ? `${window.location.origin}/groups/join?groupId=${group._id}` : "",
40+
[group]
41+
);
3342

34-
async function handleRemoveParticipant(participantId: string, participantName: string) {
35-
if (!confirm(`Tem certeza que deseja remover ${participantName} do grupo?`)) {
43+
async function handleRemoveParticipant(
44+
participantId: string,
45+
participantName: string
46+
) {
47+
if (
48+
!confirm(`Tem certeza que deseja remover ${participantName} do grupo?`)
49+
) {
3650
return;
3751
}
3852

@@ -50,6 +64,26 @@ function GroupDetails() {
5064
}
5165
}
5266

67+
async function handleLeaveGroup() {
68+
if (!confirm("Tem certeza que deseja sair deste grupo?")) {
69+
return;
70+
}
71+
72+
setIsLeavingGroup(true);
73+
try {
74+
await leaveGroup({
75+
groupId: groupId as Id<"groups">,
76+
userId: userId as Id<"users">,
77+
});
78+
navigate({ to: "/groups" });
79+
} catch (error: any) {
80+
console.error("Error leaving group:", error);
81+
alert(error.message || "Erro ao sair do grupo. Tente novamente.");
82+
} finally {
83+
setIsLeavingGroup(false);
84+
}
85+
}
86+
5387
if (group === undefined) {
5488
return (
5589
<div className="space-y-4 p-4">
@@ -59,18 +93,18 @@ function GroupDetails() {
5993
);
6094
}
6195

62-
if (!group) {
63-
return <div className="p-4">Group not found</div>;
64-
}
96+
if (!group) {
97+
return <div className="p-4">Group not found</div>;
98+
}
6599

66100
return (
67101
<main className="flex flex-col justify-center items-center min-h-screen p-4 pt-16">
68102
<div className="w-full max-w-md mb-4">
69-
<button
103+
<button
70104
onClick={() => navigate({ to: "/groups" })}
71105
className="flex items-center gap-2 text-[#7c6a5c] hover:text-[#bfa98a] mb-3 transition-colors cursor-pointer"
72106
>
73-
<ArrowLeft size={20}/>
107+
<ArrowLeft size={20} />
74108
<span className="font-medium">Voltar</span>
75109
</button>
76110
<div className="text-center mb-2">
@@ -83,36 +117,73 @@ function GroupDetails() {
83117
<div className="bg-[#f8f3ed] p-8 flex flex-col gap-4 rounded-xl shadow w-full max-w-md">
84118
<div className="flex items-center justify-between mb-2">
85119
{isOwner && (
86-
<button title="Editar nome" className="text-[#7c6a5c] hover:text-[#bfa98a]">
87-
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
120+
<button
121+
title="Editar nome"
122+
className="text-[#7c6a5c] hover:text-[#bfa98a]"
123+
>
124+
<svg
125+
width="20"
126+
height="20"
127+
fill="none"
128+
stroke="currentColor"
129+
strokeWidth="2"
130+
viewBox="0 0 24 24"
131+
>
132+
<path d="M12 20h9" />
133+
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4 12.5-12.5z" />
134+
</svg>
88135
</button>
89136
)}
90137
</div>
91138
<Card className="bg-white border-none shadow-none p-4 flex flex-col items-center">
92-
<div className="text-[#7c6a5c] text-sm mb-1">Estoque Combinado Atual</div>
93-
<div className="text-3xl font-bold text-green-700 mb-1">{group.stock?.toLocaleString()} sacas</div>
139+
<div className="text-[#7c6a5c] text-sm mb-1">
140+
Estoque Combinado Atual
141+
</div>
142+
<div className="text-3xl font-bold text-green-700 mb-1">
143+
{group.stock?.toLocaleString()} sacas
144+
</div>
94145
</Card>
95146
<Card className="bg-white border-none shadow-none p-4">
96-
<div className="font-semibold text-[#7c6a5c] mb-2">Participantes ({group.participantsFull?.length ?? 0})</div>
147+
<div className="font-semibold text-[#7c6a5c] mb-2">
148+
Participantes ({group.participantsFull?.length ?? 0})
149+
</div>
97150
<div className="flex flex-col gap-2">
98151
{group.participantsFull?.map((p: any) => (
99-
<div key={p._id} className="flex items-center gap-3 bg-[#f5ede3] rounded-lg px-3 py-2">
152+
<div
153+
key={p._id}
154+
className="flex items-center gap-3 bg-[#f5ede3] rounded-lg px-3 py-2"
155+
>
100156
<Avatar name={p.name ?? p.email ?? "?"} />
101-
<span className="font-medium text-[#7c6a5c] flex-1">{p.name ?? p.email}{String(p._id) === String(userId) ? " (Você)" : ""}</span>
157+
<span className="font-medium text-[#7c6a5c] flex-1">
158+
{p.name ?? p.email}
159+
{String(p._id) === String(userId) ? " (Você)" : ""}
160+
</span>
102161
{isOwner && String(p._id) !== String(userId) && (
103162
<button
104163
title="Remover participante"
105164
className="text-[#bfa98a] hover:text-red-500 transition-colors disabled:opacity-50"
106-
onClick={() => handleRemoveParticipant(p._id, p.name ?? p.email ?? "participante")}
165+
onClick={() =>
166+
handleRemoveParticipant(
167+
p._id,
168+
p.name ?? p.email ?? "participante"
169+
)
170+
}
107171
disabled={removingUserId === p._id}
108172
>
109173
{removingUserId === p._id ? (
110174
<div className="w-5 h-5 border-2 border-[#bfa98a] border-t-transparent rounded-full animate-spin" />
111175
) : (
112-
<svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
113-
<circle cx="12" cy="12" r="10"/>
114-
<line x1="15" y1="9" x2="9" y2="15"/>
115-
<line x1="9" y1="9" x2="15" y2="15"/>
176+
<svg
177+
width="20"
178+
height="20"
179+
fill="none"
180+
stroke="currentColor"
181+
strokeWidth="2"
182+
viewBox="0 0 24 24"
183+
>
184+
<circle cx="12" cy="12" r="10" />
185+
<line x1="15" y1="9" x2="9" y2="15" />
186+
<line x1="9" y1="9" x2="15" y2="15" />
116187
</svg>
117188
)}
118189
</button>
@@ -122,24 +193,43 @@ function GroupDetails() {
122193
</div>
123194
</Card>
124195
<Card className="bg-white border-none shadow-none p-4">
125-
<div className="font-semibold text-green-700 mb-1">Link de convite</div>
196+
<div className="font-semibold text-green-700 mb-1">
197+
Link de convite
198+
</div>
126199
<div className="text-sm text-[#7c6a5c] mb-2">{inviteLink}</div>
127200
<Button
128201
variant="secondary"
129-
className="w-full bg-[#ffa726] text-white font-semibold"
202+
className="w-full bg-[#ffa726] text-white font-semibold hover:bg-[#ff9800] transition-all cursor-pointer shadow-sm hover:shadow-md"
130203
onClick={async () => {
131204
await navigator.clipboard.writeText(inviteLink);
132205
setInviteCopied(true);
133206
setTimeout(() => setInviteCopied(false), 2000);
134207
}}
135208
>
136-
{inviteCopied ? "Link copiado!" : "Convidar"}
209+
{inviteCopied ? "Link copiado!" : "📋 Convidar"}
137210
</Button>
138211
</Card>
212+
{!isOwner && (
213+
<Button
214+
variant="outline"
215+
className="w-full border-2 border-amber-600 text-amber-600 bg-white hover:bg-amber-50 hover:border-amber-700 transition-all font-semibold cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
216+
onClick={handleLeaveGroup}
217+
disabled={isLeavingGroup}
218+
>
219+
{isLeavingGroup ? (
220+
<>
221+
<div className="w-4 h-4 border-2 border-amber-600 border-t-transparent rounded-full animate-spin mr-2" />
222+
Saindo...
223+
</>
224+
) : (
225+
"Sair do Grupo"
226+
)}
227+
</Button>
228+
)}
139229
{isOwner && (
140230
<Button
141231
variant="destructive"
142-
className="w-full border-2 border-[#bfa98a] text-[#bfa98a] bg-white hover:bg-[#f5ede3] mt-2"
232+
className="w-full border-2 border-red-500 text-red-600 bg-white hover:bg-red-50 hover:border-red-600 transition-all font-semibold cursor-pointer"
143233
>
144234
Encerrar Grupo
145235
</Button>

codigo-fonte/cultivo/apps/web/src/routes/groups/index.tsx

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ function RouteComponent() {
1919
const navigate = useNavigate();
2020
const groups = useQuery(api.group.list);
2121
const userId = getUserIdFromLocalStorage();
22+
const userGroup = useQuery(
23+
api.group.getUserGroup,
24+
userId ? { userId } : "skip"
25+
);
2226
const ownedGroups =
2327
groups?.filter((g: any) => String(g.createdBy) === String(userId)) ?? [];
2428
const memberGroups =
@@ -34,6 +38,9 @@ function RouteComponent() {
3438
const [editing, setEditing] = useState(false);
3539
const [editGroup, setEditGroup] = useState<any | null>(null);
3640

41+
// Check if user is already in a group
42+
const isInGroup = userGroup !== undefined && userGroup !== null;
43+
3744
async function onDelete(id: any) {
3845
if (!confirm("Excluir grupo?")) return;
3946
await remove({ id });
@@ -60,16 +67,39 @@ function RouteComponent() {
6067
</h2>
6168
</div>
6269
</div>
63-
<p className="text-[#7c6a5c] mb-4">
64-
Crie um grupo para colaborar com outros produtores rurais
65-
</p>
66-
<Button
67-
onClick={() => navigate({ to: "/groups/new" })}
68-
className="w-full bg-[#ffa726] text-white font-semibold hover:bg-[#ff9800]"
69-
>
70-
<Plus size={18} className="mr-2" />
71-
Criar Novo Grupo
72-
</Button>
70+
{isInGroup ? (
71+
<>
72+
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
73+
<p className="text-amber-800 text-sm mb-2">
74+
⚠️ Você já está em um grupo
75+
</p>
76+
<p className="text-amber-700 text-sm">
77+
Você já é membro do grupo "<strong>{userGroup?.name}</strong>
78+
". Para criar um novo grupo, você precisa sair do grupo atual
79+
primeiro.
80+
</p>
81+
</div>
82+
<Button
83+
onClick={() => navigate({ to: `/groups/${userGroup._id}` })}
84+
className="w-full bg-[#ffa726] text-white font-semibold hover:bg-[#ff9800]"
85+
>
86+
Ver Meu Grupo
87+
</Button>
88+
</>
89+
) : (
90+
<>
91+
<p className="text-[#7c6a5c] mb-4">
92+
Crie um grupo para colaborar com outros produtores rurais
93+
</p>
94+
<Button
95+
onClick={() => navigate({ to: "/groups/new" })}
96+
className="w-full bg-[#ffa726] text-white font-semibold hover:bg-[#ff9800]"
97+
>
98+
<Plus size={18} className="mr-2" />
99+
Criar Novo Grupo
100+
</Button>
101+
</>
102+
)}
73103
</Card>
74104

75105
{/* Owned Groups */}
@@ -315,37 +345,45 @@ function RouteComponent() {
315345
}))
316346
}
317347
/>
318-
{(editGroup.participantsFull ?? []).map((p: any) => (
319-
<div className="space-y-2">
320-
<div className="text-sm font-medium">Participantes</div>
321-
<ul className="ml-2">
348+
<div className="space-y-2">
349+
<div className="text-sm font-medium">Participantes</div>
350+
<ul className="ml-2 space-y-2">
351+
{(editGroup.participantsFull ?? []).map((p: any) => (
322352
<li
323353
key={p._id}
324354
className="flex items-center justify-between gap-2"
325355
>
326356
<span className="text-sm">
327-
{p.name ?? p.email} ({p._id})
357+
{p.name ?? p.email}
358+
{String(p._id) === String(userId) && " (Você)"}
328359
</span>
329-
<Button
330-
size="sm"
331-
variant="destructive"
332-
onClick={async () => {
333-
await removeParticipant({
334-
groupId: editGroup._id,
335-
userId: p._id,
336-
});
337-
const refreshed =
338-
groups?.find((gg: any) => gg._id === editGroup._id) ??
339-
null;
340-
setEditGroup(refreshed);
341-
}}
342-
>
343-
Remover
344-
</Button>
360+
{String(p._id) === String(userId) ? (
361+
<span className="text-xs text-gray-500 italic">
362+
Proprietário
363+
</span>
364+
) : (
365+
<Button
366+
size="sm"
367+
variant="destructive"
368+
onClick={async () => {
369+
await removeParticipant({
370+
groupId: editGroup._id,
371+
userId: p._id,
372+
});
373+
const refreshed =
374+
groups?.find(
375+
(gg: any) => gg._id === editGroup._id
376+
) ?? null;
377+
setEditGroup(refreshed);
378+
}}
379+
>
380+
Remover
381+
</Button>
382+
)}
345383
</li>
346-
</ul>
347-
</div>
348-
))}
384+
))}
385+
</ul>
386+
</div>
349387
<div className="flex justify-end gap-2 mt-2">
350388
<Button
351389
type="button"

0 commit comments

Comments
 (0)