Skip to content

Commit d3f52cc

Browse files
feat: add role functionality
1 parent 9f264ca commit d3f52cc

2 files changed

Lines changed: 243 additions & 82 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client"
2+
import { USER_ROLE } from "@polinetwork/backend"
3+
import { useMutation, useQueryClient } from "@tanstack/react-query"
4+
import { Plus, Search, X } from "lucide-react"
5+
import { useRouter } from "next/navigation"
6+
import { useState } from "react"
7+
import { toast } from "sonner"
8+
import { Badge } from "@/components/ui/badge"
9+
import { Button } from "@/components/ui/button"
10+
import {
11+
Dialog,
12+
DialogClose,
13+
DialogContent,
14+
DialogDescription,
15+
DialogFooter,
16+
DialogHeader,
17+
DialogTitle,
18+
DialogTrigger,
19+
} from "@/components/ui/dialog"
20+
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
21+
import { useSession } from "@/lib/auth"
22+
import { useTRPC } from "@/lib/trpc/client"
23+
import type { ApiInput, ApiOutput } from "@/lib/trpc/types"
24+
25+
type User = ApiOutput["tg"]["users"]["getByUsername"]["user"]
26+
type Roles = NonNullable<ApiOutput["tg"]["permissions"]["getRoles"]["roles"]>
27+
28+
const ARRAY_USER_ROLES = [
29+
USER_ROLE.ADMIN,
30+
USER_ROLE.HR,
31+
USER_ROLE.OWNER,
32+
USER_ROLE.CREATOR,
33+
USER_ROLE.DIRETTIVO,
34+
USER_ROLE.PRESIDENT,
35+
] as const
36+
37+
export function AddRole({ user, alreadyRoles }: { user: User; alreadyRoles: Roles }) {
38+
const sesh = useSession()
39+
const adderId = sesh.data?.user.telegramId
40+
41+
const availableRoles = ARRAY_USER_ROLES.filter((r) => !alreadyRoles.includes(r)).map((g) => ({
42+
value: g,
43+
label: `${g.slice(0, 1).toUpperCase()}${g.slice(1)}`,
44+
}))
45+
46+
const trpc = useTRPC()
47+
const qc = useQueryClient()
48+
const router = useRouter()
49+
50+
const [open, setOpen] = useState(false)
51+
const [selectedRole, setSelectedRole] = useState<Roles[number] | null>(null)
52+
53+
const submitMutation = useMutation(trpc.tg.permissions.addRole.mutationOptions())
54+
55+
async function submit() {
56+
if (!adderId) return toast.warning("Invalid session, try reloading the page")
57+
if (!selectedRole) return toast.warning("No group selected, cannot proceed")
58+
if (!user) return toast.warning("Invalid user, try restarting the dialog")
59+
60+
try {
61+
await submitMutation.mutateAsync({ adderId, userId: user.id, role: selectedRole })
62+
toast.info(`Role added`)
63+
handleOpenChange(false)
64+
router.refresh()
65+
} catch (err) {
66+
console.error(err)
67+
handleOpenChange(false)
68+
toast.error("There was an error, check logs")
69+
}
70+
}
71+
72+
function handleOpenChange(v: boolean) {
73+
setOpen(v)
74+
if (v === false) {
75+
// closing
76+
qc.invalidateQueries(trpc.tg.permissions.getRoles.queryOptions({ userId: user?.id ?? 0 }))
77+
setSelectedRole(null)
78+
}
79+
}
80+
81+
return (
82+
<Dialog open={open} onOpenChange={handleOpenChange}>
83+
<DialogTrigger
84+
render={
85+
<Button variant="outline">
86+
<Plus size={20} /> Add Role
87+
</Button>
88+
}
89+
/>
90+
<DialogContent className="sm:max-w-xl">
91+
<DialogHeader>
92+
<DialogTitle>Add Role</DialogTitle>
93+
<DialogDescription>Add a new role to a telegram user</DialogDescription>
94+
</DialogHeader>
95+
{user && (
96+
<p>
97+
Target: {user.firstName} {user.username && `@${user.username}`} [{user.id}]
98+
</p>
99+
)}
100+
<div className="flex items-center justify-start gap-2">
101+
<span>User roles: </span>
102+
{alreadyRoles.map((r) => (
103+
<Badge key={r}>{r}</Badge>
104+
))}
105+
</div>
106+
107+
<Select
108+
items={availableRoles}
109+
value={selectedRole}
110+
onValueChange={(v) => setSelectedRole(v)}
111+
disabled={availableRoles.length === 0}
112+
>
113+
<SelectTrigger className="w-full max-w-48">
114+
<SelectValue />
115+
</SelectTrigger>
116+
<SelectContent>
117+
<SelectGroup>
118+
{availableRoles.map((item) => (
119+
<SelectItem key={item.value} value={item.value}>
120+
{item.label}
121+
</SelectItem>
122+
))}
123+
</SelectGroup>
124+
</SelectContent>
125+
</Select>
126+
127+
<DialogFooter>
128+
<DialogClose render={<Button variant="outline">Cancel</Button>} />
129+
<Button onClick={submit} disabled={!selectedRole}>
130+
Confirm
131+
</Button>
132+
</DialogFooter>
133+
</DialogContent>
134+
</Dialog>
135+
)
136+
}

src/app/dashboard/(active)/telegram/users/page.tsx

Lines changed: 107 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { ArrowLeft, ExternalLinkIcon, Search, X } from "lucide-react"
44
import Link from "next/link"
55
import { useState } from "react"
66
import { toast } from "sonner"
7+
import { Badge } from "@/components/ui/badge"
78
import { Button } from "@/components/ui/button"
89
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
910
import { Input } from "@/components/ui/input"
1011
import { Label } from "@/components/ui/label"
1112
import { useTRPC } from "@/lib/trpc/client"
1213
import type { ApiOutput } from "@/lib/trpc/types"
1314
import { stripChatId } from "@/lib/utils/telegram"
15+
import { AddRole } from "./add-role"
1416
import { DeleteGroupAdmin } from "./delete-group-admin"
1517
import { NewGroupAdmin } from "./new-group-admin"
1618

@@ -48,6 +50,7 @@ export default function TgUsers() {
4850
<Link href="/dashboard/telegram" className="flex gap-1 items-center text-muted-foreground mb-2 hover:underline">
4951
<ArrowLeft size={16} /> Back
5052
</Link>
53+
5154
<form onSubmit={search} className="pt-2 gap-y-4 flex flex-col justify-start items-start">
5255
<div className="flex gap-2 flex-col items-start justify-start">
5356
<Label htmlFor="email" className="text-base">
@@ -75,109 +78,131 @@ export default function TgUsers() {
7578
</Button>
7679
)}
7780
</div>
78-
<span className="text-muted-foreground text-xs">Max results: 20</span>
7981
</div>
8082
</form>
8183

8284
<br />
8385

8486
{user && (
8587
<>
86-
<Card>
87-
<CardHeader>
88-
<CardTitle>User ID: {user.id}</CardTitle>
89-
</CardHeader>
90-
<CardContent>
91-
<p>
92-
<span className="text-muted-foreground">Name: </span>
93-
{user.firstName} {user.lastName}
94-
</p>
95-
<p>
96-
<span className="text-muted-foreground">Username: </span>
97-
{user.username}
98-
</p>
99-
<p>
100-
<span className="text-muted-foreground">Roles: </span>
101-
{userData?.roles ? userData.roles.join(", ") : 0}
102-
</p>
103-
</CardContent>
104-
</Card>
105-
88+
<UserInfoCard user={user} roles={userData?.roles} />
10689
<div className="pt-6 flex gap-4 items-center">
10790
<p>Admin in groups:</p>
10891
<NewGroupAdmin user={user} alreadyIn={userData?.groupAdmin.map((g) => g?.group.id ?? 0) ?? []} />
10992
</div>
11093
<div className="grid grid-cols-3 py-2 gap-4">
111-
{userData?.groupAdmin.map(
112-
(m, idx) =>
113-
m && (
114-
<Card key={m.group.id ?? `ga-${idx}`}>
115-
<CardContent>
116-
<p>
117-
{" "}
118-
<span className="text-muted-foreground">Chat: </span>
119-
{m.group && <span>{m.group.title}</span>} [{m.group.id}]
120-
</p>
121-
<p>
122-
<span className="text-muted-foreground">Added By: </span>
123-
{m.addedBy.firstName} {m.addedBy.username ? `@${m.addedBy.username}` : ""}
124-
</p>
125-
</CardContent>
126-
<CardFooter className="justify-end gap-2">
127-
<DeleteGroupAdmin userId={user.id} chatId={m.group.id} />
128-
</CardFooter>
129-
</Card>
130-
)
131-
)}
94+
{userData?.groupAdmin
95+
.filter((m) => m !== null && m !== undefined)
96+
.map((m) => (
97+
<GroupAdminCard groupAdminInfo={m} user={user} key={m.group.id} />
98+
))}
13299
</div>
133100

134101
<p className="pt-6">Last messages:</p>
135102
<div className="grid grid-cols-3 py-2 gap-4">
136103
{messages?.messages?.map((m) => (
137-
<Card key={`${m.messageId}-${m.chatId}`}>
138-
<CardContent>
139-
<p>
140-
{" "}
141-
<span className="text-muted-foreground">Chat: </span>
142-
{m.group && <span>{m.group.title}</span>} [{m.chatId}]
143-
</p>
144-
<p>
145-
{" "}
146-
<span className="text-muted-foreground">Message ID: </span>
147-
{m.messageId}
148-
</p>
149-
<p>
150-
{" "}
151-
<span className="text-muted-foreground">Timestamp: </span>
152-
{m.timestamp.toLocaleString()}
153-
</p>
154-
<span className="text-muted-foreground">Content:</span>
155-
<p className="pl-3">{m.message}</p>
156-
</CardContent>
157-
<CardFooter className="justify-end gap-2">
158-
{m.group?.inviteLink && (
159-
<a href={m.group.inviteLink} rel="noopener noreferral" target="_blank" aria-label="Join group">
160-
<Button variant="outline">
161-
<ExternalLinkIcon size={20} /> Join Chat
162-
</Button>
163-
</a>
164-
)}
165-
<a
166-
href={`https://t.me/c/${stripChatId(m.chatId)}/${m.messageId}`}
167-
rel="noopener noreferral"
168-
target="_blank"
169-
aria-label="Open message in chat"
170-
>
171-
<Button variant="outline">
172-
<ExternalLinkIcon size={20} /> Open
173-
</Button>
174-
</a>
175-
</CardFooter>
176-
</Card>
104+
<MessageCard message={m} key={`${m.chatId}-${m.messageId}`} />
177105
))}
178106
</div>
179107
</>
180108
)}
181109
</div>
182110
)
183111
}
112+
113+
type UserRoles = ApiOutput["tg"]["permissions"]["getRoles"]["roles"]
114+
function UserInfoCard({ user, roles }: { user: NonNullable<User>; roles: UserRoles }) {
115+
return (
116+
<Card>
117+
{" "}
118+
<CardHeader>
119+
<CardTitle>User ID: {user.id}</CardTitle>
120+
</CardHeader>
121+
<CardContent className="space-y-2">
122+
<p>
123+
<span className="text-muted-foreground">Name: </span>
124+
{user.firstName} {user.lastName}
125+
</p>
126+
<p>
127+
<span className="text-muted-foreground">Username: </span>
128+
{user.username}
129+
</p>
130+
<div className="flex items-center gap-2">
131+
<span className="text-muted-foreground">Roles: </span>
132+
{roles ? roles.map((r) => <Badge key={r}>{r}</Badge>) : "N/A"}
133+
</div>
134+
</CardContent>
135+
<CardFooter>
136+
<AddRole alreadyRoles={roles ?? []} user={user} />
137+
</CardFooter>
138+
</Card>
139+
)
140+
}
141+
142+
type GroupAdminSingle = NonNullable<ApiOutput["tg"]["permissions"]["getRoles"]["groupAdmin"][number]>
143+
function GroupAdminCard({ user, groupAdminInfo: m }: { user: NonNullable<User>; groupAdminInfo: GroupAdminSingle }) {
144+
return (
145+
<Card key={m.group.id}>
146+
<CardContent>
147+
<p>
148+
{" "}
149+
<span className="text-muted-foreground">Chat: </span>
150+
{m.group && <span>{m.group.title}</span>} [{m.group.id}]
151+
</p>
152+
<p>
153+
<span className="text-muted-foreground">Added By: </span>
154+
{m.addedBy.firstName} {m.addedBy.username ? `@${m.addedBy.username}` : ""}
155+
</p>
156+
</CardContent>
157+
<CardFooter className="justify-end gap-2">
158+
<DeleteGroupAdmin userId={user.id} chatId={m.group.id} />
159+
</CardFooter>
160+
</Card>
161+
)
162+
}
163+
164+
type Message = NonNullable<ApiOutput["tg"]["messages"]["getLastByUser"]["messages"]>[number]
165+
function MessageCard({ message: m }: { message: Message }) {
166+
return (
167+
<Card key={`${m.messageId}-${m.chatId}`}>
168+
<CardContent>
169+
<p>
170+
{" "}
171+
<span className="text-muted-foreground">Chat: </span>
172+
{m.group && <span>{m.group.title}</span>} [{m.chatId}]
173+
</p>
174+
<p>
175+
{" "}
176+
<span className="text-muted-foreground">Message ID: </span>
177+
{m.messageId}
178+
</p>
179+
<p>
180+
{" "}
181+
<span className="text-muted-foreground">Timestamp: </span>
182+
{m.timestamp.toLocaleString()}
183+
</p>
184+
<span className="text-muted-foreground">Content:</span>
185+
<p className="pl-3">{m.message}</p>
186+
</CardContent>
187+
<CardFooter className="justify-end gap-2">
188+
{m.group?.inviteLink && (
189+
<a href={m.group.inviteLink} rel="noopener noreferral" target="_blank" aria-label="Join group">
190+
<Button variant="outline">
191+
<ExternalLinkIcon size={20} /> Join Chat
192+
</Button>
193+
</a>
194+
)}
195+
<a
196+
href={`https://t.me/c/${stripChatId(m.chatId)}/${m.messageId}`}
197+
rel="noopener noreferral"
198+
target="_blank"
199+
aria-label="Open message in chat"
200+
>
201+
<Button variant="outline">
202+
<ExternalLinkIcon size={20} /> Open
203+
</Button>
204+
</a>
205+
</CardFooter>
206+
</Card>
207+
)
208+
}

0 commit comments

Comments
 (0)