feat: add IncrociDialog component and integrate into admin and user interactions
This commit is contained in:
parent
c7dc508821
commit
eef0030676
8 changed files with 276 additions and 111 deletions
|
|
@ -1,8 +1,9 @@
|
|||
import { ArrowRight, BadgePlus } from "lucide-react";
|
||||
import { ArrowRight, BadgePlus, Edit, Printer } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
import { ContattoAnnuncio } from "~/components/annuncio-interactions/contatto_modal";
|
||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||
import { IncrociDialog } from "~/components/servizio/incroci";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
|
|
@ -14,9 +15,13 @@ import { api } from "~/utils/api";
|
|||
|
||||
export const AnnuncioInteractions = ({ disabled }: { disabled?: boolean }) => {
|
||||
const { user } = useSession();
|
||||
const { id, tipo } = useAnnuncio();
|
||||
const { locale } = useTranslation();
|
||||
if (!user) return null;
|
||||
|
||||
if (user.isAdmin) {
|
||||
return <AdminInteractions />;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return (
|
||||
<div className="w-full rounded-md bg-red-500 p-2 text-center font-semibold text-base text-white">
|
||||
|
|
@ -26,6 +31,39 @@ export const AnnuncioInteractions = ({ disabled }: { disabled?: boolean }) => {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
return <UserInteractions />;
|
||||
};
|
||||
const AdminInteractions = () => {
|
||||
const { id } = useAnnuncio();
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<Link
|
||||
className="w-full"
|
||||
href={`/area-riservata/admin/edit-annuncio/${id}`}
|
||||
>
|
||||
<Button className="w-full" type="button">
|
||||
<Edit /> Modifica annuncio
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
className="w-full"
|
||||
href={`/area-riservata/scheda-annuncio-stampa/${id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button className="w-full" type="button" variant="outline">
|
||||
<Printer /> Scheda immobile
|
||||
</Button>
|
||||
</Link>
|
||||
<IncrociDialog annuncioId={id} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UserInteractions = () => {
|
||||
const { user } = useSession();
|
||||
const { id, tipo } = useAnnuncio();
|
||||
const { locale } = useTranslation();
|
||||
if (!user) return null;
|
||||
return (
|
||||
<div className="flex w-full flex-col justify-around gap-3 md:gap-5">
|
||||
{user ? (
|
||||
|
|
@ -41,25 +79,11 @@ export const AnnuncioInteractions = ({ disabled }: { disabled?: boolean }) => {
|
|||
) : (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<ContattoAnnuncio />
|
||||
{/* <span className="w-full text-center text-sm">oppure</span>
|
||||
<Link
|
||||
aria-label="Login"
|
||||
href={{
|
||||
pathname: "/login",
|
||||
query: { redirect: router.asPath },
|
||||
}}
|
||||
>
|
||||
<Button className="w-full">
|
||||
<LogIn />
|
||||
<span>Accedi se hai un account</span>
|
||||
</Button>
|
||||
</Link> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ServizioInteraction = ({
|
||||
userId,
|
||||
tipologia,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import toast from "react-hot-toast";
|
||||
import { IncrociDialog } from "~/components/servizio/incroci";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
|
|
@ -73,6 +74,10 @@ export const AdminActions = () => {
|
|||
<h3 className="ml-1 font-semibold">Azioni Admin:</h3>
|
||||
<div className="flex w-full max-w-fit flex-wrap justify-start gap-3">
|
||||
<AnnuncioContatti data={data} />
|
||||
<IncrociDialog
|
||||
annuncioId={data.id}
|
||||
props={{ className: "w-fit", size: "sm" }}
|
||||
/>
|
||||
<AnnuncioConfermaPopover
|
||||
annuncioId={annuncioId}
|
||||
servizioId={servizioId}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import type { Table } from "@tanstack/react-table";
|
||||
import { format } from "date-fns";
|
||||
import { NotebookPen, Toolbox } from "lucide-react";
|
||||
import { Eye, NotebookPen, Toolbox } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { type RefObject, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||
|
|
@ -11,12 +12,17 @@ import {
|
|||
AnnuncioContatti,
|
||||
UserActions,
|
||||
} from "~/components/servizio/annuncio_actions";
|
||||
import { IncrociDialog } from "~/components/servizio/incroci";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { useMediaQuery } from "~/hooks/use-media-query";
|
||||
import { handleConsegna } from "~/lib/annuncio_details";
|
||||
import { getStorageUrl } from "~/lib/storage_utils";
|
||||
import { MutationPageRestore } from "~/lib/tableUtils";
|
||||
import {
|
||||
MutationPageRestore,
|
||||
type onMutate,
|
||||
type onSettled,
|
||||
} from "~/lib/tableUtils";
|
||||
import { cn, formatCurrency } from "~/lib/utils";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
|
||||
|
|
@ -125,21 +131,8 @@ export const AnnuncioCardRichieste = ({
|
|||
tableRef: RefObject<Table<RichiesteData> | null>;
|
||||
data: ServizioAnnuncioData;
|
||||
}) => {
|
||||
const [newNote, setNewNote] = useState<string | null>(data.note);
|
||||
const utils = api.useUtils();
|
||||
const { onMutate, onSettled } = MutationPageRestore(tableRef);
|
||||
const { mutate, isPending } = api.servizio.updateServizioAnnunci.useMutation({
|
||||
onMutate,
|
||||
onSettled,
|
||||
onSuccess: async () => {
|
||||
toast.success("Note aggiornate");
|
||||
await utils.servizio.invalidate();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Errore nell'aggiornamento note");
|
||||
},
|
||||
});
|
||||
const isDesktop = useMediaQuery("(min-width: 40rem)");
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-7xl gap-2 rounded-md border-none bg-muted py-3">
|
||||
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2">
|
||||
|
|
@ -164,6 +157,15 @@ export const AnnuncioCardRichieste = ({
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link href={`/annuncio/${data.codice}`} target="_blank">
|
||||
<Button size="sm" type="button" variant="outline">
|
||||
<Eye /> vista pubblica
|
||||
</Button>
|
||||
</Link>
|
||||
<IncrociDialog
|
||||
annuncioId={data.id}
|
||||
props={{ className: "w-fit" , size: "sm"}}
|
||||
/>
|
||||
<div>
|
||||
<AnnuncioContatti data={data} />
|
||||
</div>
|
||||
|
|
@ -176,48 +178,13 @@ export const AnnuncioCardRichieste = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Collapsible
|
||||
className={cn("space-y-2", isDesktop && "relative -top-10")}
|
||||
open={isDesktop || undefined}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
className={cn(isDesktop && "cursor-default")}
|
||||
size="sm"
|
||||
variant={isDesktop ? "flat" : "outline"}
|
||||
>
|
||||
<NotebookPen />
|
||||
<span>Annotazioni</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="">
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Textarea
|
||||
className="min-h-64"
|
||||
id={`note-${data.id}`}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
placeholder=""
|
||||
value={newNote || ""}
|
||||
<AnnuncioNote
|
||||
annuncioId={data.id}
|
||||
note={data.note}
|
||||
onMutate={onMutate}
|
||||
onSettled={onSettled}
|
||||
servizioId={data.servizio_id}
|
||||
/>
|
||||
<LoadingButton
|
||||
disabled={newNote === data.note}
|
||||
loading={isPending}
|
||||
onClick={() => {
|
||||
mutate({
|
||||
annuncioId: data.id,
|
||||
servizioId: data.servizio_id,
|
||||
data: { note: newNote },
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
variant="success"
|
||||
>
|
||||
Salva
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
@ -228,14 +195,20 @@ const AnnuncioNote = ({
|
|||
servizioId,
|
||||
annuncioId,
|
||||
note,
|
||||
onMutate,
|
||||
onSettled,
|
||||
}: {
|
||||
servizioId: ServizioServizioId;
|
||||
annuncioId: AnnunciId;
|
||||
note: string | null;
|
||||
onMutate?: onMutate;
|
||||
onSettled?: onSettled;
|
||||
}) => {
|
||||
const [newNote, setNewNote] = useState<string | null>(note);
|
||||
const utils = api.useUtils();
|
||||
const { mutate, isPending } = api.servizio.updateServizioAnnunci.useMutation({
|
||||
onMutate,
|
||||
onSettled,
|
||||
onSuccess: async () => {
|
||||
toast.success("Note aggiornate");
|
||||
await utils.servizio.invalidate();
|
||||
|
|
@ -245,12 +218,36 @@ const AnnuncioNote = ({
|
|||
},
|
||||
});
|
||||
const isDesktop = useMediaQuery("(min-width: 40rem)");
|
||||
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<Collapsible
|
||||
className={cn("space-y-2", isDesktop && "relative -top-10")}
|
||||
open={isDesktop || undefined}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Textarea
|
||||
className="min-h-64"
|
||||
id={`note-${annuncioId}`}
|
||||
onChange={(e) => setNewNote(e.target.value)}
|
||||
placeholder=""
|
||||
value={newNote || ""}
|
||||
/>
|
||||
<LoadingButton
|
||||
disabled={newNote === note}
|
||||
loading={isPending}
|
||||
onClick={() => {
|
||||
mutate({
|
||||
annuncioId,
|
||||
servizioId,
|
||||
data: { note: newNote },
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
variant="success"
|
||||
>
|
||||
Salva
|
||||
</LoadingButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Collapsible className={cn("space-y-2", isDesktop && "relative -top-10")}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
className={cn(isDesktop && "cursor-default")}
|
||||
|
|
|
|||
115
apps/infoalloggi/src/components/servizio/incroci.tsx
Normal file
115
apps/infoalloggi/src/components/servizio/incroci.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { format } from "date-fns";
|
||||
import { Waypoints } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button, type ButtonProps } from "~/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { UserAvatar } from "~/components/user_avatar";
|
||||
import { cn } from "~/lib/utils";
|
||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export const IncrociDialog = ({
|
||||
annuncioId,
|
||||
props,
|
||||
}: {
|
||||
annuncioId: AnnunciId;
|
||||
props?: ButtonProps;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className={cn("w-full", props?.className)}
|
||||
variant={props?.variant || "info"}
|
||||
{...props}
|
||||
>
|
||||
<Waypoints /> Incroci
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Lista Incroci</DialogTitle>
|
||||
<DialogDescription>
|
||||
Lista utenti che hanno ricevuto i contatti di questo annuncio
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<IncrociTable annuncioId={annuncioId} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const IncrociTable = ({ annuncioId }: { annuncioId: AnnunciId }) => {
|
||||
const { data: incroci, isLoading } = api.annunci.getIncrociAnnuncio.useQuery({
|
||||
annuncioId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="">Utente</TableHead>
|
||||
<TableHead>Data</TableHead>
|
||||
<TableHead>Note</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<p>Caricamento...</p>
|
||||
) : (
|
||||
incroci?.map((incrocio) => (
|
||||
<TableRow key={incrocio.servizio_id}>
|
||||
<TableCell>
|
||||
<Link
|
||||
aria-label="Visualizza Utente"
|
||||
className="flex items-center gap-2"
|
||||
href={`/area-riservata/admin/user-view/ricerca/${incrocio.user_id}`}
|
||||
>
|
||||
<UserAvatar
|
||||
className="size-8"
|
||||
userId={incrocio.user_id}
|
||||
username={incrocio.username}
|
||||
/>
|
||||
<span className="font-semibold text-sm">
|
||||
{incrocio.username}
|
||||
</span>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
<p>
|
||||
Aggiunto: {format(incrocio.created_at, "dd/MM/yyyy HH:mm")}
|
||||
</p>
|
||||
<p>
|
||||
Aperto:{" "}
|
||||
{incrocio.open_contatti_at
|
||||
? format(incrocio.open_contatti_at, "dd/MM/yyyy HH:mm")
|
||||
: "Non aperto"}
|
||||
</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="whitespace-pre-wrap">
|
||||
{incrocio.note}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,7 +2,20 @@
|
|||
import type { Table } from "@tanstack/react-table";
|
||||
import type { RefObject } from "react";
|
||||
|
||||
export const MutationPageRestore = (tableRef: RefObject<Table<any> | null>) => {
|
||||
export type onMutate = () => {
|
||||
pageIdx: number | undefined;
|
||||
};
|
||||
|
||||
export type onSettled = (
|
||||
_a: any,
|
||||
_b: any,
|
||||
_c: any,
|
||||
context?: { pageIdx?: number },
|
||||
) => void;
|
||||
|
||||
export const MutationPageRestore = (
|
||||
tableRef: RefObject<Table<any> | null>,
|
||||
): { onMutate: onMutate; onSettled: onSettled } => {
|
||||
return {
|
||||
onMutate: () => {
|
||||
const pageIdx = tableRef.current?.getState().pagination.pageIndex; // 0 based index
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import {
|
|||
Car,
|
||||
Clock,
|
||||
Copy,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
Printer,
|
||||
Ruler,
|
||||
Share2,
|
||||
TrafficCone,
|
||||
|
|
@ -64,7 +62,6 @@ import { cn } from "~/lib/utils";
|
|||
import { AnnuncioContext, useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||
import { useTranslation } from "~/providers/I18nProvider";
|
||||
import { buildRicercaUrl } from "~/providers/RicercaProvider";
|
||||
import { useSession } from "~/providers/SessionProvider";
|
||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||
import type { AnnuncioData } from "~/server/controllers/annunci.controller";
|
||||
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
||||
|
|
@ -121,7 +118,6 @@ const AnnuncioDettaglio: NextPage<AnnuncioProps> = ({
|
|||
};
|
||||
|
||||
const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
|
||||
const { user } = useSession();
|
||||
useStaleImageReload();
|
||||
|
||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
|
|
@ -146,33 +142,7 @@ const AnnuncioView = ({ data, flag }: AnnuncioProps) => {
|
|||
<div className="mb-6 flex flex-col md:mb-0 md:w-1/3">
|
||||
<div className="flex w-full flex-col flex-nowrap items-center justify-center gap-6">
|
||||
<CardInfos />
|
||||
{user?.isAdmin ? (
|
||||
<>
|
||||
<Link
|
||||
className="w-full"
|
||||
href={`/area-riservata/admin/edit-annuncio/${data.id}`}
|
||||
>
|
||||
<Button className="w-full" type="button">
|
||||
<Edit /> Modifica annuncio
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
className="w-full"
|
||||
href={`/area-riservata/scheda-annuncio-stampa/${data.id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
className="w-full"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Printer /> Scheda immobile
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<AnnuncioInteractions disabled={flag === "true"} />
|
||||
)}
|
||||
<ComeFunziona />
|
||||
{data.tipo &&
|
||||
(data.tipo === "Transitorio" || data.tipo === "Stabile") && (
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
removeAnnuncioMedia,
|
||||
type VideoToRemove,
|
||||
} from "~/server/controllers/annunci.controller";
|
||||
import { getIncrociAnnuncio } from "~/server/controllers/servizio.controller";
|
||||
import { db } from "~/server/db";
|
||||
import {
|
||||
invalidateCache,
|
||||
|
|
@ -188,4 +189,13 @@ export const annunciRouter = createTRPCRouter({
|
|||
...input,
|
||||
});
|
||||
}),
|
||||
getIncrociAnnuncio: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
annuncioId: zAnnuncioId,
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return await getIncrociAnnuncio(input.annuncioId);
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1282,3 +1282,34 @@ export const getRichieste = async (): Promise<RichiesteData[]> => {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getIncrociAnnuncio = async (annuncioId: AnnunciId) => {
|
||||
try {
|
||||
const data = await db
|
||||
.selectFrom("servizio_annunci")
|
||||
.innerJoin(
|
||||
"servizio",
|
||||
"servizio.servizio_id",
|
||||
"servizio_annunci.servizio_id",
|
||||
)
|
||||
.innerJoin("users", "users.id", "servizio.user_id")
|
||||
.where("servizio_annunci.annunci_id", "=", annuncioId)
|
||||
.select([
|
||||
"servizio_annunci.open_contatti_at",
|
||||
"servizio_annunci.created_at",
|
||||
"servizio.servizio_id",
|
||||
"servizio.user_id",
|
||||
"users.username",
|
||||
"servizio_annunci.note",
|
||||
])
|
||||
.orderBy("servizio_annunci.created_at", "desc")
|
||||
.execute();
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: `Error fetching incroci annuncio: ${(e as Error).message}`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue