cleanup
This commit is contained in:
parent
1e3a28d81a
commit
40076ce48a
4 changed files with 167 additions and 303 deletions
1
apps/infoalloggi/.vscode/settings.json
vendored
1
apps/infoalloggi/.vscode/settings.json
vendored
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
TODOS:
|
||||
- Updated_at timestamp per appartamento se in backend ho modificato le foto, e uso quello per cache key
|
||||
- Indicare di dover modificare le info nel onboarding
|
||||
- Modale annunci compatibili invece di collapsible
|
||||
- Ordine annunci selezionati non deve variare
|
||||
- Sostituire collapre ricerca con modale con anche counter annunci come in gestionale
|
||||
|
||||
- Modale contatti mettere Proprietario/Referente
|
||||
- Modale contatti, aggiunti tasto whatsapp
|
||||
- Modale contatti, warning contatti in rosso
|
||||
- Mod contatti, caricamento doc identità, con ref salvato nell' servizio-annunci row
|
||||
- Mod conferma deve essere tipo "sei interessato a confermare imm.." con caricamento del file di motivazione
|
||||
|
||||
- Parametri del servizio transitorio specificare termine contrato/scadenza altra motivazione transitoria
|
||||
- Preparazione sevizio, aggiungere flags "può saltare pagamento", "può saltare controllo documento", "può confermare senza doc motivazione"
|
||||
- Se può saltare passaggi i bottoni o sezioni allegare doc sono nascosti
|
||||
- Preparazione sevizio, aggiungere flags "può saltare pagamento", "può saltare controllo documento", "può confermare senza doc motivazione" Se può saltare passaggi i bottoni o sezioni allegare doc sono nascosti
|
||||
|
||||
- Mod conferma deve essere tipo "sei interessato a confermare imm.." con caricamento del file di motivazione
|
||||
- Modale conferma mostrare scorciatoia a doc lavoro o simile per facilitare
|
||||
- Post modale conferma, mostrare se transitorio , modale con informativa tipo:" il saldo del servizio e il periodo di permanenza veranno adeguati alle condizioni stabilite insieme al locatore nel momento della trattativa e alla documentazione presentata come giustificazione di transitorietà" (modificato)
|
||||
|
||||
- Mostrare last update in annunci e in singolo annuncio
|
||||
|
||||
NEW IDEA TODOS:
|
||||
|
|
|
|||
|
|
@ -1,25 +1,16 @@
|
|||
import { ExternalLink, Package, PackageCheck, UserCircle } from "lucide-react";
|
||||
import { Package, PackageCheck, UserCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { type ReactNode, useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { InteressatoButtonBatchV } from "~/components/annuncio-interactions/annuncio_interactions";
|
||||
import { AddButton } from "~/components/servizio/annuncio_actions";
|
||||
import {
|
||||
AnnuncioCard,
|
||||
BasicAnnuncioCard,
|
||||
} from "~/components/servizio/annuncio_card";
|
||||
|
||||
import { AnnuncioCard } from "~/components/servizio/annuncio_card";
|
||||
import { SaldoButton } from "~/components/servizio/interactions";
|
||||
import { ServizioDuration } from "~/components/servizio/service-duration-display";
|
||||
import {
|
||||
ServizioActions,
|
||||
ServizioActions2,
|
||||
} from "~/components/servizio/servizio_actions";
|
||||
import {
|
||||
AnnunciCompatibili,
|
||||
AnnunciInConferma,
|
||||
AnnunciSelezionati,
|
||||
} from "~/components/servizio/servizio_annunci_accordions";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -28,14 +19,6 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Progress } from "~/components/ui/progress";
|
||||
import { cn } from "~/lib/utils";
|
||||
import {
|
||||
|
|
@ -46,6 +29,11 @@ import {
|
|||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { ServizioData } from "~/server/controllers/servizio.controller";
|
||||
import { api } from "~/utils/api";
|
||||
import {
|
||||
AnnunciCompatibili,
|
||||
AnnunciInConferma,
|
||||
AnnunciSelezionati,
|
||||
} from "./servizio_annunci_accordions";
|
||||
|
||||
export const ServizioContainer = ({
|
||||
servizio,
|
||||
|
|
@ -190,12 +178,7 @@ export const ServizioContent = () => {
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<AnnunciSelezionati
|
||||
annunci={servizio.annunci}
|
||||
isConfirming={false}
|
||||
/>
|
||||
)}
|
||||
{isAdmin && <AnnunciSelezionati annunci={servizio.annunci} />}
|
||||
</div>
|
||||
}
|
||||
header={
|
||||
|
|
@ -301,145 +284,10 @@ export const ServizioContent = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* <div className="space-y-2">
|
||||
<AnnunciInConferma annunci={annunciInConferma} />
|
||||
<AnnunciSelezionati
|
||||
annunci={annunciNotInConferma}
|
||||
isConfirming={annunciInConferma.length > 0}
|
||||
/>
|
||||
<AnnunciCompatibili />
|
||||
</div> */}
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogServizio
|
||||
title="Annunci in conferma"
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button
|
||||
disabled={annunciInConferma.length === 0}
|
||||
size="xl"
|
||||
variant="success"
|
||||
>
|
||||
Annunci in conferma
|
||||
</Button>
|
||||
<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">
|
||||
{annunciInConferma.length}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
|
||||
{annunciInConferma.map((data) => {
|
||||
return (
|
||||
<ServizioAnnuncioProvider data={data} key={data.id}>
|
||||
<AnnuncioCard className="border-2 border-green-500" />
|
||||
</ServizioAnnuncioProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DialogServizio>
|
||||
<DialogServizio
|
||||
title="Annunci selezionati"
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button size="xl" variant="orange">
|
||||
Annunci selezionati
|
||||
</Button>
|
||||
<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">
|
||||
{annunciNotInConferma.length}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
|
||||
{annunciNotInConferma.length > 0 ? (
|
||||
annunciNotInConferma.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>
|
||||
</DialogServizio>
|
||||
{(() => {
|
||||
const { servizioId, userId, isAdmin } = useServizio();
|
||||
const { data, isLoading } =
|
||||
api.servizio.getCompatibileAnnunci.useQuery({
|
||||
servizioId,
|
||||
});
|
||||
|
||||
const { data: userIntrests } =
|
||||
api.intrests.getUserInterests.useQuery({
|
||||
userId,
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
return (
|
||||
<DialogServizio
|
||||
title="Annunci compatibili"
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button className="bg-purple-500" size="xl">
|
||||
Annunci compatibili
|
||||
</Button>
|
||||
<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">
|
||||
{!data || data.length === 0 ? (
|
||||
<div className="flex items-center justify-center gap-1 rounded-md bg-white p-4 sm:flex">
|
||||
<span>
|
||||
Nessun annuncio compatibile trovato. Amplia la ricerca
|
||||
per trovare annunci compatibili.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
data.map((a) => {
|
||||
return (
|
||||
<BasicAnnuncioCard
|
||||
className="border-2 border-neutral-500"
|
||||
data={a}
|
||||
interactions={
|
||||
<>
|
||||
{isAdmin ? (
|
||||
<AddButton annuncioId={a.id} />
|
||||
) : (
|
||||
<InteressatoButtonBatchV
|
||||
annuncioId={a.id}
|
||||
hasInterest={
|
||||
userIntrests?.includes(a.id) || false
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
key={a.id}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</DialogServizio>
|
||||
);
|
||||
})()}
|
||||
<AnnunciInConferma annunci={annunciInConferma} />
|
||||
<AnnunciSelezionati annunci={annunciNotInConferma} />
|
||||
<AnnunciCompatibili />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
@ -452,31 +300,6 @@ export const ServizioContent = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const DialogServizio = ({
|
||||
trigger,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className="md:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Servizio Dialog
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const ServizioMain = () => {
|
||||
const { servizio, isAdmin, userId } = useServizio();
|
||||
const utils = api.useUtils();
|
||||
|
|
@ -567,12 +390,7 @@ const ServizioMain = () => {
|
|||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<AnnunciSelezionati
|
||||
annunci={servizio.annunci}
|
||||
isConfirming={false}
|
||||
/>
|
||||
)}
|
||||
{isAdmin && <AnnunciSelezionati annunci={servizio.annunci} />}
|
||||
</div>
|
||||
}
|
||||
header={
|
||||
|
|
@ -704,10 +522,7 @@ const ServizioMain = () => {
|
|||
|
||||
<div className="space-y-2">
|
||||
<AnnunciInConferma annunci={annunciInConferma} />
|
||||
<AnnunciSelezionati
|
||||
annunci={annunciNotInConferma}
|
||||
isConfirming={annunciInConferma.length > 0}
|
||||
/>
|
||||
<AnnunciSelezionati annunci={annunciNotInConferma} />
|
||||
<AnnunciCompatibili />
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { ExternalLink, Trash2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { InteressatoButtonBatchV } from "~/components/annuncio-interactions/annuncio_interactions";
|
||||
import { AddButton } from "~/components/servizio/annuncio_actions";
|
||||
|
|
@ -21,84 +22,126 @@ import {
|
|||
import type { UsersId } from "~/schemas/public/Users";
|
||||
import type { ServizioData } from "~/server/controllers/servizio.controller";
|
||||
import { api } from "~/utils/api";
|
||||
import { LoadingPage } from "../loading";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../ui/dialog";
|
||||
|
||||
const DialogServizio = ({
|
||||
trigger,
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent className="md:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Servizio Dialog
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export const AnnunciInConferma = ({
|
||||
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>
|
||||
<DialogServizio
|
||||
title="Annunci in conferma"
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button
|
||||
className="text-lg"
|
||||
disabled={annunci.length === 0}
|
||||
size="xl"
|
||||
variant="success"
|
||||
>
|
||||
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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
|
||||
{annunci.map((data) => {
|
||||
return (
|
||||
<ServizioAnnuncioProvider data={data} key={data.id}>
|
||||
<AnnuncioCard className="border-2 border-green-500" />
|
||||
</ServizioAnnuncioProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DialogServizio>
|
||||
);
|
||||
};
|
||||
|
||||
export const AnnunciSelezionati = ({
|
||||
isConfirming,
|
||||
annunci,
|
||||
}: {
|
||||
isConfirming: boolean;
|
||||
annunci: ServizioData["annunci"];
|
||||
}) => {
|
||||
return (
|
||||
<Accordion
|
||||
collapsible
|
||||
defaultValue={!isConfirming ? "not-in-conferma" : undefined}
|
||||
type="single"
|
||||
<DialogServizio
|
||||
title="Annunci selezionati"
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button className="text-lg" size="xl" variant="orange">
|
||||
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}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AccordionItem className="space-y-4" value="not-in-conferma">
|
||||
<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 className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
|
||||
{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>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</DialogServizio>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -112,20 +155,27 @@ export const AnnunciCompatibili = () => {
|
|||
userId,
|
||||
});
|
||||
|
||||
if (isLoading) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
collapsible
|
||||
defaultValue={!data || data.length === 0 ? "compatibili" : undefined}
|
||||
type="single"
|
||||
<DialogServizio
|
||||
title="Annunci compatibili"
|
||||
trigger={
|
||||
<div className="relative">
|
||||
<Button className="bg-purple-500 text-lg" size="xl">
|
||||
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>
|
||||
}
|
||||
>
|
||||
<AccordionItem className="space-y-4" value="compatibili">
|
||||
<AccordionTrigger className="cursor-pointer rounded-md border-2 border-neutral-500 bg-white p-2">
|
||||
Annunci Compatibili:
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="flex flex-col flex-wrap gap-5">
|
||||
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
|
||||
{isLoading ? (
|
||||
<LoadingPage />
|
||||
) : (
|
||||
<>
|
||||
{!data || data.length === 0 ? (
|
||||
<div className="flex items-center justify-center gap-1 rounded-md bg-white p-4 sm:flex">
|
||||
<span>
|
||||
|
|
@ -134,32 +184,30 @@ export const AnnunciCompatibili = () => {
|
|||
</span>
|
||||
</div>
|
||||
) : (
|
||||
data.map((a) => {
|
||||
return (
|
||||
<BasicAnnuncioCard
|
||||
className="border-2 border-neutral-500"
|
||||
data={a}
|
||||
interactions={
|
||||
<>
|
||||
{isAdmin ? (
|
||||
<AddButton annuncioId={a.id} />
|
||||
) : (
|
||||
<InteressatoButtonBatchV
|
||||
annuncioId={a.id}
|
||||
hasInterest={userIntrests?.includes(a.id) || false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
key={a.id}
|
||||
/>
|
||||
);
|
||||
})
|
||||
data.map((a) => (
|
||||
<BasicAnnuncioCard
|
||||
className="border-2 border-neutral-500"
|
||||
data={a}
|
||||
interactions={
|
||||
<>
|
||||
{isAdmin ? (
|
||||
<AddButton annuncioId={a.id} />
|
||||
) : (
|
||||
<InteressatoButtonBatchV
|
||||
annuncioId={a.id}
|
||||
hasInterest={userIntrests?.includes(a.id) || false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
key={a.id}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogServizio>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue