feat: add Richieste management page and table for admin
This commit is contained in:
parent
98d6f5763d
commit
400dc037fe
5 changed files with 329 additions and 0 deletions
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
200
apps/infoalloggi/src/components/tables/richieste-table.tsx
Normal file
200
apps/infoalloggi/src/components/tables/richieste-table.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>;
|
||||||
|
};
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue