feat: enhance RichiesteTable with improved status and decorrenza handling

- Updated RichiesteTable to display decorrenza with tooltip based on status.
- Enhanced status display with badges indicating various states (e.g., Attesa attivazione, Interrotto).
- Added Popover for displaying Annunci details in RichiesteTable.
- Refactored API to fetch user interests and related announcements.
- Introduced Queue component to display email events for users.
- Updated translations to include new "Requests" section in admin navigation.
- Improved database queries for fetching user interests and announcements.
This commit is contained in:
Marco Pedone 2026-03-25 17:30:33 +01:00
parent 8d07c82e7e
commit 7adb33f485
12 changed files with 659 additions and 380 deletions

View file

@ -15,7 +15,10 @@ import { getStorageUrl } from "~/lib/storage_utils";
import { cn, formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { AnnuncioRicerca } from "~/server/controllers/annunci.controller";
import type { ServizioAnnuncioData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api";
import { Button } from "../ui/button";
import {
@ -30,15 +33,9 @@ export const AnnuncioCard = ({ className }: { className?: string }) => {
const { data } = useServizioAnnuncio();
return (
<BasicAnnuncioCard
<AnnuncioCardContent
className={className}
data={{
...data,
modificato_il: data.modificato_il ? new Date(data.modificato_il) : null,
media_updated_at: data.media_updated_at
? new Date(data.media_updated_at)
: null,
}}
data={data}
interactions={
isAdmin ? (
<div className="flex flex-col gap-2">
@ -60,20 +57,22 @@ export const AnnuncioCard = ({ className }: { className?: string }) => {
<UserActions />
)
}
isAdmin={isAdmin}
/>
);
};
export const BasicAnnuncioCard = ({
export const AnnuncioCardContent = ({
interactions,
className,
data,
isAdmin,
}: {
interactions?: React.ReactNode;
className?: string;
data: AnnuncioRicerca;
data: ServizioAnnuncioData;
isAdmin: boolean;
}) => {
const { isAdmin } = useServizio();
return (
<Card
className={cn(
@ -99,18 +98,29 @@ export const BasicAnnuncioCard = ({
<div className="flex w-full flex-col gap-4">{interactions}</div>
</div>
{isAdmin && <AnnuncioNote />}
{isAdmin && (
<AnnuncioNote
annuncioId={data.id}
note={data.note}
servizioId={data.servizio_id}
/>
)}
</div>
</CardContent>
</Card>
);
};
const AnnuncioNote = () => {
const { servizioId } = useServizio();
const { data } = useServizioAnnuncio();
const [newNote, setNewNote] = useState(data.note || "");
const AnnuncioNote = ({
servizioId,
annuncioId,
note,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
note: string | null;
}) => {
const [newNote, setNewNote] = useState<string | null>(note);
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.updateServizioAnnunci.useMutation({
onSuccess: async () => {
@ -142,21 +152,22 @@ const AnnuncioNote = () => {
<div className="flex w-full flex-col gap-2">
<Textarea
className="min-h-64"
id={`note-${data.id}`}
id={`note-${annuncioId}`}
onChange={(e) => setNewNote(e.target.value)}
placeholder=""
value={newNote}
value={newNote || ""}
/>
<LoadingButton
disabled={newNote === data.note}
disabled={newNote === note}
loading={isPending}
onClick={() =>
onClick={() => {
mutate({
annuncioId: data.id,
annuncioId,
servizioId,
data: { note: newNote },
})
}
});
}}
type="button"
variant="success"
>
Salva

View file

@ -12,6 +12,7 @@ import {
type LucideIcon,
MousePointerClick,
PackageCheck,
SearchX,
Trash2,
} from "lucide-react";
import Link from "next/link";
@ -160,7 +161,7 @@ const ServizioLinkCard = ({
if (servizio.isInterrotto) {
status = "interrotto";
StatusIcon = Trash2;
StatusIcon = SearchX;
} else if (!servizio.isOkAcconto) {
status = "in_attesa";
StatusIcon = ClockFading;
@ -332,7 +333,7 @@ const Content = () => {
};
const Main = () => {
const { servizio, isAdmin } = useServizio();
const { servizio, isAdmin, userId } = useServizio();
const { t } = useTranslation();
const utils = api.useUtils();
const { mutate: attivazioneSenzaPagamento } =
@ -349,10 +350,28 @@ const Main = () => {
// se interrotto, mostra avvisos
if (servizio.isInterrotto) {
return (
<>
<p className="font-medium text-lg">{t.servizio.servizio_interrotto}</p>
<p>{t.servizio.servizio_interrotto_desc}</p>
</>
<div className="flex w-full flex-col items-center justify-center gap-4 py-10 text-center">
<div className="flex size-14 items-center justify-center rounded-full bg-red-100 dark:bg-red-500/10">
<SearchX className="size-7 text-red-500 dark:text-red-400" />
</div>
<div className="flex flex-col gap-1">
<p className="font-semibold text-xl">
{t.servizio.servizio_interrotto}
</p>
<p className="max-w-sm text-muted-foreground text-sm">
{t.servizio.servizio_interrotto_desc}
</p>
</div>
<Link
href={
isAdmin
? `/area-riservata/admin/user-view/ricerca/${userId}`
: "/area-riservata/dashboard"
}
>
<Button variant="secondary">Torna alla lista dei servizi</Button>
</Link>
</div>
);
}
// mostra onboarding

View file

@ -1,6 +1,6 @@
import { Plus, Trash2 } from "lucide-react";
import toast from "react-hot-toast";
import { BasicAnnuncioCard } from "~/components/servizio/annuncio_card";
import { AnnuncioDisplay } from "~/components/servizio/annuncio_card";
import {
Accordion,
AccordionContent,
@ -8,6 +8,8 @@ import {
AccordionTrigger,
} from "~/components/ui/accordion";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { cn } from "~/lib/utils";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
@ -64,11 +66,24 @@ export const AnnunciRichiesti = ({
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
{data.map((a) => {
return (
<BasicAnnuncioCard
className="w-full border-2 border-pink-500"
data={a}
interactions={
<>
<Card
className="w-full max-w-7xl gap-2 rounded-md border-2 border-pink-500 bg-muted py-3"
key={a.id}
>
<CardHeader className="flex flex-row flex-wrap items-center justify-between space-y-0 px-3 pb-2">
<CardTitle className="text-2xl">
Annuncio {a.codice}
</CardTitle>
</CardHeader>
<CardContent className="px-3">
<div className="grid grid-cols-1 gap-4">
<div
className={cn("@container flex w-full flex-col gap-4")}
id="container"
>
<AnnuncioDisplay data={a} />
<div className="flex w-full flex-col gap-4">
{servizioId && (
<Button
onClick={() => {
@ -89,10 +104,11 @@ export const AnnunciRichiesti = ({
Rimuovi
<Trash2 className="size-4" />
</Button>
</>
}
key={a.id}
/>
</div>
</div>
</div>
</CardContent>
</Card>
);
})}
{data.length % 2 === 1 && (

View file

@ -1,6 +1,16 @@
import type { ColumnDef } from "@tanstack/react-table";
import { ExternalLink } from "lucide-react";
import { format } from "date-fns";
import {
CircleCheck,
CircleCheckBig,
CircleEllipsis,
ClockFading,
ExternalLink,
SearchX,
} from "lucide-react";
import Link from "next/link";
import { AnnuncioCardContent } from "~/components/servizio/annuncio_card";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { DataTable } from "~/components/ui/data-table";
import { DataTableColumnHeader } from "~/components/ui/dataTable-header";
@ -8,6 +18,21 @@ import type {
PinnedFiltro,
SearchFiltro,
} from "~/components/ui/dataTable-toolbar";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "~/components/ui/popover";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "~/components/ui/tooltip";
import { UserAvatar } from "~/components/user_avatar";
import { cn } from "~/lib/utils";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { RichiesteData } from "~/server/controllers/servizio.controller";
@ -17,13 +42,12 @@ type RichiesteTableProps = {
export const RichiesteTable = ({ data }: RichiesteTableProps) => {
const tabledata = data;
const columns_titles = {
id: "ID Ordine",
username: "Utente",
created_at: "Creato il",
decorrenza: "Decorrenza",
status: "Status",
tipo: "Tipo",
tipologia: "Tipo",
annunci: "Annunci",
actions: "Azioni",
};
@ -39,10 +63,6 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
{ label: "Stabile", value: TipologiaPosizioneEnum.Stabile },
];
// const [selectedOrdine, setSelectedOrdine] = useState<OrdiniOrdineId | null>(
// null,
// );
type Column = (typeof tabledata)[number];
const columns: ColumnDef<Column>[] = [
@ -58,10 +78,17 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
cell: ({ row }) => {
return (
<Link
aria-label="Visualizza Utente"
className="flex items-center gap-2"
href={`/area-riservata/admin/user-view/ricerca/${row.original.user_id}`}
>
<Button size="sm">{row.original.username}</Button>
<UserAvatar
className="size-8"
userId={row.original.user_id}
username={row.original.username}
/>
<span className="font-semibold text-sm">
{row.original.username}
</span>
</Link>
);
},
@ -74,16 +101,39 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
),
},
{
accessorKey: "created_at",
accessorKey: "decorrenza",
cell: ({ row }) => {
const date = row.getValue("created_at");
return new Date(date as Date).toLocaleDateString();
let decorrenzaColor = "";
let decorrenzaTooltip = "";
if (!row.original.isOkAcconto || !row.original.decorrenza) {
decorrenzaColor = "text-muted-foreground";
decorrenzaTooltip = "Attesa di attivazione";
} else if (row.original.isInterrotto) {
decorrenzaColor = "text-destructive";
decorrenzaTooltip = "Servizio interrotto";
} else if (row.original.isOkAcconto && !row.original.isOkSaldo) {
decorrenzaColor = "text-warning";
decorrenzaTooltip = "Attesa di saldo";
}
return (
<Tooltip>
<TooltipTrigger className={decorrenzaColor}>
{row.original.decorrenza
? format(row.original.decorrenza, "dd/MM")
: "---"}
</TooltipTrigger>
<TooltipContent>
<p>{decorrenzaTooltip}</p>
</TooltipContent>
</Tooltip>
);
},
enableHiding: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.created_at}
title={columns_titles.decorrenza}
/>
),
sortingFn: "datetime",
@ -91,37 +141,94 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
{
accessorKey: "tipologia",
cell: ({ row }) => {
return row.original.tipologia.toString();
const { tipologia } = row.original;
return (
<Badge
className={cn(
"px-1.5",
tipologia === TipologiaPosizioneEnum.Transitorio
? "border-transitorio bg-transitorio/15"
: "border-stabile bg-stabile/15",
)}
variant="outline"
>
{tipologia.toString()}
</Badge>
);
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.tipo} />
<DataTableColumnHeader
column={column}
title={columns_titles.tipologia}
/>
),
},
{
accessorKey: "status",
cell: ({ row }) => {
let badgeClass = "";
let contents: React.ReactNode;
const annunci = row.original.annunci_servizio;
const awaitingConferma = annunci.some(
(a) => a.user_confirmed_at !== null && !a.hasConfermaAdmin,
);
if (awaitingConferma) {
return "Conferma in corso";
}
if (row.original.isOkSaldo) {
return "Saldo pagato";
}
if (row.original.isOkAcconto) {
return "Acconto pagato";
}
if (!row.original.onboardOk) {
return "Attesa di attivazione";
const hasConfermaAdmin = annunci.some((a) => a.hasConfermaAdmin);
if (row.original.isInterrotto) {
badgeClass = "border-destructive bg-destructive/10 text-destructive";
contents = (
<>
<SearchX className="stroke-red-500 dark:stroke-red-400" />
Interrotto
</>
);
} else if (!row.original.onboardOk) {
badgeClass = "border-yellow-500 bg-yellow-500/10 text-yellow-600";
contents = (
<>
<ClockFading className="stroke-yellow-500 dark:stroke-yellow-400" />
Attesa attivazione
</>
);
} else if (awaitingConferma) {
badgeClass = "border-sky-500 bg-sky-500/10 text-sky-500";
contents = (
<>
<CircleEllipsis className="stroke-sky-500 dark:stroke-sky-400" />
In Conferma
</>
);
} else if (row.original.isOkSaldo && hasConfermaAdmin) {
badgeClass = "border-blue-500 bg-blue-500/10 text-blue-500";
contents = (
<>
<CircleCheckBig className="stroke-blue-500 dark:stroke-blue-400" />
Saldo pagato
</>
);
} else if (row.original.isOkAcconto) {
badgeClass = "border-green-500 bg-green-500/10 text-green-500";
contents = (
<>
<CircleCheck className="stroke-green-500 dark:stroke-green-400" />
Attivo
</>
);
}
return (
<Badge
className={cn(
"flex items-center px-1.5 text-muted-foreground [&>svg]:size-4",
badgeClass,
)}
variant="outline"
>
{contents}
</Badge>
);
},
filterFn: (row, id, value) => {
@ -135,7 +242,43 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
accessorKey: "annunci_servizio",
cell: ({ row }) => {
const annunci = row.original.annunci_servizio;
return <span>{annunci.length}</span>;
return (
<Popover>
<PopoverTrigger asChild>
<Button size="sm" variant="outline">
{annunci.length} Annunci
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="w-4xl">
<PopoverHeader>
<PopoverTitle>Annunci Inseriti</PopoverTitle>
<PopoverDescription className="sr-only">
Desc
</PopoverDescription>
</PopoverHeader>
<div className="flex flex-col gap-4 divide-y">
{annunci.map((a) => (
<AnnuncioCardContent
data={a}
interactions={
<div>
<p>
Visto contatti:{" "}
{a.open_contatti_at
? format(a.open_contatti_at, "dd/MM/yyyy HH:mm")
: "Non ancora visto"}
</p>
{/*TODO CAPIRE COSA AGGIUNGERE */}
</div>
}
isAdmin
key={a.annunci_id}
/>
))}
</div>
</PopoverContent>
</Popover>
);
},
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.annunci} />
@ -143,16 +286,16 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
enableSorting: false,
},
{
cell: () => {
cell: ({ row }) => {
return (
<Button
aria-label="Apri Dettagli Ordine"
//onClick={() => {}}
size="sm"
variant="outline"
<Link
href={`/area-riservata/admin/user-view/servizio/${row.original.user_id}/${row.original.servizio_id}`}
target="_blank"
>
<Button aria-label="Apri servizio" size="sm" variant="outline">
<ExternalLink className="size-5" />
</Button>
</Link>
);
},
enableHiding: false,
@ -173,7 +316,7 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
title: "Stato Ordine",
},
{
columnName: "tipo",
columnName: "tipologia",
options: type_options,
title: "Tipo Ordine",
},
@ -185,16 +328,10 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => {
columns={columns}
columns_titles={columns_titles}
data={tabledata}
defaultSort={[{ desc: true, id: "created_at" }]}
defaultSort={[{ desc: true, id: "decorrenza" }]}
pinnedFiltri={pinnedFiltri}
searchColumn={searchFiltro}
/>
{/* <AdminWrapperOrdiniModal
ordineId={selectedOrdine}
setSelected={setSelectedOrdine}
/>
*/}
</div>
);
};

View file

@ -2,7 +2,6 @@
import { Popover as PopoverPrimitive } from "radix-ui";
import type * as React from "react";
import { cn } from "~/lib/utils";
function Popover({
@ -28,7 +27,7 @@ function PopoverContent({
<PopoverPrimitive.Content
align={align}
className={cn(
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 flex w-72 origin-(--radix-popover-content-transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-popover-foreground text-sm shadow-md outline-hidden ring-1 ring-foreground/10 duration-100 data-closed:animate-out data-open:animate-in",
className,
)}
data-slot="popover-content"
@ -45,4 +44,45 @@ function PopoverAnchor({
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
className={cn("flex flex-col gap-0.5 text-sm", className)}
data-slot="popover-header"
{...props}
/>
);
}
function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
return (
<div
className={cn("cn-font-heading font-medium", className)}
data-slot="popover-title"
{...props}
/>
);
}
function PopoverDescription({
className,
...props
}: React.ComponentProps<"p">) {
return (
<p
className={cn("text-muted-foreground", className)}
data-slot="popover-description"
{...props}
/>
);
}
export {
Popover,
PopoverAnchor,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
};

View file

@ -657,6 +657,7 @@ export const en: LangDict = {
items: [
{ href: "/area-riservata/admin/potenziali", title: "Potenziali" },
{ href: "/area-riservata/admin/utenti", title: "Users" },
{ href: "/area-riservata/admin/richieste", title: "Requests" },
{ href: "/area-riservata/admin/chats", title: "Chat List" },
{ href: "/area-riservata/admin/ordini", title: "Orders" },
{ href: "/area-riservata/admin/incroci", title: "Incroci" },

View file

@ -662,6 +662,7 @@ export const it: LangDict = {
items: [
{ href: "/area-riservata/admin/potenziali", title: "Potenziali" },
{ href: "/area-riservata/admin/utenti", title: "Utenti" },
{ href: "/area-riservata/admin/richieste", title: "Richieste" },
{ href: "/area-riservata/admin/chats", title: "Lista Chat" },
{ href: "/area-riservata/admin/ordini", title: "Ordini" },
{ href: "/area-riservata/admin/incroci", title: "Incroci" },

View file

@ -74,7 +74,7 @@ const ComunicazioniUser: NextPageWithLayout<ComunicazioniUserProps> = ({
</PopoverContent>
</Popover>
</div>
<Queue userId={userId} />
<EmailAccordion isAdmin={user.isAdmin} userId={userId} />
</main>
);
@ -119,3 +119,13 @@ export const getServerSideProps = (async (context) => {
ComunicazioniUser.getLayout = function getLayout(page) {
return <AreaRiservataLayoutUserView>{page}</AreaRiservataLayoutUserView>;
};
const Queue = ({ userId }: { userId: UsersId }) => {
const { data } = api.comunicazioni.getUserQueuedEmails.useQuery({ userId });
console.log("Queue data", data);
return (
<div>
<h2>Eventi email in coda per user {userId}</h2>
</div>
);
};

View file

@ -6,13 +6,13 @@ import { db } from "~/server/db";
import {
AddIntrest,
GetUserInterests,
GetUserInterestsAnnunci,
HasUserInterest,
RemoveInterest,
} from "~/server/services/interests.service";
import { NewMail } from "~/server/services/mailer";
import { findUser_byId_MINI } from "~/server/services/user.service";
import { zAnnuncioId, zUserId } from "~/server/utils/zod_types";
import { withImages, withVideos } from "~/utils/kysely-helper";
export const intrestsRouter = createTRPCRouter({
addIntrest: protectedProcedure
@ -85,49 +85,7 @@ export const intrestsRouter = createTRPCRouter({
}),
)
.query(async ({ input, ctx }) => {
try {
const interests = await GetUserInterests(
input.userId || ctx.session.id,
);
if (!interests || interests.length === 0) {
return [];
}
const annunci = await db
.selectFrom("annunci")
.select((_eb) => [
"annunci.id",
"annunci.codice",
"annunci.comune",
"annunci.provincia",
"annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
"annunci.desc_en",
"annunci.desc_it",
"annunci.modificato_il",
"annunci.stato",
"annunci.external_videos",
"annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
withImages(),
withVideos(),
])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")
.where("annunci.id", "in", interests)
.execute();
return annunci;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Errore durante il recupero degli annunci: ${(e as Error).message}`,
});
}
return await GetUserInterestsAnnunci(input.userId || ctx.session.id);
}),
hasUserInterest: protectedProcedure
.input(

View file

@ -1,11 +1,13 @@
import type { Override } from "TypeHelpers.type";
import { TRPCError } from "@trpc/server";
import { add, set } from "date-fns";
import { sql } from "kysely";
import type { Attachment } from "nodemailer/lib/mailer";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { NewEventQueue } from "~/schemas/public/EventQueue";
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import type { UsersId } from "~/schemas/public/Users";
import {
type MinimalSMSData,
SendMessage,
@ -155,3 +157,20 @@ export const processEvents = async () => {
}
return { message: `${events.length} events processed` };
};
export const userEmailQueue = async (userId: UsersId) => {
try {
const events = await db
.selectFrom("event_queue")
.selectAll()
.where(sql`data->>'tipo'`, "=", "email")
.where(sql`data->'data'->>'userId'`, "=", userId)
.execute();
return events;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while getting user events from queue, ${(e as Error).message}`,
});
}
};

View file

@ -1,4 +1,3 @@
import type { Override } from "TypeHelpers.type";
import { TRPCError } from "@trpc/server";
import { add, isBefore } from "date-fns";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
@ -7,6 +6,7 @@ import type { PurchaseData } from "~/components/acquisto_receipt";
import { env } from "~/env";
import type { PaymentType } from "~/i18n/stripe";
import { parseDBComune, parseDBNazione } from "~/lib/catasto";
import { withParsedDates } from "~/lib/dateParserJsonKysely";
import type { Annunci, AnnunciId } from "~/schemas/public/Annunci";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini";
@ -344,11 +344,8 @@ type DocRef = {
storageId: string;
ref: UsersStorageUserStorageId | null;
};
export type ServizioData = Servizio & {
doc_personale_fronte: DocRef | null;
doc_personale_retro: DocRef | null;
doc_motivazione: DocRef | null;
annunci: (ServizioAnnunci &
export type ServizioAnnuncioData = ServizioAnnunci &
Pick<
Annunci,
| "id"
@ -382,7 +379,12 @@ export type ServizioData = Servizio & {
thumb: string;
video: string;
}[];
})[];
};
export type ServizioData = Servizio & {
doc_personale_fronte: DocRef | null;
doc_personale_retro: DocRef | null;
doc_motivazione: DocRef | null;
annunci: ServizioAnnuncioData[];
ordini: (Ordini & Pick<Prezziario, "testo_condizioni">)[];
};
@ -390,7 +392,8 @@ export const getAllServizioAnnunci = async (
userId: UsersId,
): Promise<ServizioData[]> => {
try {
const data = await db
const data = await withParsedDates(
db
.selectFrom("servizio")
.where("user_id", "=", userId)
.leftJoin(
@ -490,47 +493,33 @@ export const getAllServizioAnnunci = async (
eb
.selectFrom("ordini")
.selectAll("ordini")
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.innerJoin(
"prezziario",
"prezziario.idprezziario",
"ordini.packid",
)
.select("prezziario.testo_condizioni")
.whereRef("ordini.servizio_id", "=", "servizio.servizio_id")
.orderBy("ordini.created_at", "desc"),
).as("ordini"),
])
.orderBy("servizio.created_at", "desc")
.execute();
.orderBy("servizio.created_at", "desc"),
[
"annunci.modificato_il",
"annunci.created_at",
"annunci.open_contatti_at",
"annunci.user_confirmed_at",
"annunci.accettato_conferma_at",
"annunci.contratto_decorrenza",
"annunci.contratto_scadenza",
"annunci.disponibile_da",
"annunci.media_updated_at",
"ordini.created_at",
"ordini.paid_at",
],
).execute();
const formattedData = data.map((item) => {
return {
...item,
annunci: item.annunci.map((annuncio) => ({
...annuncio,
created_at: new Date(annuncio.created_at),
open_contatti_at: parseNullableDateString(annuncio.open_contatti_at),
user_confirmed_at: parseNullableDateString(
annuncio.user_confirmed_at,
),
accettato_conferma_at: parseNullableDateString(
annuncio.accettato_conferma_at,
),
contratto_decorrenza: parseNullableDateString(
annuncio.contratto_decorrenza,
),
contratto_scadenza: parseNullableDateString(
annuncio.contratto_scadenza,
),
modificato_il: parseNullableDateString(annuncio.modificato_il),
disponibile_da: parseNullableDateString(annuncio.disponibile_da),
media_updated_at: parseNullableDateString(annuncio.media_updated_at),
})),
ordini: item.ordini.map((ordine) => ({
...ordine,
created_at: new Date(ordine.created_at),
paid_at: ordine.paid_at ? new Date(ordine.paid_at) : null,
})),
};
});
return formattedData;
return data;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@ -1460,31 +1449,14 @@ export const getOrdineRicevutaData = async ({
};
export type RichiesteData = Servizio &
Pick<Users, "username"> & {
annunci_servizio: (Override<
ServizioAnnunci,
{
created_at: string;
open_contatti_at: string | null;
user_confirmed_at: string | null;
accettato_conferma_at: string | null;
contratto_decorrenza: string | null;
contratto_scadenza: string | null;
}
> &
Pick<Annunci, "codice" | "titolo_it">)[];
ordini_servizio: (Override<
Ordini,
{
created_at: string;
paid_at: string | null;
}
> &
Pick<Prezziario, "testo_condizioni">)[];
annunci_servizio: ServizioAnnuncioData[];
ordini_servizio: (Ordini & Pick<Prezziario, "testo_condizioni">)[];
};
export const getRichieste = async (): Promise<RichiesteData[]> => {
try {
return await db
const data = await withParsedDates(
db
.selectFrom("servizio")
.innerJoin("users", "users.id", "servizio.user_id")
.selectAll("servizio")
@ -1495,7 +1467,40 @@ export const getRichieste = async (): Promise<RichiesteData[]> => {
.selectFrom("servizio_annunci")
.selectAll("servizio_annunci")
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
.select(["annunci.codice", "annunci.titolo_it"])
.select((_eb) => [
"annunci.id",
"annunci.codice",
"annunci.comune",
"annunci.provincia",
"annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
"annunci.desc_en",
"annunci.desc_it",
"annunci.modificato_il",
"annunci.stato",
"annunci.external_videos",
"annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
"annunci.disponibile_da",
"annunci.persone",
"annunci.permanenza",
withImages(),
withVideos(),
])
.orderBy("servizio_annunci.user_confirmed_at", (ob) =>
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.open_contatti_at", (ob) =>
ob.asc().nullsLast(),
)
.orderBy("servizio_annunci.created_at", "asc")
.whereRef(
"servizio.servizio_id",
"=",
@ -1507,14 +1512,32 @@ export const getRichieste = async (): Promise<RichiesteData[]> => {
eb
.selectFrom("ordini")
.selectAll("ordini")
.innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid")
.innerJoin(
"prezziario",
"prezziario.idprezziario",
"ordini.packid",
)
.select("prezziario.testo_condizioni")
.whereRef("ordini.servizio_id", "=", "servizio.servizio_id")
.orderBy("ordini.created_at", "desc"),
).as("ordini_servizio"),
])
.orderBy("servizio.created_at", "desc")
.execute();
.orderBy("servizio.created_at", "desc"),
[
"annunci_servizio.created_at",
"annunci_servizio.open_contatti_at",
"annunci_servizio.disponibile_da",
"annunci_servizio.user_confirmed_at",
"annunci_servizio.accettato_conferma_at",
"annunci_servizio.contratto_decorrenza",
"annunci_servizio.contratto_scadenza",
"annunci_servizio.modificato_il",
"annunci_servizio.media_updated_at",
"ordini_servizio.created_at",
"ordini_servizio.paid_at",
],
).execute();
return data;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",

View file

@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { UsersId } from "~/schemas/public/Users";
import { db } from "~/server/db";
import { withImages, withVideos } from "~/utils/kysely-helper";
export const AddIntrest = async ({
annuncioId,
@ -91,3 +92,46 @@ export const GetUserInterests = async (userId: UsersId) => {
});
}
};
export const GetUserInterestsAnnunci = async (userId: UsersId) => {
try {
const interests = await GetUserInterests(userId);
if (!interests || interests.length === 0) {
return [];
}
const annunci = await db
.selectFrom("annunci")
.select((_eb) => [
"annunci.id",
"annunci.codice",
"annunci.comune",
"annunci.provincia",
"annunci.prezzo",
"annunci.consegna",
"annunci.numero_camere",
"annunci.mq",
"annunci.tipo",
"annunci.titolo_it",
"annunci.titolo_en",
"annunci.desc_en",
"annunci.desc_it",
"annunci.modificato_il",
"annunci.stato",
"annunci.external_videos",
"annunci.media_updated_at",
"annunci.homepage",
"annunci.web",
withImages(),
withVideos(),
])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")
.where("annunci.id", "in", interests)
.execute();
return annunci;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Errore durante il recupero degli annunci: ${(e as Error).message}`,
});
}
};