Refactor and enhance UI components across the application

- Renamed `AnnunciInConferma` to `AnnunciInConfermaDialog` and `AnnunciSelezionati` to `AnnunciSelezionatiDialog` for clarity.
- Simplified button notifications for announcements by always displaying the count.
- Introduced `AnnunciInConfermaAccordion` and `AnnunciSelezionatiAccordion` components to manage accordion behavior for announcements.
- Updated button styles to remove unnecessary flex properties for better alignment.
- Improved form validation logic in `FormNewServizioAcquisto` for better clarity and performance.
- Enhanced the `UploadModal` to accept props directly, simplifying the component structure.
- Added a new `CardAnnuncio` component for displaying announcements with improved layout and functionality.
- Updated global styles to include a new background color variable.
- Refactored API routes and controllers for better clarity and maintainability.
- Cleaned up various components by removing redundant class names and improving accessibility.
This commit is contained in:
Marco Pedone 2025-11-03 10:59:45 +01:00
parent 97503b34e5
commit 6ad74c9002
43 changed files with 829 additions and 416 deletions

View file

@ -109,7 +109,6 @@
"noDangerouslySetInnerHtml": "off"
},
"style": {
"noNestedTernary": "info",
"noUselessElse": "on",
"noYodaExpression": "on",
"useThrowNewError": "on",

View file

@ -199,11 +199,7 @@ const UserViewHeader = () => {
</div>
<Link href={`https://wa.me/${data.telefono}`} target="_blank">
<Button
className="flex items-center gap-2"
size="sm"
variant="success"
>
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp
</Button>
@ -212,7 +208,7 @@ const UserViewHeader = () => {
<div className="flex flex-row flex-wrap gap-2">
<Link href={`/area-riservata/admin/user-view/edit-user/${data.id}`}>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={pathname.includes("edit-user") ? "secondary" : "outline"}
>
@ -221,7 +217,7 @@ const UserViewHeader = () => {
</Link>
<Link href={`/area-riservata/admin/user-view/servizio/${data.id}`}>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={pathname.includes("servizio") ? "secondary" : "outline"}
>
@ -231,7 +227,7 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/ricerca/${data.id}`}>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={pathname.includes("ricerca") ? "secondary" : "outline"}
>
@ -240,7 +236,7 @@ const UserViewHeader = () => {
</Link>
<Link href={`/area-riservata/admin/user-view/comunicazioni/${data.id}`}>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={
pathname.includes("comunicazioni") ? "secondary" : "outline"
@ -252,7 +248,7 @@ const UserViewHeader = () => {
<Link href={`/area-riservata/admin/user-view/ordini/${data.id}`}>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={pathname.includes("ordini") ? "secondary" : "outline"}
>
@ -261,7 +257,7 @@ const UserViewHeader = () => {
</Link>
<Link href={`/area-riservata/admin/user-view/allegati/${data.id}`}>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={pathname.includes("allegati") ? "secondary" : "outline"}
>
@ -281,7 +277,7 @@ const UserViewHeader = () => {
target="_blank"
>
<Button
className="flex items-center gap-2 border"
className="border"
size="sm"
variant={!data.chatid ? "ghost" : "outline"}
>

View file

@ -146,7 +146,7 @@ const PricePreparation = ({
href={`/servizio/condizioni/${prezziario.testo_condizioni}`}
target="_blank"
>
<Button className="flex w-full items-center gap-2 text-sm">
<Button className="w-full text-sm">
<span>{t.acquisto.leggi_condizioni}</span>
<ExternalLink />
</Button>
@ -154,7 +154,7 @@ const PricePreparation = ({
)}
<Button
className="flex w-full items-center gap-2 text-lg"
className="w-full text-lg"
disabled={isLoading || !prezziario}
onClick={onProcedi}
size="xl"

View file

@ -1,4 +1,4 @@
import { CardAnnuncio } from "~/components/annuncio_card";
import { CardAnnuncio } from "~/components/annuncio_card2";
import { getTitoloTranslation } from "~/lib/annuncio_details";
import { useTranslation } from "~/providers/I18nProvider";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";

View file

@ -54,7 +54,7 @@ export const CTA_TipologiaModal = () => {
open={open}
>
<DialogTrigger asChild>
<Button className="flex items-center gap-2" variant="info">
<Button variant="info">
<CircleHelp />{" "}
<span className="hidden sm:block">
{t.tipologia_tutorial.btn_title}
@ -78,7 +78,7 @@ export const CTA_TipologiaModal = () => {
<p>{t.tipologia_tutorial.transitorio_desc}</p>
<Button
aria-label={t.tipologia_tutorial.transitorio_cta}
className="flex w-full items-center gap-2 bg-transitorio"
className="w-full bg-transitorio"
onClick={async () => await update("Transitorio")}
>
{t.tipologia_tutorial.transitorio_cta}
@ -93,7 +93,7 @@ export const CTA_TipologiaModal = () => {
<p>{t.tipologia_tutorial.stabile_desc}</p>
<Button
aria-label={t.tipologia_tutorial.stabile_cta}
className="flex w-full items-center gap-2 bg-stabile"
className="w-full bg-stabile"
onClick={async () => await update("Stabile")}
>
{t.tipologia_tutorial.stabile_cta}
@ -104,7 +104,7 @@ export const CTA_TipologiaModal = () => {
<Button
aria-label={t.tipologia_tutorial.alone}
className="flex w-full items-center gap-2"
className="w-full"
onClick={async () => await update(null)}
variant="outline"
>

View file

@ -100,7 +100,7 @@ const ServizioInteraction = ({
aria-label="Vai alla tua area riservata"
href={`/area-riservata/dashboard`}
>
<Button className="flex w-full items-center gap-2" variant="success">
<Button className="w-full" variant="success">
Vai alla tua area riservata <ArrowRight />
</Button>
</Link>
@ -128,7 +128,7 @@ const InteressatoButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
if (isLoading || hasInterest === undefined) {
return (
<Button className="flex w-full items-center gap-2" disabled>
<Button className="w-full" disabled>
<BadgePlus className="size-6" />
Caricamento...
</Button>
@ -146,7 +146,7 @@ const InteressatoButton = ({ annuncioId }: { annuncioId: AnnunciId }) => {
return (
<Button
aria-label="Sono interessato"
className="flex w-full items-center gap-2"
className="w-full"
//size="xl"
onClick={() => {
add({
@ -188,7 +188,7 @@ export const InteressatoButtonBatchV = ({
return (
<Button
aria-label="Sono interessato"
className="flex w-full items-center gap-2"
className="w-full"
//size="xl"
onClick={() => {
add({

View file

@ -18,10 +18,7 @@ export const ContattoAnnuncio = () => {
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
className="flex w-full items-center gap-2"
variant="destructive"
>
<Button className="w-full" variant="destructive">
<MessageCircleQuestion className="size-6" /> Contattaci
</Button>
</CredenzaTrigger>

View file

@ -89,7 +89,7 @@ export const CardAnnuncio = ({
<Carousel opts={{ loop: true }}>
<CarouselContent>
{images?.map((img, idx) => (
<CarouselItem className="" key={`${img.img}key`}>
<CarouselItem key={`${img.img}key`}>
<ImageFlbk
alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${img.thumb}?image=true`}

View file

@ -0,0 +1,404 @@
import { BedDouble, CalendarClock, Maximize2, Ruler } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { ImageFlbk } from "~/components/ImageWithFallback";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "~/components/ui/carousel";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { camereTesti, handleConsegna } from "~/lib/annuncio_details";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { VideoPlayer } from "./videoPlayer";
type CardAnnuncioProps = {
id: number;
codice: string;
prezzo: number;
titolo: string | null;
mq: string | null;
comune: string | null;
provincia: string | null;
consegna: number | null;
camere: number | null;
//immagini: string[] | undefined;
tipo: string | null;
stato: string | null;
className?: string;
videos?: string[] | undefined;
updated_at: Date | null;
images: { img: string; thumb: string }[];
};
export const CardAnnuncio = ({
id,
codice,
prezzo,
titolo,
mq,
comune,
provincia,
consegna,
camere,
//immagini,
tipo,
stato,
className,
videos,
updated_at,
images,
}: CardAnnuncioProps) => {
const { t } = useTranslation();
return (
<div
className={cn(
"h-[464px] rounded-md bg-white shadow-neutral-100 shadow-sm hover:shadow-md hover:outline-neutral-600",
className,
)}
id={`card-annuncio-${id}`}
>
<Link
className="block p-2"
//target="_blank"
href={`/annuncio/${codice}`} //duration-700 ease-in-out animate-in fade-in
>
<div className="group relative overflow-clip text-clip rounded-md">
<div className="absolute top-2 left-2 z-20 rounded-md bg-white px-2 py-1 font-semibold text-neutral-800 text-sm shadow-md shadow-neutral-300 backdrop-blur-sm">
Cod: {codice}
</div>
{stato === "Trattativa" && (
<div>
<div className="absolute z-20 h-56 w-full">
<div className="absolute bottom-0 w-full touch-none select-none rounded-b-md bg-violet-500 text-center text-lg text-white">
Annuncio in Trattativa
</div>
</div>
</div>
)}
<Carousel opts={{ loop: true }}>
<CarouselContent>
{images?.map((img, idx) => (
<CarouselItem key={`${img.img}key`}>
<ImageFlbk
alt={t.card.alt_immagine}
blurDataURL={`/storage-api/get/${img.thumb}?image=true`}
className={"h-64 w-full object-cover"}
height={1080}
priority={idx === 0}
src={`/storage-api/get/${img.img}?image=true`}
width={1920}
/>
</CarouselItem>
))}
{videos?.map((video) => {
if (!video) return null;
if (video.includes("youtu")) {
return null;
}
return (
<CarouselItem
className={cn("group relative")}
key={`videoplayer-${video}`}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<VideoPlayer
cacheKey={updated_at?.toString() || new Date().toString()}
className="h-56"
coverImage={
images?.[0]
? `/storage-api/get/${images[0].img}?image=true`
: ""
}
key={`videoplayer-${video}`}
videoSrc={video}
/>
</CarouselItem>
);
})}
</CarouselContent>
<div
className="block opacity-0 transition-all duration-200 group-hover:opacity-60 disabled:group-hover:opacity-0"
onClick={(e) => e.preventDefault()}
onKeyDown={(e) => e.stopPropagation()}
role="button"
tabIndex={0}
>
<CarouselPrevious
className="left-2 cursor-pointer"
disabled={!images || images.length === 1}
/>
<CarouselNext
className="right-2 cursor-pointer"
disabled={!images || images.length === 1}
/>
</div>
</Carousel>
</div>
<div
className={cn(
"mt-2 flex w-full items-center justify-center rounded-md",
tipo === "Transitorio" && "bg-transitorio",
tipo === "Stabile" && "bg-stabile",
tipo === "Cessione" && "bg-blue-400",
tipo === "Vendita" && "bg-green-400",
)}
>
<div className="font-semibold text-white">{tipo}</div>
</div>
<div className="mt-2 space-y-2">
<div className="flex flex-row justify-around">
<div className="text-center">
<span className="sr-only">{t.card.codice}</span>
<span className="font-semibold text-red-600 text-xl">
Cod: {codice}
</span>
</div>
<div className="text-center">
<span className="sr-only">{t.card.prezzo}</span>
<span className="font-semibold text-xl">
{formatCurrency(prezzo / 1e2)}
</span>
</div>
</div>
<div className="mb-3 text-center text-muted-foreground">
<span className="sr-only">{t.card.titolo}</span>
<span className="line-clamp-2 min-h-[3.5rem] font-medium text-base md:text-lg">
{titolo}
</span>
</div>
<div className="text-center text-muted-foreground">
<span className="sr-only">{t.card.indirizzo}</span>
<span className="font-medium text-sm">
{comune && provincia && `${comune} (${provincia})`}
</span>
</div>
</div>
<div className="mt-4 flex items-center justify-around gap-2 text-muted-foreground text-xs">
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
<Ruler className="size-4 text-indigo-700" />
<div>
<p className="font-medium">
{mq ? Number.parseFloat(mq) : 0}{" "}
<span>
m<sup>2</sup>
</span>
</p>
</div>
</div>
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
<BedDouble className="size-4 text-indigo-700" />
<div>
<p className="font-medium">
{camereTesti({ camere: camere, testi: t.card })}
</p>
</div>
</div>
<div className="flex w-full items-center justify-center gap-1 rounded-md bg-neutral-100 py-3">
<CalendarClock className="size-4 text-indigo-700" />
<div>
<p className="truncate font-medium">
{handleConsegna({
aggiornamento: t.card.in_aggiornamento,
consegna: consegna,
consegna_da: t.card.consegna_da,
mesi: t.preferenze.mesi,
subito: t.card.consegna_subito,
})}
</p>
</div>
</div>
</div>
</Link>
</div>
);
};
export const CarouselAnnuncio = ({
single,
immagini,
videos,
updated_at,
}: {
single?: boolean;
immagini: string[] | null;
videos: string[] | null;
updated_at: Date | null;
}) => {
const [openModal, setOpenModal] = useState(false);
const [idxModal, setIdxModal] = useState(0);
function handleOpenModal(position: number) {
setIdxModal(position);
setOpenModal(true);
}
return (
<>
<div className="mx-auto my-4 w-full text-clip rounded-md">
<div className="relative">
<Carousel opts={{ align: "start", loop: true }}>
<CarouselContent>
{immagini?.map((img, idx) => (
<CarouselItem
className={cn(
"group relative flex items-center justify-center",
immagini && immagini.length === 1
? ""
: !single && "md:basis-1/2 lg:basis-1/3",
)}
key={`${img}-cF`}
>
<ImageFlbk
alt={img}
className={cn(
"aspect-square max-h-92 w-full rounded-md object-cover sm:max-h-80",
immagini.length === 1 && "object-contain",
)}
height={400}
onClick={() => handleOpenModal(idx)}
priority={idx === 0}
src={`/storage-api/get/${img}?image=true&${updated_at?.toString() || new Date().toString()}`}
width={800}
/>
<Maximize2
className="absolute right-2 bottom-2 size-7 origin-bottom-right cursor-pointer rounded-md bg-white/70 stroke-2 p-1 transition-all duration-300 ease-in-out hover:scale-150 group-hover:opacity-100 sm:opacity-0"
onClick={() => handleOpenModal(idx)}
/>
</CarouselItem>
))}
{videos?.map((video, idx) => {
if (!video) return null;
if (video.includes("youtu")) {
return null;
}
return (
<CarouselItem
className={cn(
"group relative",
immagini && immagini.length === 1
? ""
: !single && "md:basis-1/2 lg:basis-1/3",
)}
key={`videoplayer-${video}`}
>
<VideoPlayer
cacheKey={updated_at?.toString() || new Date().toString()}
className="h-72"
coverImage={
immagini?.[0]
? `/storage-api/get/${immagini[0]}?image=true`
: ""
}
key={`videoplayer-${idx}-${openModal}`}
videoSrc={video}
/>
<Maximize2
className="absolute right-2 bottom-2 size-7 origin-bottom-right cursor-pointer rounded-md bg-white/70 stroke-2 p-1 transition-all duration-300 ease-in-out hover:scale-150 group-hover:opacity-100 sm:opacity-0"
onClick={() =>
handleOpenModal(immagini?.length || 0 + idx)
}
/>
</CarouselItem>
);
})}
</CarouselContent>
<CarouselPrevious
className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
<CarouselNext
className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
</Carousel>
</div>
</div>
<Dialog onOpenChange={setOpenModal} open={openModal}>
<DialogContent className="flex h-full w-full max-w-full items-center justify-center border-transparent border-none bg-white p-0 md:max-w-[80vw] md:p-6 [&_#dialog-close>svg]:size-8">
<DialogHeader className="absolute top-0 right-0 left-0 z-10">
<DialogTitle className="sr-only">Image Dialog</DialogTitle>
<DialogDescription className="sr-only">Immagine</DialogDescription>
</DialogHeader>
<Carousel opts={{ loop: true, startIndex: idxModal }}>
<CarouselContent>
{immagini?.map((img, idx) => (
<CarouselItem key={`${img}-cD`}>
<Image
alt={`carousel-img-${idx}`}
className="mx-auto h-[90vh] w-full rounded-md object-contain sm:w-[80vw]"
height={1080}
src={`/storage-api/get/${img}?image=true&${updated_at?.toString() || new Date().toString()}`}
width={1920}
/>
</CarouselItem>
))}
{videos?.map((video) => {
if (!video) return null;
if (video.includes("youtu")) {
return null;
}
return (
<CarouselItem
className="relative flex items-center justify-center"
key={`carousel-videoplayer-${video}`}
>
<VideoPlayer
cacheKey={updated_at?.toString() || new Date().toString()}
className="absolute mx-auto h-[90vh] w-full max-w-fit rounded-md object-contain"
coverImage={
immagini?.[0]
? `/storage-api/get/${immagini[0]}?image=true`
: ""
}
videoSrc={video}
/>
</CarouselItem>
);
})}
</CarouselContent>
<CarouselPrevious
className="left-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
<CarouselNext
className="right-2 cursor-pointer opacity-60 disabled:opacity-0"
disabled={!immagini || immagini.length === 1}
type="button"
/>
</Carousel>
</DialogContent>
</Dialog>
</>
);
};

View file

@ -30,7 +30,7 @@ export const AdminDashboard = () => {
<div className="text-red-500">Errore caricamento statistiche</div>
) : (
<div className="grid grid-cols-1 gap-4 px-2 pt-4 sm:grid-cols-2 sm:px-4">
<Card className="">
<Card>
<CardHeader>
<CardTitle>Utenti</CardTitle>
<CardDescription>

View file

@ -145,7 +145,7 @@ export const OrdiniModal = ({
href={`/area-riservata/payment-recap/${data.ordine_id}`}
target="_blank"
>
<Button className="flex items-center gap-2" variant="outline">
<Button variant="outline">
Ricevuta di cortesia <ReceiptText />
</Button>
</Link>

View file

@ -102,7 +102,7 @@ export const ChatAttachments = ({
target="_blank"
>
<Button
className="flex h-8 items-center gap-2 truncate p-0 text-base has-[>svg]:px-0"
className="h-8 truncate p-0 text-base has-[>svg]:px-0"
variant="link"
>
<ExtIcon mimeType={file.mimeType} />

View file

@ -157,7 +157,7 @@ const FormMessage = React.forwardRef<
return (
<p
className={cn(
"font-medium text-red-500 text-sm dark:text-red-600",
"font-medium text-red-500 text-sm leading-none dark:text-red-600",
className,
)}
id={formMessageId}

View file

@ -107,12 +107,7 @@ export const OnboardTutorial = () => {
</div>
</AlertDialogHeader>
<AlertDialogFooter>
<Button
className="flex items-center gap-2"
onClick={handleClose}
size="lg"
variant="success"
>
<Button onClick={handleClose} size="lg" variant="success">
<span>{t.onboarding_tutorial.button_text}</span>
<ArrowRight className="size-5" />
</Button>

View file

@ -200,7 +200,7 @@ export const PricingComponent = ({
href={"/prezzi#contratti"}
id="contratto-link"
>
<Button className="flex items-center gap-2">
<Button>
Più info <ExternalLink className="size-4" />
</Button>
</Link>

View file

@ -216,7 +216,7 @@ function SchedaAnnuncio({ data }: { data: Annunci }) {
className="flex items-center justify-start gap-2"
key={item.text}
>
<span className="">
<span>
<IconMatrix type={item.icon} />
</span>
<span>

View file

@ -7,6 +7,7 @@ import {
FolderKanban,
Pen,
Plus,
ShieldUser,
Trash2,
Wrench,
} from "lucide-react";
@ -99,7 +100,7 @@ export const AnnuncioActions = () => {
href={`https://win-jk822fggr6b:480/id/${data.gestionale_id}`}
target="_blank"
>
<Button className="flex items-center gap-2">
<Button>
<FolderKanban className="size-5" /> Gestionale
</Button>
</Link>
@ -107,18 +108,29 @@ export const AnnuncioActions = () => {
</>
)}
{isAdmin && (
<>
<Popover>
<PopoverTrigger asChild>
<Button>
<ShieldUser />
<span>Admin</span>
</Button>
</PopoverTrigger>
<PopoverContent
align="end"
className="flex w-fit flex-col items-center gap-2"
>
<Link
href={`/area-riservata/admin/edit-annuncio/${annuncioId}`}
target="_blank"
>
<Button
aria-label="Edit Annuncio"
className="flex w-full items-center gap-2"
className="w-full"
size="sm"
variant="secondary"
>
<Wrench className="size-6" />
<Wrench />
<span>Modifica Annuncio</span>
</Button>
</Link>
@ -134,14 +146,16 @@ export const AnnuncioActions = () => {
>
<Button
aria-label="Delete Annuncio"
className="flex items-center gap-2"
className="w-full"
size="sm"
variant="destructive"
>
<Trash2 className="size-6" />
<Trash2 />
<span>Elimina Annuncio</span>
</Button>
</Confirm>
</>
</PopoverContent>
</Popover>
)}
</div>
);
@ -232,7 +246,6 @@ const ContrattoImport = ({
<div className="flex items-center gap-3">
<Button
aria-label="Import Contratto"
className="flex items-center gap-2"
onClick={async () => {
const clipboardText = await navigator.clipboard.readText();
@ -245,7 +258,6 @@ const ContrattoImport = ({
</Button>
<Button
aria-label="Reset Contratto"
className="flex items-center gap-2"
onClick={async () => {
setContratto({
contratto_decorrenza: null,
@ -445,7 +457,7 @@ const AdminContratto = () => {
<CredenzaTrigger asChild>
<Button
aria-label="Open Contratto Section"
className="flex w-fit items-center gap-2"
className="w-fit"
variant={data.doc_contratto_added ? "success" : "secondary"}
>
{data.doc_contratto_added ? (
@ -709,7 +721,7 @@ const AdminLavoraConferma = () => {
<CredenzaTrigger asChild>
<Button
aria-label="Open Lavora Conferma Section"
className="flex w-fit items-center gap-2"
className="w-fit"
variant={data.hasConfermaAdmin ? "success" : "secondary"}
>
{data.hasConfermaAdmin ? (
@ -983,7 +995,6 @@ const Caparra = ({
<div className="flex flex-wrap items-center gap-3">
<Button
aria-label="Import Caparra"
className="flex items-center gap-2"
onClick={async () => {
const clipboardText = await navigator.clipboard.readText();
@ -996,7 +1007,6 @@ const Caparra = ({
</Button>
<Button
aria-label="Reset Caparra"
className="flex items-center gap-2"
onClick={async () => {
setCaparra({
caparra_causale: null,

View file

@ -120,10 +120,7 @@ export const BasicAnnuncioCard = ({
aria-label="Pagina Annuncio"
href={`/annuncio/${data.codice}`}
>
<Button
className="flex items-center gap-2"
variant="outline"
>
<Button variant="outline">
<ExternalLink className="size-6" />
</Button>
</Link>
@ -159,11 +156,7 @@ const AnnuncioDettaglio = ({ data }: { data: AnnuncioData }) => {
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Dettagli Annuncio"
className="flex items-center gap-2"
variant="outline"
>
<Button aria-label="Dettagli Annuncio" variant="outline">
<UnfoldVertical className="size-6" /> Dettagli annuncio
</Button>
</CredenzaTrigger>

View file

@ -58,10 +58,7 @@ export const Interactions = () => {
aria-label="Conferma immobile"
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
>
<Button
className="flex w-fit items-center gap-2"
variant="success"
>
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Confermato - Procedi ora
</Button>
@ -71,7 +68,7 @@ export const Interactions = () => {
<Tooltip>
<TooltipTrigger asChild>
<Button
className="flex w-fit cursor-not-allowed items-center gap-2 opacity-50"
className="w-fit cursor-not-allowed opacity-50"
variant="success"
>
<Pointer className="size-6" />
@ -92,7 +89,7 @@ export const Interactions = () => {
aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="flex w-fit items-center gap-2" variant="success">
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Rivedi la conferma
</Button>
@ -136,7 +133,7 @@ export const Interactions = () => {
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="flex w-fit items-center gap-2">
<Button className="w-fit">
<FileText className="size-6" />
Visualizza il contratto
</Button>
@ -145,7 +142,7 @@ export const Interactions = () => {
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="flex w-fit cursor-not-allowed items-center gap-2 opacity-50">
<Button className="w-fit cursor-not-allowed opacity-50">
<FileText className="size-6" />
Visualizza il contratto
</Button>
@ -172,7 +169,7 @@ export const Interactions = () => {
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="flex w-fit items-center gap-2">
<Button className="w-fit">
<FileBadge className="size-6" />
Visualizza la registrazione
</Button>
@ -181,7 +178,7 @@ export const Interactions = () => {
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="flex w-fit cursor-not-allowed items-center gap-2 opacity-50">
<Button className="w-fit cursor-not-allowed opacity-50">
<FileBadge className="size-6" />
Visualizza la registrazione
</Button>
@ -230,7 +227,7 @@ export const SaldoButton = ({
return (
<Button
aria-label="Procedi al saldo"
className={cn("flex w-fit items-center gap-2", className)}
className={cn("w-fit", className)}
onClick={() => {
mutate({
annuncioId,
@ -271,7 +268,7 @@ const SaldoConsulenzaButton = ({
return (
<Button
aria-label="Procedi al saldo consulenza"
className={cn("flex w-fit items-center gap-2", className)}
className={cn("w-fit", className)}
onClick={() => {
mutate({
annuncioId,
@ -311,10 +308,7 @@ const AnnuncioConfirm = () => {
return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button
aria-label="Conferma immobile"
className="flex w-fit items-center gap-2"
>
<Button aria-label="Conferma immobile" className="w-fit">
<Handshake className="size-6" /> Conferma immobile
</Button>
</CredenzaTrigger>
@ -356,10 +350,7 @@ const ConfermaLavorazioneModal = () => {
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Conferma in lavorazione"
className="flex w-fit items-center gap-2"
>
<Button aria-label="Conferma in lavorazione" className="w-fit">
<CircleCheck className="size-7" />
Conferma in lavorazione
</Button>

View file

@ -1,3 +1,4 @@
import { add } from "date-fns";
import { Package, PackageCheck, UserCircle } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
@ -5,8 +6,7 @@ import { useEffect } from "react";
import toast from "react-hot-toast";
import { AnnuncioCard } from "~/components/servizio/annuncio_card";
import { SaldoButton } from "~/components/servizio/interactions";
import { ServizioDuration } from "~/components/servizio/service-duration-display";
import { ServizioActions2 } from "~/components/servizio/servizio_actions";
import { ServizioActions } from "~/components/servizio/servizio_actions";
import { Button } from "~/components/ui/button";
import {
Card,
@ -24,7 +24,8 @@ import {
import { api } from "~/utils/api";
import {
AnnunciCompatibili,
AnnunciSelezionati,
AnnunciSelezionatiAccordion,
AnnunciSelezionatiDialog,
} from "./servizio_annunci_accordions";
const ServizioCard = ({
@ -42,7 +43,10 @@ const ServizioCard = ({
}) => {
return (
<Card
className={cn("gap-2 border-neutral-200 bg-neutral-200", cardClassName)}
className={cn(
"gap-2 rounded-md border-neutral-200 bg-neutral-200",
cardClassName,
)}
>
<CardHeader
className={cn(
@ -149,7 +153,7 @@ export const ServizioContent = () => {
</Button>
)}
</div>
{isAdmin && <AnnunciSelezionati annunci={servizio.annunci} />}
{isAdmin && <AnnunciSelezionatiDialog annunci={servizio.annunci} />}
</div>
}
header={
@ -157,7 +161,7 @@ export const ServizioContent = () => {
<span className="text-foreground/70 text-lg">
Servizio non attivo
</span>
<ServizioActions2 />
<ServizioActions />
</div>
}
/>
@ -183,7 +187,7 @@ export const ServizioContent = () => {
}
header={
<>
<ServizioActions2 />
<ServizioActions />
</>
}
/>
@ -192,7 +196,6 @@ export const ServizioContent = () => {
const openContattiAnnunci = servizio.annunci.filter(
(a) => a.open_contatti_at !== null && a.user_confirmed_at === null,
);
const annunciInConferma = servizio.annunci.filter(
@ -206,19 +209,27 @@ export const ServizioContent = () => {
<ServizioCard
content={
<>
<div className="flex flex-col gap-2">
<div className="flex w-full flex-col gap-4 sm:flex-row sm:gap-8">
<div className="flex flex-2 flex-col gap-2 sm:max-w-1/2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCircle className="size-6 text-primary" />
<span className="font-medium">Contatti sbloccati</span>
</div>
<span className="text-sm">{openContattiAnnunci.length} / 10</span>
<span className="text-sm">
{openContattiAnnunci.length} / 10
</span>
</div>
<Progress className="h-2" value={openContattiAnnunci.length * 10} />
<Progress
className="h-2"
value={openContattiAnnunci.length * 10}
/>
</div>
<span>
{`Durata servizio: ${new Date(servizio.decorrenza).toLocaleDateString("it-IT")} - ${new Date(add(servizio.decorrenza, { days: 60 })).toLocaleDateString("it-IT")}`}
</span>
{/* <ServizioDuration decorrenza={servizio.decorrenza} /> */}
</div>
<ServizioDuration decorrenza={servizio.decorrenza} />
{annuncioConfermato.length > 0 && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
@ -256,15 +267,6 @@ export const ServizioContent = () => {
</div>
)}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 overflow-y-auto">
{openContattiAnnunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider>
);
})}
</div>
<div className="flex flex-col gap-2 overflow-y-auto">
{annunciInConferma.map((data) => {
return (
@ -275,8 +277,8 @@ export const ServizioContent = () => {
})}
</div>
<div className="flex flex-col items-center gap-4 sm:flex-row">
<AnnunciSelezionati annunci={annunciNotInConferma} />
<AnnunciSelezionatiAccordion annunci={annunciNotInConferma} />
<div className="flex items-center justify-center">
<AnnunciCompatibili />
</div>
</div>
@ -284,7 +286,7 @@ export const ServizioContent = () => {
}
header={
<>
<ServizioActions2 />
<ServizioActions />
</>
}
/>

View file

@ -32,13 +32,19 @@ export const ContattiProprietarioModal = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const isUnlocked = data.open_contatti_at != null;
const needsDocuments =
!servizio.skipControlloDoc &&
(!servizio.doc_personale_fronte || !servizio.doc_personale_retro);
return (
<Credenza>
<CredenzaTrigger asChild>
{data.open_contatti_at != null ? (
<Button
aria-label="Vedi contatti proprietario"
className="flex w-fit items-center gap-2"
className="w-fit"
variant="success"
>
<CircleUser className="size-7" />
@ -47,7 +53,7 @@ export const ContattiProprietarioModal = () => {
) : (
<Button
aria-label="Contatta proprietario"
className="flex w-fit items-center gap-2"
className="w-fit"
variant="warning"
>
<CircleUser className="size-7" />
@ -55,21 +61,19 @@ export const ContattiProprietarioModal = () => {
</Button>
)}
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaContent className="max-h-[90vh] sm:max-w-2xl">
<CredenzaHeader>
<CredenzaTitle>{t.contatto.title}</CredenzaTitle>
<CredenzaTitle className="text-xl">{t.contatto.title}</CredenzaTitle>
<CredenzaDescription className="sr-only">
{t.contatto.descrizione}
</CredenzaDescription>
</CredenzaHeader>
{servizio.doc_personale_fronte == null ||
servizio.doc_personale_retro == null ? (
{needsDocuments ? (
<DocCheckpoint />
// biome-ignore lint/style/noNestedTernary: <this is fine>
) : data.open_contatti_at == null ? (
<PreUnlock />
) : (
) : isUnlocked ? (
<PostUnlock />
) : (
<PreUnlock />
)}
</CredenzaContent>
</Credenza>
@ -88,22 +92,30 @@ const DocComponent = ({
}) => {
const { isAdmin, userId } = useServizio();
if (!docInfo?.storageId) {
return <UploadModal cb_onUpload={cb} isAdmin={isAdmin} userId={userId} />;
}
if (docInfo?.storageId) {
return (
<div className="flex items-center gap-2 text-green-500">
<CircleCheck className="size-5" />
<span>Documento caricato</span>
</div>
);
}
return (
<UploadModal
cb_onUpload={cb}
isAdmin={isAdmin}
maxFiles={1}
userId={userId}
/>
);
};
const DocCheckpoint = () => {
const { t } = useTranslation();
const { userId, servizio } = useServizio();
const utils = api.useUtils();
const { mutate } = api.servizio.updateServizio.useMutation({
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId,
@ -117,37 +129,38 @@ const DocCheckpoint = () => {
toast.error(`Errore aggiornamento servizio: ${err.message}`);
},
});
const handleUpload = (
field: "doc_personale_fronte_ref" | "doc_personale_retro_ref",
id: string,
) => {
mutate({
servizioId: servizio.servizio_id,
data: { [field]: id },
});
};
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="space-y-5">
<p>Devi ancora caricare un tuo documento</p>
<div className="flex items-center gap-2">
Fronte documento:{" "}
<div className="flex flex-col gap-2">
<span>Fronte documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
mutate({
servizioId: servizio.servizio_id,
data: {
doc_personale_fronte_ref: userStorageIds[0],
},
})
userStorageIds[0] &&
handleUpload("doc_personale_fronte_ref", userStorageIds[0])
}
docInfo={servizio.doc_personale_fronte}
/>
</div>
<div className="flex items-center gap-2">
Retro documento:{" "}
<div className="flex flex-col gap-2">
<span>Retro documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
mutate({
servizioId: servizio.servizio_id,
data: {
doc_personale_retro_ref: userStorageIds[0],
},
})
userStorageIds[0] &&
handleUpload("doc_personale_retro_ref", userStorageIds[0])
}
docInfo={servizio.doc_personale_retro}
/>
@ -166,7 +179,7 @@ const DocCheckpoint = () => {
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
<Button aria-label="Chiudi" disabled={isPending} variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
@ -182,7 +195,8 @@ const PreUnlock = () => {
const utils = api.useUtils();
const { mutate } = api.servizio.unlockServizioContatti.useMutation({
const { mutate, isPending } = api.servizio.unlockServizioContatti.useMutation(
{
onError: (err) => {
toast.error(err.message, {
id: "unlock-contacts",
@ -207,11 +221,11 @@ const PreUnlock = () => {
userId,
});
},
});
},
);
const d = new Date();
const tooEarly = d.getHours() < 9;
const tooLate = d.getHours() >= 20;
const currentHour = new Date().getHours();
const isOutsideBusinessHours = currentHour < 9 || currentHour >= 20;
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
@ -221,13 +235,12 @@ const PreUnlock = () => {
<p className="text-muted-foreground text-xs">
{t.contatto.disclaimer3}
</p>
{(tooEarly || tooLate) && (
{isOutsideBusinessHours && (
<div className="flex items-center gap-2 rounded-md bg-yellow-200 p-2">
<TriangleAlert className="size-8" />
{/*TODO testare */}
<p>
Il proprietario riceverà la notifica alle ore 9.00{" "}
{tooLate && "di domani"}
{currentHour >= 20 && "di domani"}
</p>
</div>
)}
@ -236,6 +249,7 @@ const PreUnlock = () => {
<CredenzaFooter>
<Button
aria-label="Contatta proprietario"
disabled={isPending}
onClick={() =>
mutate({
annuncioId,
@ -248,7 +262,11 @@ const PreUnlock = () => {
</Button>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
<Button
aria-label="Chiudi modal credenza"
disabled={isPending}
variant="outline"
>
{t.chiudi}
</Button>
</CredenzaClose>
@ -258,18 +276,14 @@ const PreUnlock = () => {
};
function getNavigationUrl(lat: string, lon: string): string {
if (typeof window !== "undefined") {
const isIOS =
navigator.userAgent.toUpperCase().includes("MAC") ||
navigator.userAgent.toUpperCase().includes("IPAD") ||
navigator.userAgent.toUpperCase().includes("IPHONE") ||
navigator.userAgent.toUpperCase().includes("IOS") ||
navigator.userAgent.toUpperCase().includes("IPOD");
return `${isIOS ? "maps" : "https"}://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`;
}
if (typeof window === "undefined") {
return `https://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`;
}
const isIOS = /iPad|iPhone|iPod|Mac/.test(navigator.userAgent);
const protocol = isIOS ? "maps" : "https";
return `${protocol}://www.google.com/maps/dir/?api=1&travelmode=driving&layer=traffic&destination=${lat},${lon}`;
}
const PostUnlock = () => {
const { servizioId } = useServizio();
@ -290,80 +304,86 @@ const PostUnlock = () => {
if (isLoading) {
return (
<div className="flex justify-center">
<CredenzaBody className="flex justify-center p-8">
<LoadingPage />
</div>
</CredenzaBody>
);
}
if (!data || !data.propData) {
return <p>{t.contatto.error}</p>;
return (
<CredenzaBody>
<p className="text-destructive">{t.contatto.error}</p>
</CredenzaBody>
);
}
const { opened_at, propData } = data;
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="flex flex-col gap-2">
<h3 className="font-semibold">{t.contatto.propietario}:</h3>
<p>
{t.contatto.nome}: {data.propData.locatore}
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5 sm:max-w-xl">
<div className="space-y-10">
{/* Proprietario Section */}
<section>
<h3 className="mb-2 font-semibold">{t.contatto.propietario}:</h3>
<p className="mb-2">
{t.contatto.nome}: {propData.locatore}
</p>
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-5">
<a
className="flex items-center gap-2"
href={`tel:${data.propData.numero}`}
className="flex items-center gap-2 text-blue-600 hover:underline"
href={`tel:${propData.numero}`}
>
<Phone />:
<span className="text-blue-600 underline">
{data.propData.numero}
</span>
<Phone className="size-4" />
<span>{propData.numero}</span>
</a>
<Link
href={`https://wa.me/${data.propData.numero}`}
target="_blank"
>
<Button
className="flex items-center gap-2"
size="sm"
variant="success"
>
<Link href={`https://wa.me/${propData.numero}`} target="_blank">
<Button size="sm" variant="success">
<WhatsAppIcon2 className="size-4 fill-white" />
WhatsApp
</Button>
</Link>
</div>
</section>
<br />
<h3 className="font-semibold">{t.contatto.immobile}:</h3>
<p>
{t.contatto.indirizzo}: {data.propData.indirizzo},{" "}
{data.propData.civico} {data.propData.comune} (
{data.propData.provincia})
{/* Immobile Section */}
<section>
<h3 className="mb-2 font-semibold">{t.contatto.immobile}:</h3>
<p className="mb-2">
{t.contatto.indirizzo}: {propData.indirizzo}, {propData.civico}{" "}
{propData.comune} ({propData.provincia})
</p>
{navigationUrl && (
<a
className="flex w-fit items-center gap-2 rounded-md border border-border px-3 py-2"
className="inline-flex items-center gap-2 rounded-md border border-border px-3 py-2 hover:bg-accent"
href={navigationUrl}
rel="noreferrer"
target="_blank"
>
<Navigation /> <span>{t.contatto.apri_nav}</span>
<Navigation className="size-4" />
<span>{t.contatto.apri_nav}</span>
</a>
<br />
<p>
)}
</section>
{/* Warning */}
<div className="flex w-fit items-start gap-3 rounded-md bg-red-500 p-2 text-white">
<TriangleAlert className="size-6 shrink-0" />
<div className="space-y-1">
<p className="font-medium">{t.contatto.contatta}</p>
<p className="text-sm">{t.contatto.orari}</p>
</div>
</div>
{/* Metadata */}
<p className="text-muted-foreground text-sm">
{t.contatto.sbloccato_il}:{" "}
{data.opened_at.toLocaleString("it", {
{opened_at.toLocaleString("it", {
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
</p>
<div className="flex items-center gap-2 rounded-md bg-red-500 p-2 text-white">
<TriangleAlert className="size-8" />
<div>
<p>{t.contatto.contatta}</p>
<p className="font-semibold">{t.contatto.orari}</p>
</div>
</div>
</div>
</CredenzaBody>
<CredenzaFooter>

View file

@ -1,5 +1,5 @@
import { add } from "date-fns";
import { Bug, Calculator, ClockAlert, Plus, Wrench } from "lucide-react";
import { Bug, Calculator, ClockAlert, Plus, ShieldUser } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import {
@ -50,7 +50,7 @@ import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
export const ServizioActions2 = () => {
export const ServizioActions = () => {
const { servizio, isAdmin, userId } = useServizio();
const canUserEdit =
@ -65,7 +65,7 @@ export const ServizioActions2 = () => {
<div className="flex w-full flex-col flex-wrap justify-between gap-3 sm:flex-row">
{(isAdmin || canUserEdit) && (
<div className="flex items-center gap-3">
<span className="hidden sm:block">Azioni: </span>
{/* <span className="hidden sm:block">Azioni: </span> */}
<EditPreferenze />
<InterruzioneServizio />
</div>
@ -123,7 +123,7 @@ export const AddAnnuncio = ({
<DialogTrigger asChild>
<Button
aria-label="Add Annuncio"
className="items-center gap-2"
className=""
onClick={() => {
setSelectedAnnunci([]);
}}
@ -222,11 +222,10 @@ const InterruzioneServizio = () => {
<AlertDialogTrigger asChild>
<Button
aria-label="Request Interruzione"
className="items-center gap-2"
className=""
disabled={!beforeInterruzioneLimit}
variant="outline"
>
<ClockAlert /> <span>Interruzione</span>
<ClockAlert /> <span> Richiedi Interruzione Servizio</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
@ -288,12 +287,9 @@ const EditPreferenze = () => {
return (
<Credenza onOpenChange={setOpenEditPreferenze} open={openEditPreferenze}>
<CredenzaTrigger asChild>
<Button
aria-label="Preferenze di ricerca"
className="items-center gap-2"
>
<Button aria-label="Preferenze di ricerca" className="">
<Calculator />
Preferenze
Modifica Preferenze
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
@ -352,11 +348,8 @@ const EditServizioAdmin = () => {
return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button
aria-label="Modifica Servizio Admin"
className="items-center gap-2"
>
<Wrench />
<Button aria-label="Modifica Servizio Admin">
<ShieldUser />
Admin
</Button>
</CredenzaTrigger>
@ -375,7 +368,7 @@ const EditServizioAdmin = () => {
)}
<Collapsible>
<CollapsibleTrigger asChild>
<Button className="items-center gap-2" variant="warning">
<Button variant="warning">
<Bug /> Debug Info Servizio
</Button>
</CollapsibleTrigger>

View file

@ -56,7 +56,7 @@ const DialogServizio = ({
</Dialog>
);
};
export const AnnunciInConferma = ({
export const AnnunciInConfermaDialog = ({
annunci,
}: {
annunci: ServizioData["annunci"];
@ -74,11 +74,10 @@ export const AnnunciInConferma = ({
>
Annunci in conferma
</Button>
{annunci.length > 0 && (
<span className="-translate-y-1/2 absolute top-0 right-0 flex min-w-6 origin-center translate-x-1/2 items-center justify-center rounded-full bg-destructive px-1 text-white">
{annunci.length}
{annunci.length || 0}
</span>
)}
</div>
}
>
@ -95,7 +94,7 @@ export const AnnunciInConferma = ({
);
};
export const AnnunciSelezionati = ({
export const AnnunciSelezionatiDialog = ({
annunci,
}: {
annunci: ServizioData["annunci"];
@ -112,11 +111,10 @@ export const AnnunciSelezionati = ({
>
Annunci selezionati
</Button>
{annunci.length > 0 && (
<span className="-translate-y-1/2 absolute top-0 right-0 flex min-w-6 origin-center translate-x-1/2 items-center justify-center rounded-full bg-destructive px-1 text-white">
{annunci.length}
{annunci.length || 0}
</span>
)}
</div>
}
>
@ -149,6 +147,79 @@ export const AnnunciSelezionati = ({
);
};
export const AnnunciInConfermaAccordion = ({
annunci,
}: {
annunci: ServizioData["annunci"];
}) => {
if (annunci.length === 0) {
return null; // If there are no announcements in confirmation, do not render this section
}
return (
<Accordion collapsible defaultValue="in-conferma" type="single">
<AccordionItem className="space-y-4" value="in-conferma">
<AccordionTrigger className="rounded-md border-2 border-green-500 bg-white p-2">
Annunci in conferma:
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col flex-wrap gap-5">
{annunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider>
);
})}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
export const AnnunciSelezionatiAccordion = ({
annunci,
}: {
annunci: ServizioData["annunci"];
}) => {
return (
<Accordion collapsible defaultValue="open" type="single">
<AccordionItem className="space-y-4" value="open">
<AccordionTrigger className="cursor-pointer rounded-md border-2 border-sky-500 bg-white p-2">
Annunci Selezionati:
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col flex-wrap gap-5">
{annunci.length > 0 ? (
annunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard className="border-2 border-sky-500" />
</ServizioAnnuncioProvider>
);
})
) : (
<div className="flex items-center justify-center gap-1 rounded-md bg-white p-4 sm:flex">
<span>
Nessun annuncio selezionato, fai una ricerca nella pagina{" "}
<Link
aria-label="Annunci Compatibili"
className="inline-flex items-center gap-1 text-blue-500 underline"
href="/annunci"
>
Annunci
<ExternalLink className="size-4" />
</Link>
</span>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
};
export const AnnunciCompatibili = () => {
const { servizioId, userId, isAdmin } = useServizio();
const { data, isLoading } = api.servizio.getCompatibileAnnunci.useQuery({
@ -163,16 +234,9 @@ export const AnnunciCompatibili = () => {
<DialogServizio
title="Annunci compatibili"
trigger={
<div className="relative flex w-full sm:w-fit">
<Button className="w-full bg-purple-500 text-lg sm:w-fit" size="xl">
Annunci compatibili
<Button className="w-full bg-purple-500 sm:w-fit">
Esplora Annunci Compatibili
</Button>
{data && data.length > 0 && (
<span className="-translate-y-1/2 absolute top-0 right-0 flex min-w-6 origin-center translate-x-1/2 items-center justify-center rounded-full bg-destructive px-1 text-white">
{data?.length}
</span>
)}
</div>
}
>
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
@ -254,7 +318,6 @@ export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
data={a}
interactions={
<Button
className="flex items-center gap-2"
onClick={() => {
deleteAnnuncio({ annuncioId: a.id, userId });
}}

View file

@ -51,11 +51,7 @@ export const AnnunciTable = ({
href={`/area-riservata/admin/edit-annuncio/${row.original.id}`}
target="_blank"
>
<Button
className="flex items-center gap-2 px-2"
size="sm"
variant="default"
>
<Button className="px-2" size="sm" variant="default">
<Wrench />
</Button>
</Link>

View file

@ -166,7 +166,6 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => {
return (
<Button
aria-label="Apri Dettagli Ordine"
className="flex items-center gap-2"
onClick={() => {
setSelectedOrdine(row.original.id);
setModalOpen(true);

View file

@ -33,17 +33,7 @@ export type UploadCallback = (args: {
userStorageIds: UsersStorageUserStorageId[];
}) => void;
type UploadModalProps = {
cb_onUpload?: UploadCallback;
isAdmin?: boolean;
userId?: UsersId;
};
export const UploadModal = ({
cb_onUpload,
isAdmin,
userId,
}: UploadModalProps) => {
export const UploadModal = (props: UploadComponentProps) => {
const { t } = useTranslation();
return (
@ -63,11 +53,7 @@ export const UploadModal = ({
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<UploadComponent
cb_onUpload={cb_onUpload}
isAdmin={isAdmin}
userId={userId}
/>
<UploadComponent {...props} />
</CredenzaBody>
</CredenzaContent>
</Credenza>

View file

@ -246,12 +246,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithImages }) => {
<div className="flex flex-wrap items-center gap-2">
<Link href={`/annuncio/${data.codice}`} target="_blank">
<Button
className="flex items-center gap-2"
size="sm"
type="button"
variant="outline"
>
<Button size="sm" type="button" variant="outline">
<Eye /> pubblica
</Button>
</Link>
@ -259,28 +254,19 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithImages }) => {
href={`/area-riservata/admin/scheda-annuncio-stampa/${data.id}`}
target="_blank"
>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
variant="outline"
>
<Button size="sm" type="button" variant="outline">
<Printer /> Scheda
</Button>
</Link>
<Popover>
<PopoverTrigger asChild>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
>
<Button size="sm" type="button">
<CodeSquare /> Strumenti
</Button>
</PopoverTrigger>
<PopoverContent className="flex w-fit items-center gap-2">
<Button
className="flex items-center gap-2 bg-purple-500"
className="bg-purple-500"
onClick={async () => {
await revalidate({
cod: data.codice,
@ -298,12 +284,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithImages }) => {
}}
title="Aggiornamento Annuncio"
>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
variant="warning"
>
<Button size="sm" type="button" variant="warning">
<TriangleAlert /> Aggiorna
</Button>
</Confirm>

View file

@ -182,7 +182,6 @@ export const FormEditServizio = ({
<div className="flex flex-col items-start gap-2">
<Label htmlFor="reset_decorrenza">Reset Decorrenza</Label>
<Button
className="flex items-center gap-2"
id="reset_decorrenza"
onClick={() => form.setValue("decorrenza", null)}
type="button"

View file

@ -233,10 +233,7 @@ export const FormNewServizioAcquisto = ({
path: ["n_adulti"],
});
}
if (
permanenza === null &&
tipologia === TipologiaPosizioneEnum.Transitorio
) {
if (!permanenza && tipologia === TipologiaPosizioneEnum.Transitorio) {
refinementContext.issues.push({
code: "custom",
input: "",
@ -264,6 +261,14 @@ export const FormNewServizioAcquisto = ({
path: ["scadenza_motivazione_transitoria"],
});
}
if (!permanenza) {
refinementContext.issues.push({
code: "custom",
input: "",
message: "Inserisci permanenza",
path: ["permanenza"],
});
}
} else {
if (reddito === "" || reddito === null) {
refinementContext.issues.push({
@ -315,7 +320,7 @@ export const FormNewServizioAcquisto = ({
defaultValues: defaultValues,
});
const DatePickerLocale = locale === "it" ? it : enUS;
const { mutate } = api.servizio.postAcquistoData.useMutation({
const { mutate } = api.servizio.submitOnboardForPurchase.useMutation({
onError: (error) => {
toast.error(`Errore invio dati: ${error.message}`, {
id: "salvataggio",
@ -424,7 +429,7 @@ export const FormNewServizioAcquisto = ({
fattura: "Vuoi che la fatturazione sia intestata alla tua azienda?",
personali: "Dati personali",
personali_desc: "Inserisci i tuoi dati personali",
salva: "Salva e procedi al pagamento",
salva: "Salva e procedi",
sdi: "Codice destinatario SDI",
}
: {
@ -433,7 +438,7 @@ export const FormNewServizioAcquisto = ({
fattura: "Do you want the invoice to be issued to your company?",
personali: "Personal Data",
personali_desc: "Enter your personal data",
salva: "Save and proceed to payment",
salva: "Save and proceed",
sdi: "SDI Recipient Code",
};
@ -694,7 +699,7 @@ export const FormNewServizioAcquisto = ({
</div>
{tipologia === TipologiaPosizioneEnum.Transitorio ? (
<>
<div className="items-top flex w-full flex-col justify-between gap-4 sm:flex-row">
<FormField
control={form.control}
key={tipologia}
@ -704,10 +709,9 @@ export const FormNewServizioAcquisto = ({
t.preferenze.motivazioni_mappings.find(
(m) => m.id === field.value,
);
return (
<FormItem>
<div className="flex flex-wrap items-center gap-x-2">
<FormItem className="w-full">
<div className="flex w-full flex-wrap items-center gap-x-2">
<FormLabel htmlFor="motivazione_transitorio">
{t.preferenze.motivazione_label}
</FormLabel>
@ -736,7 +740,6 @@ export const FormNewServizioAcquisto = ({
)}
placeholder={t.seleziona_placeholder}
/>
<FormDescription>
{t.preferenze.motivazione_desc}
</FormDescription>
@ -794,7 +797,7 @@ export const FormNewServizioAcquisto = ({
</FormItem>
)}
/>
</>
</div>
) : (
<FormField
control={form.control}

View file

@ -60,7 +60,7 @@ const Annunci = ({ options }: AnnunciPageProps) => {
<meta content={t.heads.annunci_description} name="description" />
</Head>
<main className="mx-auto w-full max-w-7xl space-y-5 px-2 py-5 md:px-8">
<main className="mx-auto w-full max-w-7xl space-y-5 bg-background2 px-2 py-5 md:px-8">
<div className="inline-block rounded-md bg-primary/10 px-3 py-1 text-primary text-sm">
{t.annunci.titolo}
</div>

View file

@ -150,7 +150,7 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
href={`/area-riservata/admin/edit-annuncio/${data.id}`}
>
<Button
className="flex w-full items-center gap-2"
className="w-full"
type="button"
>
<Edit /> Modifica annuncio
@ -162,7 +162,7 @@ const AnnuncioView = ({ cod, flag }: Omit<AnnuncioProps, "meta">) => {
target="_blank"
>
<Button
className="flex w-full items-center gap-2"
className="w-full"
type="button"
variant="outline"
>
@ -669,7 +669,7 @@ const AnnuncioFooter = ({
}}
target="_blank"
>
<Button className="my-10 flex w-full items-center gap-2 bg-neutral-500 text-xl">
<Button className="my-10 w-full bg-neutral-500 text-xl">
Cerca annunci simili <ExternalLink className="size-6" />
</Button>
</Link>

View file

@ -53,17 +53,13 @@ const Admin_Annunci: NextPageWithLayout = () => {
</button>
<Popover>
<PopoverTrigger asChild>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
>
<Button size="sm" type="button">
<CodeSquare /> Strumenti avanzati
</Button>
</PopoverTrigger>
<PopoverContent className="flex flex-col items-center gap-2">
<Button
className="flex items-center gap-2 bg-purple-500"
className="bg-purple-500"
onClick={async () => {
await revalidate();
}}
@ -79,12 +75,7 @@ const Admin_Annunci: NextPageWithLayout = () => {
}}
title="Aggiornamento Annunci"
>
<Button
className="flex items-center gap-2"
size="sm"
type="button"
variant="warning"
>
<Button size="sm" type="button" variant="warning">
<TriangleAlert /> Aggiorna tutti gli annunci
</Button>
</Confirm>

View file

@ -61,7 +61,7 @@ const AdminBanners: NextPageWithLayout = () => {
</div>
<div className="mt-3 md:mt-0">
<Button
className="flex items-center gap-2 font-semibold"
className="font-semibold"
onClick={() => {
handleEdit(null);
handleOpen(true);

View file

@ -62,7 +62,7 @@ const AdminBlacklist: NextPageWithLayout = () => {
</div>
<div className="mt-3 md:mt-0">
<Button
className="flex items-center gap-2 font-semibold"
className="font-semibold"
onClick={() => {
handleEdit(null);
handleOpen(true);

View file

@ -62,7 +62,7 @@ const AdminEtichette: NextPageWithLayout = () => {
</div>
<div className="mt-3 md:mt-0">
<Button
className="flex items-center gap-2 font-semibold"
className="font-semibold"
onClick={() => {
handleEdit(null);
handleOpen(true);

View file

@ -60,7 +60,7 @@ const AdminFlags: NextPageWithLayout = () => {
</div>
<div className="mt-3 md:mt-0">
<Button
className="flex items-center gap-2 font-semibold"
className="font-semibold"
onClick={() => {
handleEdit(null);
handleOpen(true);

View file

@ -62,7 +62,7 @@ const AdminTestiStringhe: NextPageWithLayout = () => {
</div>
<div className="mt-3 md:mt-0">
<Button
className="flex items-center gap-2 font-semibold"
className="font-semibold"
onClick={() => {
handleEdit(null);
handleOpen(true);

View file

@ -125,10 +125,7 @@ const EditUser: NextPageWithLayout<EditUserProps> = ({
<div className="flex flex-wrap items-end gap-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="flex items-center gap-2"
variant={userData.isBlocked ? "success" : "orange"}
>
<Button variant={userData.isBlocked ? "success" : "orange"}>
{userData.isBlocked ? (
<>
<Unlock className="size-4" />
@ -161,10 +158,7 @@ const EditUser: NextPageWithLayout<EditUserProps> = ({
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
className="flex items-center gap-2"
variant="destructive"
>
<Button variant="destructive">
<Trash2 className="size-4" /> Elimina utente permanentemente
</Button>
</AlertDialogTrigger>

View file

@ -65,7 +65,7 @@ const PreOnboardServizio: NextPageWithLayout<PreOnboardServizioProps> = ({
</div>
<div className="flex justify-center">
<Button
className="flex items-center gap-2 text-xl"
className="text-xl"
onClick={async () => {
if (data.hasPrevious) {
await router.push({

View file

@ -65,7 +65,7 @@ const RiapriConferma: NextPageWithLayout<RiapriConfermaProps> = ({
</div>
<Button
className="mx-auto mb-4 flex items-center gap-2"
className="mx-auto mb-4"
onClick={() => router.back()}
variant="secondary"
>

View file

@ -250,7 +250,7 @@ export const servizioRouter = createTRPCRouter({
return await isOrdineAwaitingPayment(input.ordineId);
}),
postAcquistoData: protectedProcedure
submitOnboardForPurchase: protectedProcedure
.input(
z.object({
anagrafica: z.custom<UsersAnagraficaUpdate>(),

View file

@ -247,6 +247,7 @@ export const postAcquistoDataHandler = async ({
}) => {
try {
const servizioData = await db.transaction().execute(async (tx) => {
// Update user and anagrafica data
await tx
.updateTable("users")
.set(user)
@ -274,13 +275,7 @@ export const postAcquistoDataHandler = async ({
.returningAll()
.executeTakeFirstOrThrow();
}
await tx
.updateTable("users_anagrafica")
.set(anagrafica)
.where("userid", "=", userId)
.executeTakeFirstOrThrow();
// Update servizio data
return await tx
.updateTable("servizio")
.set(servizio)
@ -290,6 +285,7 @@ export const postAcquistoDataHandler = async ({
});
if (skipOrdine) {
// If skipping order, set servizio as active with decorrenza today
await updateServizio({
data: {
decorrenza: new Date(),
@ -627,6 +623,9 @@ export const getAllServizioAnnunci = async (userId: UsersId) => {
"servizio.isOkAcconto",
"servizio.isOkSaldo",
"servizio.isOkConsulenza",
"servizio.skipPayment",
"servizio.skipControlloDoc",
"servizio.skipDocMotivazione",
jsonObjectFrom(
eb
.selectFrom("users_storage")

View file

@ -42,6 +42,7 @@
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-background2: var(--background2);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
@ -171,6 +172,7 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--background2: oklch(95.041% 0.02791 62.217);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);