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">
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp
WhatsApp{data.extra_telefono_numero && ` (${data.nome})`}
</Button>
</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 ? (
<IntestazioneMaker />
) : (

View file

@ -31,6 +31,9 @@ import {
ContextMenuContent,
ContextMenuItem,
ContextMenuLabel,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "~/components/ui/context-menu";
import { DataTable } from "~/components/ui/data-table";
@ -161,7 +164,36 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
<UserCog className="text-muted-foreground" /> Profilo
</Link>
</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>
<Link
className="flex w-full items-center gap-2"
@ -172,6 +204,7 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
WhatsApp
</Link>
</ContextMenuItem>
)}
</ContextMenuContent>
</ContextMenu>
);

View file

@ -38,6 +38,9 @@ import {
ContextMenuItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
} from "../ui/context-menu";
import {
Dialog,
@ -66,10 +69,13 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
const tabledata = data.map((user) => {
return {
nome: user.nome,
chatid: user.chatid,
created_at: user.created_at,
email: user.email,
telefono: user.telefono,
extra_telefono_numero: user.extra_telefono_numero,
extra_telefono_titolo: user.extra_telefono_titolo,
note: user.note,
id: user.id,
isAdmin: user.isAdmin ? "Admin" : "Utente",
@ -190,6 +196,22 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
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",
@ -361,6 +383,36 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
<MessagesSquare /> Chat
</Link>
</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>
<Link
className="flex w-full items-center gap-2"
@ -371,6 +423,8 @@ export const UsersTable = (props: { data: UserWChatInfo[] }) => {
WhatsApp
</Link>
</ContextMenuItem>
)}
<ContextMenuSeparator />
<ContextMenuItem>
<button

View file

@ -20,7 +20,13 @@ import { api } from "~/utils/api";
type ProfileFromAccountProps = Pick<
Users,
"id" | "nome" | "cognome" | "email" | "telefono"
| "id"
| "nome"
| "cognome"
| "email"
| "telefono"
| "extra_telefono_numero"
| "extra_telefono_titolo"
> &
Partial<Pick<Users, "isAdmin">>;
@ -30,6 +36,8 @@ export const ProfileFormAccount = ({
cognome,
email,
telefono,
extra_telefono_numero,
extra_telefono_titolo,
isAdmin,
}: ProfileFromAccountProps) => {
const profileFormSchema = z.object({
@ -38,6 +46,8 @@ export const ProfileFormAccount = ({
isAdmin: z.boolean().optional(),
nome: z.string().nonempty("Inserisci un nome valido"),
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>;
@ -48,6 +58,8 @@ export const ProfileFormAccount = ({
isAdmin: isAdmin,
nome: nome || "",
telefono,
extra_telefono_numero: extra_telefono_numero || "",
extra_telefono_titolo: extra_telefono_titolo || "",
};
const { locale, t } = useTranslation();
z.config(z.locales[locale]());
@ -78,6 +90,7 @@ export const ProfileFormAccount = ({
data: {
...fields,
telefono: fields.telefono?.replace(/\s/g, ""),
extra_telefono_numero: fields.extra_telefono_numero?.replace(/\s/g, ""),
username: `${fields.cognome?.trim()} ${fields.nome?.trim()}`,
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 && (
<FormField
control={form.control}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -41,6 +41,10 @@ export default interface UsersTable {
note: ColumnType<string | null, string | null, string | null>;
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>;

View file

@ -150,6 +150,8 @@ export type UserWChatInfo = Pick<
| "username"
| "email"
| "telefono"
| "extra_telefono_numero"
| "extra_telefono_titolo"
| "isAdmin"
| "isBlocked"
| "created_at"
@ -159,12 +161,16 @@ export type UserWChatInfo = Pick<
export const getUsersWithChatInfoHandler = async (): Promise<
UserWChatInfo[]
> => {
try {
return await db
.selectFrom("users")
.select([
"users.id",
"users.nome",
"users.username",
"users.telefono",
"users.extra_telefono_numero",
"users.extra_telefono_titolo",
"users.email",
"users.isAdmin",
"users.isBlocked",
@ -193,4 +199,10 @@ export const getUsersWithChatInfoHandler = async (): Promise<
.orderBy("users.id", "desc")
.select(["users.id as id"])
.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",
"email",
"telefono",
"extra_telefono_numero",
"extra_telefono_titolo",
"created_at",
"isAdmin",
"isBlocked",