intestazione maker

This commit is contained in:
Marco Pedone 2025-11-20 16:48:29 +01:00
parent 6d38dc5d5c
commit eaf9ecc1d3
10 changed files with 302 additions and 164 deletions

View file

@ -1,20 +1,6 @@
import { getCookie } from "cookies-next"; import { getCookie } from "cookies-next";
import {
ExternalLink,
Mail,
MessagesSquare,
Paperclip,
Search,
ShoppingBag,
Tickets,
UserCog,
} from "lucide-react";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import type { ReactNode } from "react";
import { usePathname } from "next/navigation";
import { useRouter } from "next/router";
import { type ReactNode, useContext } from "react";
import toast from "react-hot-toast";
import { import {
SIDEBAR_COOKIE_NAME, SIDEBAR_COOKIE_NAME,
Sidebar, Sidebar,
@ -23,10 +9,7 @@ import { BannerFactory } from "~/components/banners";
import { Footer, MiniFooter } from "~/components/footer"; import { Footer, MiniFooter } from "~/components/footer";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import { SiteHeader } from "~/components/navbar/site-header"; import { SiteHeader } from "~/components/navbar/site-header";
import { WhatsAppIcon2 } from "~/components/svgs"; import { UserViewProvider } from "~/lib/userViewContext";
import { Button } from "~/components/ui/button";
import { Separator } from "~/components/ui/separator";
import { UserViewContext, UserViewProvider } from "~/lib/userViewContext";
import { cn } from "~/lib/utils"; import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import { import {
@ -36,6 +19,7 @@ import {
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { ValidSession } from "~/server/api/trpc"; import type { ValidSession } from "~/server/api/trpc";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { UserViewHeader } from "./area-riservata/userViewHeader";
interface Props { interface Props {
bodyClassName?: string; bodyClassName?: string;
@ -68,7 +52,6 @@ export const Layout = ({
</main> </main>
{!noFooter && <Footer className={footerClassName} />} {!noFooter && <Footer className={footerClassName} />}
{/* <CookieConsent /> */}
</div> </div>
); );
}; };
@ -171,8 +154,6 @@ export const AreaRiservataLayout = ({
</div> </div>
</main> </main>
{!noFooter && <MiniFooter className={footerClassName} />} {!noFooter && <MiniFooter className={footerClassName} />}
{/* <CookieConsent /> */}
</div> </div>
); );
}; };
@ -214,132 +195,6 @@ export const AreaRiservataLayoutUserView = ({
</main> </main>
<MiniFooter /> <MiniFooter />
{/* <CookieConsent /> */}
</div>
);
};
const UserViewHeader = () => {
const data = useContext(UserViewContext);
const pathname = usePathname();
const router = useRouter();
const { mutate: swap } = api.auth.swapUser.useMutation({
onSuccess: async () => {
toast.success("Utente cambiato con successo");
await router.push("/area-riservata/dashboard");
router.reload();
},
});
if (!data) return <div>Errore</div>;
return (
<div className="mb-5 flex flex-col space-y-5">
<div className="flex flex-row flex-wrap items-baseline gap-3">
<div className="text-3xl">{data.username}</div>
<div className="text-muted-foreground text-sm">
{data.email} | {data.telefono}
</div>
<Link href={`https://wa.me/${data.telefono}`} target="_blank">
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp
</Button>
</Link>
</div>
<div className="flex flex-row flex-wrap gap-2">
<Link href={`/area-riservata/admin/user-view/edit-user/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("edit-user") ? "secondary" : "outline"}
>
<UserCog /> Profilo
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/servizio/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("servizio") ? "secondary" : "outline"}
>
<Tickets /> Servizi
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/ricerca/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("ricerca") ? "secondary" : "outline"}
>
<Search /> Ricerca
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/comunicazioni/${data.id}`}>
<Button
className="border"
size="sm"
variant={
pathname.includes("comunicazioni") ? "secondary" : "outline"
}
>
<Mail /> Comunicazioni
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/ordini/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("ordini") ? "secondary" : "outline"}
>
<ShoppingBag /> Ordini
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/allegati/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("allegati") ? "secondary" : "outline"}
>
<Paperclip /> Allegati
</Button>
</Link>
<Link
href={
data.chatid
? {
pathname: "/area-riservata/admin/chats",
query: { chatid: data.chatid },
}
: "/area-riservata/admin/chats"
}
target="_blank"
>
<Button
className="border"
size="sm"
variant={!data.chatid ? "ghost" : "outline"}
>
<MessagesSquare /> Chat
</Button>
</Link>
<Button
className="flex gap-2"
onClick={() =>
swap({
id: data.id,
})
}
size="sm"
>
<ExternalLink /> Login
</Button>
</div>
<Separator />
</div> </div>
); );
}; };

View file

@ -4,7 +4,7 @@ import toast from "react-hot-toast";
import { useSession } from "~/providers/SessionProvider"; import { useSession } from "~/providers/SessionProvider";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { UserWithAnagrafica } from "~/server/services/user.service"; import type { CompleteUserData } from "~/server/services/user.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { Confirm } from "../confirm"; import { Confirm } from "../confirm";
import { ImageFlbk } from "../ImageWithFallback"; import { ImageFlbk } from "../ImageWithFallback";
@ -24,7 +24,7 @@ import { UploadModal } from "../upload_modal";
export const DocumentiPersonali = ({ export const DocumentiPersonali = ({
userData, userData,
}: { }: {
userData: UserWithAnagrafica; userData: CompleteUserData;
}) => { }) => {
const utils = api.useUtils(); const utils = api.useUtils();
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({ const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({

View file

@ -0,0 +1,250 @@
import { format } from "date-fns";
import {
ExternalLink,
Mail,
MessagesSquare,
Paperclip,
Search,
ShoppingBag,
Tickets,
UserCog,
UserPen,
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useRouter } from "next/router";
import toast from "react-hot-toast";
import { useUserViewContext } from "~/lib/userViewContext";
import { api } from "~/utils/api";
import { WhatsAppIcon2 } from "../svgs";
import { Button } from "../ui/button";
import { Separator } from "../ui/separator";
export const UserViewHeader = () => {
const data = useUserViewContext();
const pathname = usePathname();
const router = useRouter();
const { mutate: swap } = api.auth.swapUser.useMutation({
onSuccess: async () => {
toast.success("Utente cambiato con successo");
await router.push("/area-riservata/dashboard");
router.reload();
},
});
if (!data) return <div>Errore</div>;
return (
<div className="mb-5 flex flex-col space-y-5">
<div className="flex flex-row flex-wrap items-baseline gap-3">
<div className="text-3xl">{data.username}</div>
<Link href={`https://wa.me/${data.telefono}`} target="_blank">
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp
</Button>
</Link>
{data.codice_fiscale ? (
<IntestazioneMaker />
) : (
<Button disabled size="sm">
<UserPen /> Intestazione
</Button>
)}
</div>
<div className="flex flex-row flex-wrap gap-2">
<Link href={`/area-riservata/admin/user-view/edit-user/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("edit-user") ? "secondary" : "outline"}
>
<UserCog /> Profilo
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/servizio/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("servizio") ? "secondary" : "outline"}
>
<Tickets /> Servizi
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/ricerca/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("ricerca") ? "secondary" : "outline"}
>
<Search /> Ricerca
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/comunicazioni/${data.id}`}>
<Button
className="border"
size="sm"
variant={
pathname.includes("comunicazioni") ? "secondary" : "outline"
}
>
<Mail /> Comunicazioni
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/ordini/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("ordini") ? "secondary" : "outline"}
>
<ShoppingBag /> Ordini
</Button>
</Link>
<Link href={`/area-riservata/admin/user-view/allegati/${data.id}`}>
<Button
className="border"
size="sm"
variant={pathname.includes("allegati") ? "secondary" : "outline"}
>
<Paperclip /> Allegati
</Button>
</Link>
<Link
href={
data.chatid
? {
pathname: "/area-riservata/admin/chats",
query: { chatid: data.chatid },
}
: "/area-riservata/admin/chats"
}
target="_blank"
>
<Button
className="border"
size="sm"
variant={!data.chatid ? "ghost" : "outline"}
>
<MessagesSquare /> Chat
</Button>
</Link>
<Button
className="flex gap-2"
onClick={() =>
swap({
id: data.id,
})
}
size="sm"
>
<ExternalLink /> Login
</Button>
</div>
<Separator />
</div>
);
};
const VOCALI = ["A", "E", "I", "O", "U"] as const;
const IntestazioneMaker = () => {
const data = useUserViewContext();
const { data: comuni = [] } = api.catasto.searchComuniByCatasto.useQuery({
catasto: [data?.luogo_nascita || "", data?.comune_residenza || ""].filter(
(f) => f !== "",
),
});
const isDisabled =
!data.sesso ||
!data.cognome ||
!data.nome ||
!data.data_nascita ||
!data.luogo_nascita ||
!data.comune_residenza ||
!data.via_residenza ||
!data.civico_residenza;
const copyToClipboard = () => {
if (
!data.sesso ||
!data.cognome ||
!data.nome ||
!data.data_nascita ||
!data.luogo_nascita ||
!data.comune_residenza ||
!data.via_residenza ||
!data.civico_residenza
) {
toast.error("Dati mancanti");
return;
}
const comune_nascita = comuni.find((c) => c.catasto === data.luogo_nascita);
const comune_residenza = comuni.find(
(c) => c.catasto === data.comune_residenza,
);
if (!comune_nascita || !comune_residenza) {
toast.error("Errore comune non trovato");
return;
}
// Format names
const cognome = titleCase(data.cognome);
const nome = titleCase(data.nome);
const viaResidenza = titleCase(data.via_residenza);
const comuneNascita = titleCase(comune_nascita.nome);
const comuneResidenza = titleCase(comune_residenza.nome);
// Determine article
const isMale = data.sesso === "M";
const articolo = isMale ? "Al Sig." : "Alla Sig.ra";
const natoNata = isMale ? "nato" : "nata";
const preposizione = VOCALI.includes(comuneNascita[0]?.toUpperCase() || "")
? "ad"
: "a";
// Build phrase
const frase = [
`${articolo} ${cognome} ${nome}`,
`${natoNata} il ${format(data.data_nascita, "dd/MM/yyyy")}`,
`${preposizione} ${comuneNascita} (${comune_nascita.sigla}),`,
`residente in ${viaResidenza}, ${data.civico_residenza}`,
`a ${comuneResidenza} (${data.provincia_residenza})`,
`CAP ${data.cap_residenza},`,
`con codice fiscale: ${data.codice_fiscale}`,
].join(" ");
navigator.clipboard
.writeText(frase)
.then(() => {
toast.success("Intestazione copiata negli appunti");
})
.catch(() => {
toast.error("Errore nella copia");
});
};
return (
<Button disabled={isDisabled} onClick={copyToClipboard} size="sm">
<UserPen /> Intestazione
</Button>
);
};
function titleCase(str: string | null | undefined): string {
if (!str) return "";
return str
.trim()
.toLowerCase()
.split(/\s+/) // Handle multiple spaces
.map((word) => {
if (word.length === 0) return word;
return word.charAt(0).toUpperCase() + word.slice(1);
})
.join(" ");
}

View file

@ -34,7 +34,7 @@ import { cn } from "~/lib/utils";
import { useZodForm } from "~/lib/zodForm"; import { useZodForm } from "~/lib/zodForm";
import { useCatasto } from "~/providers/CatastoProvider"; import { useCatasto } from "~/providers/CatastoProvider";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import type { UserWithAnagrafica } from "~/server/services/user.service"; import type { CompleteUserData } from "~/server/services/user.service";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
const profileFormSchema = z.object({ const profileFormSchema = z.object({
@ -62,7 +62,7 @@ type ProfileFormValues = z.infer<typeof profileFormSchema>;
export const ProfileFormAnagrafica = ({ export const ProfileFormAnagrafica = ({
userData, userData,
}: { }: {
userData: UserWithAnagrafica; userData: CompleteUserData;
}) => { }) => {
const { comuni, nazioni } = useCatasto(); const { comuni, nazioni } = useCatasto();

View file

@ -1,11 +1,11 @@
import { createContext, type FC, type ReactNode } from "react"; import { createContext, type FC, type ReactNode, useContext } from "react";
import type { ChatsChatid } from "~/schemas/public/Chats"; import type { ChatsChatid } from "~/schemas/public/Chats";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import type { getUserReturnType } from "~/server/controllers/user.controller"; import type { UserWAnagrafica } from "~/server/controllers/user.controller";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
type UserViewContextType = getUserReturnType & { chatid: ChatsChatid | null }; type UserViewContextType = UserWAnagrafica & { chatid: ChatsChatid | null };
export const UserViewContext = createContext<UserViewContextType | null>(null); export const UserViewContext = createContext<UserViewContextType | null>(null);
@ -28,3 +28,13 @@ export const UserViewProvider: FC<{ children: ReactNode; userId: UsersId }> = ({
} }
return null; return null;
}; };
export const useUserViewContext = () => {
const context = useContext(UserViewContext);
if (!context) {
throw new Error(
"useUserViewContext must be used within a UserViewProvider",
);
}
return context;
};

View file

@ -6,6 +6,7 @@ import {
getNazioneById, getNazioneById,
getNazioni, getNazioni,
getProvincie, getProvincie,
searchComuniByCatasto,
} from "~/server/controllers/catasto.controller"; } from "~/server/controllers/catasto.controller";
import { zComuneId, zNazioneId } from "~/server/utils/zod_types"; import { zComuneId, zNazioneId } from "~/server/utils/zod_types";
@ -14,6 +15,15 @@ export const catastoRouter = createTRPCRouter({
getComuni: publicProcedure.query(async () => { getComuni: publicProcedure.query(async () => {
return await getComuni(); return await getComuni();
}), }),
searchComuniByCatasto: publicProcedure
.input(
z.object({
catasto: z.string().array(),
}),
)
.query(async ({ input }) => {
return await searchComuniByCatasto(input.catasto);
}),
getComuneById: publicProcedure getComuneById: publicProcedure
.input( .input(
z.object({ z.object({

View file

@ -15,7 +15,7 @@ import {
getUsersWithChatInfoHandler, getUsersWithChatInfoHandler,
} from "~/server/controllers/user.controller"; } from "~/server/controllers/user.controller";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { getUserWithAnagraficaHandler } from "~/server/services/user.service"; import { getClientProfilo } from "~/server/services/user.service";
import { zUserId } from "~/server/utils/zod_types"; import { zUserId } from "~/server/utils/zod_types";
export const usersRouter = createTRPCRouter({ export const usersRouter = createTRPCRouter({
@ -71,7 +71,7 @@ export const usersRouter = createTRPCRouter({
}), }),
) )
.query(async ({ input }) => { .query(async ({ input }) => {
return getUserWithAnagraficaHandler({ return getClientProfilo({
db, db,
userId: input.userId, userId: input.userId,
}); });

View file

@ -15,6 +15,20 @@ export const getComuni = async () => {
} }
}; };
export const searchComuniByCatasto = async (catasto: string[]) => {
try {
return await db
.selectFrom("comuni")
.selectAll()
.where("catasto", "in", catasto)
.execute();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while getting Comuni: ${(e as Error).message}`,
});
}
};
export const getComuneById = async (id: ComuniId) => { export const getComuneById = async (id: ComuniId) => {
try { try {
return await db return await db

View file

@ -1,5 +1,5 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { Users, UsersId, UsersUpdate } from "~/schemas/public/Users"; import type { UsersId, UsersUpdate } from "~/schemas/public/Users";
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import type { Context } from "~/server/api/trpc"; import type { Context } from "~/server/api/trpc";
import { db, type Querier } from "~/server/db"; import { db, type Querier } from "~/server/db";
@ -166,10 +166,11 @@ export const getUserHandler = async ({
}: { }: {
db: Querier; db: Querier;
id: UsersId; id: UsersId;
}): Promise<Users | null> => { }) => {
try { try {
const user = await db const user = await db
.selectFrom("users") .selectFrom("users")
.leftJoin("users_anagrafica", "users_anagrafica.userid", "users.id")
.where("id", "=", id) .where("id", "=", id)
.selectAll() .selectAll()
.executeTakeFirst(); .executeTakeFirst();
@ -185,7 +186,7 @@ export const getUserHandler = async ({
}); });
} }
}; };
export type getUserReturnType = Awaited<ReturnType<typeof getUserHandler>>; export type UserWAnagrafica = Awaited<ReturnType<typeof getUserHandler>>;
export type NonNullableUser = { export type NonNullableUser = {
id: NonNullable<UsersId>; id: NonNullable<UsersId>;

View file

@ -98,10 +98,8 @@ export const addUser = async ({
} }
}; };
export type UserWithAnagrafica = Awaited< export type CompleteUserData = Awaited<ReturnType<typeof getClientProfilo>>;
ReturnType<typeof getUserWithAnagraficaHandler> export const getClientProfilo = async ({
>;
export const getUserWithAnagraficaHandler = async ({
db, db,
userId, userId,
}: { }: {