feat: add Richieste management page and table for admin

This commit is contained in:
Marco Pedone 2026-03-11 12:07:19 +01:00
parent 98d6f5763d
commit 400dc037fe
5 changed files with 329 additions and 0 deletions

View file

@ -287,6 +287,7 @@ const Main = () => {
</div> </div>
<AnnunciAnteprima /> <AnnunciAnteprima />
{isAdmin && <AnnunciCompatibili />}
</div> </div>
); );
} }
@ -378,6 +379,7 @@ const Main = () => {
</div> </div>
<AnnunciAnteprima /> <AnnunciAnteprima />
{isAdmin && <AnnunciCompatibili />}
</div> </div>
); );
} }

View file

@ -0,0 +1,200 @@
import type { ColumnDef } from "@tanstack/react-table";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
import { DataTable } from "~/components/custom_ui/data-table";
import { DataTableColumnHeader } from "~/components/custom_ui/dataTable-header";
import type {
PinnedFiltro,
SearchFiltro,
} from "~/components/custom_ui/dataTable-toolbar";
import { Button } from "~/components/ui/button";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { RichiesteData } from "~/server/controllers/servizio.controller";
type RichiesteTableProps = {
data: RichiesteData[];
};
export const RichiesteTable = ({ data }: RichiesteTableProps) => {
const tabledata = data;
const columns_titles = {
id: "ID Ordine",
username: "Utente",
created_at: "Creato il",
status: "Status",
tipo: "Tipo",
annunci: "Annunci",
actions: "Azioni",
};
const statusOptions = [
{ label: "Attesa di attivazione", value: "attesa_attivazione" },
{ label: "Acconto pagato", value: "acconto_pagato" },
{ label: "Saldo pagato", value: "saldo_pagato" },
{ label: "Conferma in corso", value: "conferma" },
];
const type_options = [
{ label: "Transitorio", value: TipologiaPosizioneEnum.Transitorio },
{ label: "Stabile", value: TipologiaPosizioneEnum.Stabile },
];
// const [selectedOrdine, setSelectedOrdine] = useState<OrdiniOrdineId | null>(
// null,
// );
type Column = (typeof tabledata)[number];
const columns: ColumnDef<Column>[] = [
{
accessorKey: "id",
cell: () => null,
enableHiding: false,
header: () => null,
},
{
accessorKey: "username",
cell: ({ row }) => {
return (
<Link
aria-label="Visualizza Utente"
href={`/area-riservata/admin/user-view/ricerca/${row.original.user_id}`}
>
<Button size="sm">{row.original.username}</Button>
</Link>
);
},
enableHiding: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.username}
/>
),
},
{
accessorKey: "created_at",
cell: ({ row }) => {
const date = row.getValue("created_at");
return new Date(date as Date).toLocaleDateString();
},
enableHiding: false,
header: ({ column }) => (
<DataTableColumnHeader
column={column}
title={columns_titles.created_at}
/>
),
sortingFn: "datetime",
},
{
accessorKey: "tipologia",
cell: ({ row }) => {
return row.original.tipologia.toString();
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.tipo} />
),
},
{
accessorKey: "status",
cell: ({ row }) => {
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";
}
},
filterFn: (row, id, value) => {
return value.includes(row.getValue(id));
},
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.status} />
),
},
{
accessorKey: "annunci_servizio",
cell: ({ row }) => {
const annunci = row.original.annunci_servizio;
return <span>{annunci.length}</span>;
},
header: ({ column }) => (
<DataTableColumnHeader column={column} title={columns_titles.annunci} />
),
enableSorting: false,
},
{
cell: () => {
return (
<Button
aria-label="Apri Dettagli Ordine"
//onClick={() => {}}
size="sm"
variant="outline"
>
<ExternalLink className="size-5" />
</Button>
);
},
enableHiding: false,
id: "actions",
},
];
const searchFiltro: SearchFiltro = {
columnName: "id",
placeholder: "Cerca ordine...",
};
const pinnedFiltri: PinnedFiltro[] = [
{
columnName: "status",
options: statusOptions,
title: "Stato Ordine",
},
{
columnName: "tipo",
options: type_options,
title: "Tipo Ordine",
},
];
return (
<div className="mx-auto w-full">
<DataTable
columns={columns}
columns_titles={columns_titles}
data={tabledata}
defaultSort={[{ desc: true, id: "created_at" }]}
pinnedFiltri={pinnedFiltri}
searchColumn={searchFiltro}
/>
{/* <AdminWrapperOrdiniModal
ordineId={selectedOrdine}
setSelected={setSelectedOrdine}
/>
*/}
</div>
);
};

View file

@ -0,0 +1,58 @@
import type { GetServerSideProps } from "next";
import Head from "next/head";
import { AreaRiservataLayout } from "~/components/Layout";
import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page";
import { RichiesteTable } from "~/components/tables/richieste-table";
import type { NextPageWithLayout } from "~/pages/_app";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
import { api } from "~/utils/api";
/**
* Pagina di gestione richieste per admin: /area-riservata/admin/richieste
*/
const Richieste: NextPageWithLayout = () => {
const { data, isLoading } = api.servizio.getRichieste.useQuery();
if (isLoading) return <LoadingPage />;
if (!data) return <Status500 />;
return (
<>
<Head>
<title>Richieste - Infoalloggi.it</title>
</Head>
<div className="mx-3 flex flex-1 flex-col gap-4 overflow-auto pt-5">
<div className="items-start justify-between md:flex">
<div className="mx-1 flex items-center">
<h3 className="font-bold text-2xl">Richieste</h3>
</div>
</div>
<RichiesteTable data={data} />
</div>
</>
);
};
export default Richieste;
export const getServerSideProps = (async (context) => {
const access_token = context.req.cookies.access_token;
const helpers = await TrpcAuthedFetchingIstance({ access_token });
if (helpers) {
await helpers.trpc.servizio.getRichieste.prefetch();
return {
props: {
trpcState: helpers.trpc.dehydrate(),
},
};
}
return {
props: {},
};
}) satisfies GetServerSideProps;
Richieste.getLayout = function getLayout(page) {
return <AreaRiservataLayout>{page}</AreaRiservataLayout>;
};

View file

@ -20,6 +20,7 @@ import {
getCompatibileAnnunci, getCompatibileAnnunci,
getDataPerAcquisto, getDataPerAcquisto,
getOrdineRicevutaData, getOrdineRicevutaData,
getRichieste,
getServiziByUserId, getServiziByUserId,
getServizioDataById, getServizioDataById,
InteractionLogicTipologia, InteractionLogicTipologia,
@ -119,6 +120,9 @@ export const servizioRouter = createTRPCRouter({
.query(async ({ input }) => { .query(async ({ input }) => {
return await getAllServizioAnnunci(input.userId); return await getAllServizioAnnunci(input.userId);
}), }),
getRichieste: adminProcedure.query(async () => {
return await getRichieste();
}),
getServizioData: protectedProcedure getServizioData: protectedProcedure
.input( .input(
z.object({ z.object({

View file

@ -1,3 +1,4 @@
import type { Override } from "TypeHelpers.type";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { add } from "date-fns"; import { add } from "date-fns";
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
@ -1399,3 +1400,67 @@ 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">)[];
};
export const getRichieste = async (): Promise<RichiesteData[]> => {
try {
return await db
.selectFrom("servizio")
.innerJoin("users", "users.id", "servizio.user_id")
.selectAll("servizio")
.select(["users.username"])
.select((eb) => [
jsonArrayFrom(
eb
.selectFrom("servizio_annunci")
.selectAll("servizio_annunci")
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
.select(["annunci.codice", "annunci.titolo_it"])
.whereRef(
"servizio.servizio_id",
"=",
"servizio_annunci.servizio_id",
)
.orderBy("servizio_annunci.created_at", "desc"),
).as("annunci_servizio"),
jsonArrayFrom(
eb
.selectFrom("ordini")
.selectAll("ordini")
.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();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error fetching richieste: ${(e as Error).message}`,
});
}
};