feat: add telefono field to UsersTable and user controller for enhanced user information

This commit is contained in:
Marco Pedone 2026-03-10 13:57:29 +01:00
parent 0619ca0c34
commit 0759f4216f
3 changed files with 166 additions and 133 deletions

View file

@ -14,6 +14,7 @@ import {
type VisibilityState, type VisibilityState,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import type React from "react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { DataTablePagination } from "~/components/custom_ui/dataTable-pagination"; import { DataTablePagination } from "~/components/custom_ui/dataTable-pagination";
import { import {
@ -30,8 +31,9 @@ import {
TableRow, TableRow,
} from "~/components/ui/table"; } from "~/components/ui/table";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { ContextMenu, ContextMenuTrigger } from "../ui/context-menu";
interface DataTableProps<TData, TValue> { type DataTableProps<TData, TValue> = {
columns: ColumnDef<TData, TValue>[]; columns: ColumnDef<TData, TValue>[];
data: TData[]; data: TData[];
pinnedFiltri?: PinnedFiltro[]; pinnedFiltri?: PinnedFiltro[];
@ -47,7 +49,23 @@ interface DataTableProps<TData, TValue> {
onStateChange?: () => void; onStateChange?: () => void;
defaultFilterState?: ColumnFiltersState; defaultFilterState?: ColumnFiltersState;
onTableInit?: (table: TableType<TData>) => void; onTableInit?: (table: TableType<TData>) => void;
} } & (
| {
withContextMenu: true;
/**
* @example
* <ContextMenuContent>
* <ContextMenuItem>{data}<ContextMenuItem>
* <ContextMenuContent>
*/
contextMenuContent: (data: TData) => React.ReactNode;
}
| {
withContextMenu?: false;
contextMenuContent?: never;
}
);
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
columns, columns,
@ -65,6 +83,8 @@ export function DataTable<TData, TValue>({
onStateChange, onStateChange,
defaultFilterState, defaultFilterState,
onTableInit, onTableInit,
withContextMenu,
contextMenuContent,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>(defaultSort || []); const [sorting, setSorting] = useState<SortingState>(defaultSort || []);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
@ -155,7 +175,8 @@ export function DataTable<TData, TValue>({
virtualizer.getVirtualItems().map((virtualRow, index) => { virtualizer.getVirtualItems().map((virtualRow, index) => {
// biome-ignore lint/style/noNonNullAssertion: <always exists> // biome-ignore lint/style/noNonNullAssertion: <always exists>
const row = rows[virtualRow.index]!; const row = rows[virtualRow.index]!;
return (
const rowElem = (
<TableRow <TableRow
className={cn( className={cn(
rowOnClick && "cursor-pointer", rowOnClick && "cursor-pointer",
@ -189,6 +210,17 @@ export function DataTable<TData, TValue>({
})} })}
</TableRow> </TableRow>
); );
if (withContextMenu && contextMenuContent) {
return (
<ContextMenu key={row.id}>
<ContextMenuTrigger asChild>{rowElem}</ContextMenuTrigger>
{contextMenuContent(row.original)}
</ContextMenu>
);
}
return rowElem;
}) })
) : ( ) : (
<TableRow> <TableRow>

View file

@ -3,7 +3,6 @@ import {
Crown, Crown,
Mail, Mail,
MessagesSquare, MessagesSquare,
MoreHorizontal,
Paperclip, Paperclip,
Search, Search,
ShoppingBag, ShoppingBag,
@ -19,23 +18,25 @@ import type {
PinnedFiltro, PinnedFiltro,
SearchFiltro, SearchFiltro,
} from "~/components/custom_ui/dataTable-toolbar"; } from "~/components/custom_ui/dataTable-toolbar";
import { Button } from "~/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { UserAvatar } from "~/components/user_avatar"; import { UserAvatar } from "~/components/user_avatar";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import type { Users } from "~/schemas/public/Users"; import type { Users } from "~/schemas/public/Users";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import {
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
} from "../ui/context-menu";
type UserWChatInfo = Pick< type UserWChatInfo = Pick<
Users, Users,
"id" | "username" | "email" | "isAdmin" | "isBlocked" | "created_at" | "id"
| "username"
| "email"
| "telefono"
| "isAdmin"
| "isBlocked"
| "created_at"
> & { chatid: string | null }; > & { chatid: string | null };
export const UsersTable = (props: { data: UserWChatInfo[] }) => { export const UsersTable = (props: { data: UserWChatInfo[] }) => {
@ -51,10 +52,10 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
const tabledata = data.map((user) => { const tabledata = data.map((user) => {
return { return {
actions: "Azioni",
chatid: user.chatid, chatid: user.chatid,
created_at: user.created_at, created_at: user.created_at,
email: user.email, email: user.email,
telefono: user.telefono,
id: user.id, id: user.id,
isAdmin: user.isAdmin ? "Admin" : "Utente", isAdmin: user.isAdmin ? "Admin" : "Utente",
isBanned: user.isBlocked ? "Bloccato" : "Attivo", isBanned: user.isBlocked ? "Bloccato" : "Attivo",
@ -68,9 +69,9 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
]; ];
const columns_titles = { const columns_titles = {
actions: "Azioni",
created_at: "Creato il", created_at: "Creato il",
email: "Email", email: "Email",
telefono: "Telefono",
isAdmin: "Ruolo", isAdmin: "Ruolo",
isBanned: "Status", isBanned: "Status",
username: "User", username: "User",
@ -111,6 +112,30 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
<DataTableColumnHeader column={column} title={columns_titles.email} /> <DataTableColumnHeader column={column} title={columns_titles.email} />
), ),
}, },
{
accessorKey: "telefono",
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.telefono}
/>
),
},
{
accessorKey: "created_at",
cell: ({ row }) => {
const date = row.getValue("created_at");
return new Date(date as Date).toLocaleDateString();
},
enableHiding: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.created_at}
/>
),
sortingFn: "datetime",
},
{ {
accessorKey: "isAdmin", accessorKey: "isAdmin",
cell: ({ row }) => { cell: ({ row }) => {
@ -141,6 +166,7 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
<DataTableColumnHeader column={column} title={columns_titles.isAdmin} /> <DataTableColumnHeader column={column} title={columns_titles.isAdmin} />
), ),
}, },
{ {
accessorKey: "isBanned", accessorKey: "isBanned",
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
@ -153,123 +179,6 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
/> />
), ),
}, },
{
accessorKey: "created_at",
cell: ({ row }) => {
const date = row.getValue("created_at");
return new Date(date as Date).toLocaleDateString();
},
enableHiding: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.created_at}
/>
),
sortingFn: "datetime",
},
{
accessorKey: "actions",
cell: ({ row }) => {
const user = row.original;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="size-8 p-0" variant="ghost">
<span className="sr-only">Apri menu</span>
<MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Azioni</DropdownMenuLabel>
<DropdownMenuItem>
<Link
aria-label="Visualizza Ricerca"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/ricerca/${user.id}`}
>
<Search className="text-muted-foreground" /> Ricerca
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
aria-label="Visualizza Comunicazioni"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/comunicazioni/${user.id}`}
>
<Mail className="text-muted-foreground" /> Comunicazioni
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
aria-label="Visualizza Profilo"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/edit-user/${user.id}`}
>
<UserCog className="text-muted-foreground" /> Profilo
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
aria-label="Visualizza Ordini"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/ordini/${user.id}`}
>
<ShoppingBag className="text-muted-foreground" /> Ordini
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
aria-label="Visualizza Allegati"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/allegati/${user.id}`}
>
<Paperclip className="text-muted-foreground" /> Allegati
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link
aria-label="Visualizza Chat"
className={cn(
"flex w-full items-center gap-2",
!user.chatid && "text-neutral-200",
)}
href={
user.chatid
? {
pathname: "/area-riservata/admin/chats",
query: { chatId: user.chatid },
}
: "/area-riservata/admin/chats"
}
>
<MessagesSquare /> Chat
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<button
aria-label="Login as User"
className="flex w-full cursor-pointer items-center gap-2"
onClick={async () => await swap({ id: user.id })}
type="button"
>
<User /> Login
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
enableHiding: false,
enableSorting: false,
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.actions} />
),
},
]; ];
const pinnedFiltri: PinnedFiltro[] = [ const pinnedFiltri: PinnedFiltro[] = [
{ {
@ -298,10 +207,101 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
<DataTable <DataTable
columns={columns} columns={columns}
columns_titles={columns_titles} columns_titles={columns_titles}
contextMenuContent={(data) => (
<ContextMenuContent>
<ContextMenuItem>
<Link
aria-label="Visualizza Ricerca"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/ricerca/${data.id}`}
>
<Search className="text-muted-foreground" /> Ricerca
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
aria-label="Visualizza Comunicazioni"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/comunicazioni/${data.id}`}
>
<Mail className="text-muted-foreground" /> Comunicazioni
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
aria-label="Visualizza Profilo"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/edit-user/${data.id}`}
>
<UserCog className="text-muted-foreground" /> Profilo
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
aria-label="Visualizza Ordini"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/ordini/${data.id}`}
>
<ShoppingBag className="text-muted-foreground" /> Ordini
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
aria-label="Visualizza Allegati"
className="flex w-full items-center gap-2"
href={`/area-riservata/admin/user-view/allegati/${data.id}`}
>
<Paperclip className="text-muted-foreground" /> Allegati
</Link>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>
<Link
aria-label="Visualizza Chat"
className={cn(
"flex w-full items-center gap-2",
!data.chatid && "text-neutral-200",
)}
href={
data.chatid
? {
pathname: "/area-riservata/admin/chats",
query: { chatId: data.chatid },
}
: "/area-riservata/admin/chats"
}
>
<MessagesSquare /> Chat
</Link>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>
<button
aria-label="Login as User"
className="flex w-full cursor-pointer items-center gap-2"
onClick={async () => await swap({ id: data.id })}
type="button"
>
<User /> Login
</button>
</ContextMenuItem>
</ContextMenuContent>
)}
data={tabledata} data={tabledata}
defaultColumnVisibility={{
actions: true,
created_at: true,
email: true,
telefono: true,
isAdmin: true,
isBanned: false,
username: true,
}}
defaultSort={[{ desc: true, id: "created_at" }]} defaultSort={[{ desc: true, id: "created_at" }]}
pinnedFiltri={pinnedFiltri} pinnedFiltri={pinnedFiltri}
searchColumn={searchFiltro} searchColumn={searchFiltro}
withContextMenu={true}
/> />
</div> </div>
); );

View file

@ -173,6 +173,7 @@ export const getUsersWithChatInfoHandler = async ({ db }: { db: Querier }) => {
.select([ .select([
"users.id", "users.id",
"users.username", "users.username",
"users.telefono",
"users.email", "users.email",
"users.isAdmin", "users.isAdmin",
"users.isBlocked", "users.isBlocked",