feat: add support for additional phone numbers and titles in user profiles

This commit is contained in:
Marco Pedone 2026-04-13 12:45:37 +02:00
parent d600cac8ed
commit e847aefe64
13 changed files with 218 additions and 23 deletions

View file

@ -0,0 +1,5 @@
ALTER TABLE IF EXISTS public.users
ADD COLUMN IF NOT EXISTS extra_telefono_titolo text;
ALTER TABLE IF EXISTS public.users
ADD COLUMN IF NOT EXISTS extra_telefono_numero text;

View file

@ -144,9 +144,20 @@ export const UserViewHeader = () => {
<Link href={`https://wa.me/${data.telefono}`} target="_blank"> <Link href={`https://wa.me/${data.telefono}`} target="_blank">
<Button size="sm" variant="success"> <Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" /> <WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp WhatsApp{data.extra_telefono_numero && ` (${data.nome})`}
</Button> </Button>
</Link> </Link>
{data.extra_telefono_numero && (
<Link
href={`https://wa.me/${data.extra_telefono_numero}`}
target="_blank"
>
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp ({data.extra_telefono_titolo})
</Button>
</Link>
)}
{data.codice_fiscale ? ( {data.codice_fiscale ? (
<IntestazioneMaker /> <IntestazioneMaker />
) : ( ) : (

View file

@ -31,6 +31,9 @@ import {
ContextMenuContent, ContextMenuContent,
ContextMenuItem, ContextMenuItem,
ContextMenuLabel, ContextMenuLabel,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger, ContextMenuTrigger,
} from "~/components/ui/context-menu"; } from "~/components/ui/context-menu";
import { DataTable } from "~/components/ui/data-table"; import { DataTable } from "~/components/ui/data-table";
@ -161,7 +164,36 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
<UserCog className="text-muted-foreground" /> Profilo <UserCog className="text-muted-foreground" /> Profilo
</Link> </Link>
</ContextMenuItem> </ContextMenuItem>
{data.extra_telefono_numero ? (
<ContextMenuSub>
<ContextMenuSubTrigger className="flex w-full items-center gap-2">
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>
<Link
className="flex w-full items-center gap-2"
href={`https://wa.me/${data.telefono}`}
target="_blank"
>
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp ({data.nome})
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
className="flex w-full items-center gap-2"
href={`https://wa.me/${data.extra_telefono_numero}`}
target="_blank"
>
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp ({data.extra_telefono_titolo})
</Link>
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
) : (
<ContextMenuItem> <ContextMenuItem>
<Link <Link
className="flex w-full items-center gap-2" className="flex w-full items-center gap-2"
@ -172,6 +204,7 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
WhatsApp WhatsApp
</Link> </Link>
</ContextMenuItem> </ContextMenuItem>
)}
</ContextMenuContent> </ContextMenuContent>
</ContextMenu> </ContextMenu>
); );

View file

@ -38,6 +38,9 @@ import {
ContextMenuItem, ContextMenuItem,
ContextMenuLabel, ContextMenuLabel,
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
} from "../ui/context-menu"; } from "../ui/context-menu";
import { import {
Dialog, Dialog,
@ -66,10 +69,13 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
const tabledata = data.map((user) => { const tabledata = data.map((user) => {
return { return {
nome: user.nome,
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, telefono: user.telefono,
extra_telefono_numero: user.extra_telefono_numero,
extra_telefono_titolo: user.extra_telefono_titolo,
note: user.note, note: user.note,
id: user.id, id: user.id,
isAdmin: user.isAdmin ? "Admin" : "Utente", isAdmin: user.isAdmin ? "Admin" : "Utente",
@ -190,6 +196,22 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
title={columns_titles.telefono} title={columns_titles.telefono}
/> />
), ),
cell: ({ row }) => {
const { nome, telefono, extra_telefono_numero, extra_telefono_titolo } =
row.original;
return (
<div className="flex flex-col">
<span>{telefono}</span>
{extra_telefono_numero && (
<>
<span className="text-xs">{nome}</span>
<span>{extra_telefono_numero}</span>
<span className="text-xs">{extra_telefono_titolo}</span>
</>
)}
</div>
);
},
}, },
{ {
accessorKey: "email", accessorKey: "email",
@ -361,6 +383,36 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
<MessagesSquare /> Chat <MessagesSquare /> Chat
</Link> </Link>
</ContextMenuItem> </ContextMenuItem>
{data.extra_telefono_numero ? (
<ContextMenuSub>
<ContextMenuSubTrigger className="flex w-full items-center gap-2">
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp
</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem>
<Link
className="flex w-full items-center gap-2"
href={`https://wa.me/${data.telefono}`}
target="_blank"
>
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp ({data.nome})
</Link>
</ContextMenuItem>
<ContextMenuItem>
<Link
className="flex w-full items-center gap-2"
href={`https://wa.me/${data.extra_telefono_numero}`}
target="_blank"
>
<WhatsAppIcon2 className="size-4 fill-muted-foreground stroke-0" />
WhatsApp ({data.extra_telefono_titolo})
</Link>
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
) : (
<ContextMenuItem> <ContextMenuItem>
<Link <Link
className="flex w-full items-center gap-2" className="flex w-full items-center gap-2"
@ -371,6 +423,8 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
WhatsApp WhatsApp
</Link> </Link>
</ContextMenuItem> </ContextMenuItem>
)}
<ContextMenuSeparator /> <ContextMenuSeparator />
<ContextMenuItem> <ContextMenuItem>
<button <button

View file

@ -20,7 +20,13 @@ import { api } from "~/utils/api";
type ProfileFromAccountProps = Pick< type ProfileFromAccountProps = Pick<
Users, Users,
"id" | "nome" | "cognome" | "email" | "telefono" | "id"
| "nome"
| "cognome"
| "email"
| "telefono"
| "extra_telefono_numero"
| "extra_telefono_titolo"
> & > &
Partial<Pick<Users, "isAdmin">>; Partial<Pick<Users, "isAdmin">>;
@ -30,6 +36,8 @@ export const ProfileFormAccount = ({
cognome, cognome,
email, email,
telefono, telefono,
extra_telefono_numero,
extra_telefono_titolo,
isAdmin, isAdmin,
}: ProfileFromAccountProps) => { }: ProfileFromAccountProps) => {
const profileFormSchema = z.object({ const profileFormSchema = z.object({
@ -38,6 +46,8 @@ export const ProfileFormAccount = ({
isAdmin: z.boolean().optional(), isAdmin: z.boolean().optional(),
nome: z.string().nonempty("Inserisci un nome valido"), nome: z.string().nonempty("Inserisci un nome valido"),
telefono: z.string().nonempty("Inserisci un numero di telefono"), telefono: z.string().nonempty("Inserisci un numero di telefono"),
extra_telefono_numero: z.string().optional(),
extra_telefono_titolo: z.string().optional(),
}); });
type ProfileFormValues = z.infer<typeof profileFormSchema>; type ProfileFormValues = z.infer<typeof profileFormSchema>;
@ -48,6 +58,8 @@ export const ProfileFormAccount = ({
isAdmin: isAdmin, isAdmin: isAdmin,
nome: nome || "", nome: nome || "",
telefono, telefono,
extra_telefono_numero: extra_telefono_numero || "",
extra_telefono_titolo: extra_telefono_titolo || "",
}; };
const { locale, t } = useTranslation(); const { locale, t } = useTranslation();
z.config(z.locales[locale]()); z.config(z.locales[locale]());
@ -78,6 +90,7 @@ export const ProfileFormAccount = ({
data: { data: {
...fields, ...fields,
telefono: fields.telefono?.replace(/\s/g, ""), telefono: fields.telefono?.replace(/\s/g, ""),
extra_telefono_numero: fields.extra_telefono_numero?.replace(/\s/g, ""),
username: `${fields.cognome?.trim()} ${fields.nome?.trim()}`, username: `${fields.cognome?.trim()} ${fields.nome?.trim()}`,
isAdmin: fields.isAdmin || false, isAdmin: fields.isAdmin || false,
}, },
@ -186,6 +199,57 @@ export const ProfileFormAccount = ({
/> />
} }
/> />
<DualInputLayout
children1={
<FormField
control={form.control}
name="extra_telefono_numero"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="extra_telefono_numero">
{t.profile.extra_telefono_numero}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<PhoneInput
{...field}
autoComplete="off"
id="extra_telefono_numero"
/>
</FormControl>
</FormItem>
)}
/>
}
children2={
<FormField
control={form.control}
name="extra_telefono_titolo"
render={({ field }) => (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormLabel htmlFor="extra_telefono_titolo">
{t.profile.extra_telefono_titolo}
</FormLabel>
<FormMessage />
</div>
<FormControl>
<Input
placeholder=""
{...field}
autoComplete="off"
id="extra_telefono_titolo"
/>
</FormControl>
</FormItem>
)}
/>
}
/>
{isAdmin !== undefined && ( {isAdmin !== undefined && (
<FormField <FormField
control={form.control} control={form.control}

View file

@ -1215,6 +1215,8 @@ The Stable Rent service, ideal for those with demonstrable work references and f
email: "Email", email: "Email",
nome: "Name", nome: "Name",
telefono: "Phone", telefono: "Phone",
extra_telefono_numero: "Additional phone",
extra_telefono_titolo: "Additional phone contact",
}, },
proprietari: { proprietari: {
contattaci: "Contact us", contattaci: "Contact us",

View file

@ -1213,6 +1213,8 @@ export const it: LangDict = {
email: "Email", email: "Email",
nome: "Nome", nome: "Nome",
telefono: "Telefono", telefono: "Telefono",
extra_telefono_numero: "Telefono aggiuntivo",
extra_telefono_titolo: "Nominativo tel. aggiuntivo",
}, },
proprietari: { proprietari: {
contattaci: "Contattaci", contattaci: "Contattaci",

View file

@ -511,6 +511,8 @@ export type LangDict = {
cognome: string; cognome: string;
email: string; email: string;
telefono: string; telefono: string;
extra_telefono_numero: string;
extra_telefono_titolo: string;
aggiorna: string; aggiorna: string;
aggiornato: string; aggiornato: string;
}; };

View file

@ -135,6 +135,8 @@ const EditUser: NextPageWithLayout<EditUserProps> = ({
<ProfileFormAccount <ProfileFormAccount
cognome={userData.cognome} cognome={userData.cognome}
email={userData.email} email={userData.email}
extra_telefono_numero={userData.extra_telefono_numero}
extra_telefono_titolo={userData.extra_telefono_titolo}
id={userData.id} id={userData.id}
isAdmin={userData.isAdmin} isAdmin={userData.isAdmin}
nome={userData.nome} nome={userData.nome}

View file

@ -74,6 +74,8 @@ const Dashboard: NextPageWithLayout = () => {
<ProfileFormAccount <ProfileFormAccount
cognome={userData.cognome} cognome={userData.cognome}
email={userData.email} email={userData.email}
extra_telefono_numero={userData.extra_telefono_numero}
extra_telefono_titolo={userData.extra_telefono_titolo}
id={userData.id} id={userData.id}
nome={userData.nome} nome={userData.nome}
telefono={userData.telefono} telefono={userData.telefono}

View file

@ -41,6 +41,10 @@ export default interface UsersTable {
note: ColumnType<string | null, string | null, string | null>; note: ColumnType<string | null, string | null, string | null>;
comms_enabled: ColumnType<boolean, boolean | undefined, boolean>; comms_enabled: ColumnType<boolean, boolean | undefined, boolean>;
extra_telefono_titolo: ColumnType<string | null, string | null, string | null>;
extra_telefono_numero: ColumnType<string | null, string | null, string | null>;
} }
export type Users = Selectable<UsersTable>; export type Users = Selectable<UsersTable>;

View file

@ -150,6 +150,8 @@ export type UserWChatInfo = Pick<
| "username" | "username"
| "email" | "email"
| "telefono" | "telefono"
| "extra_telefono_numero"
| "extra_telefono_titolo"
| "isAdmin" | "isAdmin"
| "isBlocked" | "isBlocked"
| "created_at" | "created_at"
@ -159,12 +161,16 @@ export type UserWChatInfo = Pick<
export const getUsersWithChatInfoHandler = async (): Promise< export const getUsersWithChatInfoHandler = async (): Promise<
UserWChatInfo[] UserWChatInfo[]
> => { > => {
try {
return await db return await db
.selectFrom("users") .selectFrom("users")
.select([ .select([
"users.id", "users.id",
"users.nome",
"users.username", "users.username",
"users.telefono", "users.telefono",
"users.extra_telefono_numero",
"users.extra_telefono_titolo",
"users.email", "users.email",
"users.isAdmin", "users.isAdmin",
"users.isBlocked", "users.isBlocked",
@ -193,4 +199,10 @@ export const getUsersWithChatInfoHandler = async (): Promise<
.orderBy("users.id", "desc") .orderBy("users.id", "desc")
.select(["users.id as id"]) .select(["users.id as id"])
.execute(); .execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `errore getUsersWithChatInfoHandler: ${(e as Error).message}`,
});
}
}; };

View file

@ -125,6 +125,8 @@ export const getClientProfilo = async ({
"cognome", "cognome",
"email", "email",
"telefono", "telefono",
"extra_telefono_numero",
"extra_telefono_titolo",
"created_at", "created_at",
"isAdmin", "isAdmin",
"isBlocked", "isBlocked",