feat: implement TabRicerca component for user service search and integrate collapsible functionality

This commit is contained in:
Marco Pedone 2025-08-24 15:47:08 +02:00
parent 4982d064e3
commit 243148267b
6 changed files with 464 additions and 16 deletions

View file

@ -1,7 +1,6 @@
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { LoadingPage } from "~/components/loading"; import { LoadingPage } from "~/components/loading";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import { ServizioContainer } from "~/components/servizio/main";
import { AccordionComp } from "~/components/accordionComp"; import { AccordionComp } from "~/components/accordionComp";
import { useTranslation } from "~/providers/I18nProvider"; import { useTranslation } from "~/providers/I18nProvider";
import TimeserieBarChart from "~/components/timeserieBarChart"; import TimeserieBarChart from "~/components/timeserieBarChart";
@ -13,6 +12,7 @@ import {
CardTitle, CardTitle,
} from "~/components/ui/card"; } from "~/components/ui/card";
import TimeserieBarChartStacked from "~/components/timeserieBarChartStacked"; import TimeserieBarChartStacked from "~/components/timeserieBarChartStacked";
import { TabRicerca } from "~/components/tables/ricerca-table";
export const AdminDashboard = () => { export const AdminDashboard = () => {
const { data: stats, isLoading } = api.stats.adminDashStats.useQuery(); const { data: stats, isLoading } = api.stats.adminDashStats.useQuery();
@ -66,8 +66,8 @@ export const AdminDashboard = () => {
}; };
export const UserDashboard = ({ userId }: { userId: UsersId }) => { export const UserDashboard = ({ userId }: { userId: UsersId }) => {
const { data: servizi, isLoading: loadingServizi } = // const { data: servizi, isLoading: loadingServizi } =
api.servizio.getAllServizioAnnunci.useQuery({ userId }); // api.servizio.getAllServizioAnnunci.useQuery({ userId });
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="mx-1 flex-1 overflow-auto"> <div className="mx-1 flex-1 overflow-auto">
@ -79,7 +79,8 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
</div> </div>
<div className="flex flex-col space-y-5 px-2"> <div className="flex flex-col space-y-5 px-2">
{loadingServizi ? ( <TabRicerca userId={userId} isAdmin={false} />
{/* {loadingServizi ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
servizi?.map((servizio, idx) => ( servizi?.map((servizio, idx) => (
@ -90,7 +91,7 @@ export const UserDashboard = ({ userId }: { userId: UsersId }) => {
isAdmin={false} isAdmin={false}
/> />
)) ))
)} )} */}
</div> </div>
<AccordionComp <AccordionComp
texts={t.area_riservata.dash_accordion_minifaq} texts={t.area_riservata.dash_accordion_minifaq}

View file

@ -12,7 +12,10 @@ import type { ServizioData } from "~/server/controllers/servizio.controller";
import { Package, PackageCheck, UserCircle } from "lucide-react"; import { Package, PackageCheck, UserCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Progress } from "~/components/ui/progress"; import { Progress } from "~/components/ui/progress";
import { ServizioActions } from "~/components/servizio/servizio_actions"; import {
ServizioActions,
ServizioActions2,
} from "~/components/servizio/servizio_actions";
import { SaldoButton } from "~/components/servizio/interactions"; import { SaldoButton } from "~/components/servizio/interactions";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { import {
@ -84,6 +87,270 @@ const ServizioCard = ({
); );
}; };
export const ServizioContent = () => {
const { servizio, isAdmin, userId } = useServizio();
const utils = api.useUtils();
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
onSuccess: async () => {
toast.success("Servizio modificato con successo");
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
},
onError: (error) => {
toast.error(error.message);
},
});
const router = useRouter();
useEffect(() => {
const id = router.asPath.split("#")[1];
if (id) {
document.getElementById(id)?.scrollIntoView({
behavior: "smooth",
});
}
}, []);
if (servizio.isInterrotto) {
return (
<ServizioCard
cardClassName="bg-neutral-100 border-neutral-300"
header={
<>
<div className="flex flex-col gap-4">
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
<Package className="text-primary size-6" />
Cerco Affitto {servizio.tipologia}
</CardTitle>
<CardDescription className="text-foreground/70 space-y-2 pl-2">
<p>
aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})}
</p>
</CardDescription>
</div>
</>
}
content={
<>
<p className="text-lg font-medium">Servizio interrotto</p>
<p>
Non è possibile riutilizzare questo servizio, poichè è stato
interrotto.
</p>
</>
}
/>
);
}
if (!servizio.isOkAcconto || servizio.decorrenza === null) {
return (
<ServizioCard
header={
<div className="flex w-full flex-col gap-2">
{/* <div className="flex w-full flex-col gap-4">
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
<Package className="text-primary size-6" />
Cerco Affitto {servizio.tipologia}
</CardTitle>
<CardDescription className="text-foreground/70 pl-2">
<p>Servizio non attivo</p>
</CardDescription>
</div> */}
<span className="text-foreground/70 text-lg">
Servizio non attivo
</span>
<ServizioActions2 />
</div>
}
content={
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-3">
<Link
href={`/servizio/onboard/${servizio.servizio_id}`}
aria-label="Attiva Servizio"
>
<Button>Procedi all&apos;attivazione</Button>
</Link>
{isAdmin && (
<Button
onClick={() =>
updateServizio({
servizioId: servizio.servizio_id,
data: {
isOkAcconto: true,
decorrenza: new Date(),
},
})
}
variant="success"
>
Attiva Manualmente (Azione Admin)
</Button>
)}
</div>
{isAdmin && (
<AnnunciSelezionati
isConfirming={false}
annunci={servizio.annunci}
/>
)}
</div>
}
/>
);
}
const annuncioConfermato = servizio.annunci.filter(
(a) => a.accettato_conferma_at !== null,
);
if (servizio.isOkSaldo) {
return (
<ServizioCard
header={
<>
{/* <div className="flex w-full flex-wrap items-center gap-2">
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
<Package className="text-primary size-6" />
Cerco Affitto {servizio.tipologia}
</CardTitle>
<CardDescription className="text-foreground/70 space-y-2 pl-2">
<p>
aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})}
</p>
</CardDescription>
</div> */}
<ServizioActions2 />
</>
}
content={
<>
{annuncioConfermato.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
})}
</>
}
/>
);
}
const openContattiCount = servizio.annunci.filter(
(a) => a.open_contatti_at !== null,
).length;
const annunciInConferma = servizio.annunci.filter(
(a) => a.user_confirmed_at !== null,
);
const annunciNotInConferma = servizio.annunci.filter(
(a) => a.user_confirmed_at === null,
);
return (
<ServizioCard
header={
<>
{/* <div className="flex w-full flex-col gap-2">
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
<Package className="text-primary size-6" />
Cerco Affitto {servizio.tipologia}
</CardTitle>
<CardDescription className="text-foreground/70 space-y-2 pl-2">
<p>
aggiunto il{" "}
{servizio.created_at.toLocaleDateString("it", {
year: "numeric",
month: "2-digit",
day: "2-digit",
})}
</p>
</CardDescription>
</div> */}
<ServizioActions2 />
</>
}
content={
<>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<UserCircle className="text-primary size-6" />
<span className="font-medium">Contatti sbloccati</span>
</div>
<span className="text-sm">{openContattiCount} / 10</span>
</div>
<Progress value={openContattiCount * 10} className="h-2" />
</div>
<ServizioDuration decorrenza={servizio.decorrenza} />
{annuncioConfermato.length > 0 && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<PackageCheck className="text-primary size-6" />
<span className="font-medium">Saldi in attesa:</span>
</div>
{servizio.annunci
.filter((a) => a.accettato_conferma_at !== null)
.map((a) => (
<div
key={a.annunci_id}
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
>
<h3>
{a.codice} - confermato il:{" "}
{new Date(a.accettato_conferma_at!).toLocaleDateString(
"it",
{
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
},
)}
</h3>
<SaldoButton
variant={"outline"}
className="border-none"
annuncioId={a.annunci_id}
/>
</div>
))}
</div>
)}
<div className="space-y-2">
<AnnunciInConferma annunci={annunciInConferma} />
<AnnunciSelezionati
isConfirming={annunciInConferma.length > 0}
annunci={annunciNotInConferma}
/>
<AnnunciCompatibili />
</div>
</>
}
/>
);
};
const ServizioMain = () => { const ServizioMain = () => {
const { servizio, isAdmin, userId } = useServizio(); const { servizio, isAdmin, userId } = useServizio();
const utils = api.useUtils(); const utils = api.useUtils();

View file

@ -25,7 +25,7 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "~/components/ui/alert-dialog"; } from "~/components/ui/alert-dialog";
import { Calculator, ClockAlert, Plus, Wrench } from "lucide-react"; import { Bug, Calculator, ClockAlert, Plus, Wrench } from "lucide-react";
import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio"; import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio";
import { import {
Credenza, Credenza,
@ -45,6 +45,11 @@ import {
FormEditServizio, FormEditServizio,
type FormValues as EditFormValues, type FormValues as EditFormValues,
} from "~/forms/FormEditServizioAdmin"; } from "~/forms/FormEditServizioAdmin";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "~/components/ui/collapsible";
export const ServizioActions = () => { export const ServizioActions = () => {
const { servizio, isAdmin, userId } = useServizio(); const { servizio, isAdmin, userId } = useServizio();
@ -74,6 +79,36 @@ export const ServizioActions = () => {
</div> </div>
); );
}; };
export const ServizioActions2 = () => {
const { servizio, isAdmin, userId } = useServizio();
const canUserEdit =
servizio.isOkAcconto &&
servizio.decorrenza !== null &&
!servizio.annunci.some((a) => a.user_confirmed_at !== null);
if (!isAdmin && !canUserEdit) {
return null; // If the user is not an admin and cannot edit, do not render the actions
}
return (
<div className="flex w-full flex-col flex-wrap justify-between gap-3 sm:flex-row">
{(isAdmin || canUserEdit) && (
<div className="flex items-center gap-3">
<span className="hidden sm:block">Azioni: </span>
<EditPreferenze />
<InterruzioneServizio />
</div>
)}
{isAdmin && (
<div className="flex items-center gap-3">
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
<EditServizioAdmin />
</div>
)}
</div>
);
};
export const AddAnnuncio = ({ export const AddAnnuncio = ({
servizioId, servizioId,
userId, userId,
@ -315,7 +350,7 @@ const EditPreferenze = () => {
}; };
const EditServizioAdmin = () => { const EditServizioAdmin = () => {
const { servizioId, userId } = useServizio(); const { servizioId, userId, servizio } = useServizio();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId }); const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId });
const utils = api.useUtils(); const utils = api.useUtils();
@ -362,12 +397,26 @@ const EditServizioAdmin = () => {
Modifica Modifica
</CredenzaDescription> </CredenzaDescription>
</CredenzaHeader> </CredenzaHeader>
<CredenzaBody className="max-h-[80vh] w-full overflow-y-auto pb-5"> <CredenzaBody className="max-h-[80vh] w-full space-y-2 overflow-y-auto pb-5">
{isLoading ? ( {isLoading ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
<FormEditServizio initialData={data} onSubmit={onSubmit} /> <FormEditServizio initialData={data} onSubmit={onSubmit} />
)} )}
<Collapsible>
<CollapsibleTrigger asChild>
<Button variant="warning" className="items-center gap-2">
<Bug /> Debug Info Servizio
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
{(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { annunci, ...rest } = servizio;
return <pre>{JSON.stringify(rest, null, 2)}</pre>;
})()}
</CollapsibleContent>
</Collapsible>
</CredenzaBody> </CredenzaBody>
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>

View file

@ -0,0 +1,109 @@
import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
import { LoadingPage } from "~/components/loading";
import { Package } from "lucide-react";
import { ServizioProvider } from "~/providers/ServizioProvider";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "~/components/ui/collapsible";
import { useState } from "react";
import type { ServizioData } from "~/server/controllers/servizio.controller";
import { ServizioContent } from "~/components/servizio/main";
export const TabRicerca = ({
userId,
isAdmin,
}: {
userId: UsersId;
isAdmin: boolean;
}) => {
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
userId,
});
if (isLoading) return <LoadingPage />;
if (!data || data.length === 0) {
return (
<div className="m-4 rounded-md bg-yellow-100 p-4 text-yellow-900">
Nessun servizio attualmente inserito
</div>
);
}
return (
<div className="flex flex-col gap-2">
{data.map((servizio, idx) => {
return (
<Row
key={idx}
servizio={servizio}
userId={userId}
isAdmin={isAdmin}
/>
);
})}
</div>
);
};
const Row = ({
servizio,
userId,
isAdmin,
}: {
servizio: ServizioData;
userId: UsersId;
isAdmin: boolean;
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<ServizioProvider
isAdmin={isAdmin}
userId={userId}
servizioId={servizio.servizio_id}
servizio={servizio}
>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<div className="border-border flex items-center rounded-md border py-2 shadow-sm hover:shadow-md">
<CollapsibleTrigger className="grid w-full cursor-pointer grid-cols-10 items-center">
<div className="col-span-2 sm:col-span-1">
<span className="text-xl">{isOpen ? "👇" : "👉"}</span>
</div>
<div className="col-span-6 flex items-center gap-2 font-semibold sm:text-lg">
<Package className="text-primary size-6" />
Affitto {servizio.tipologia}
</div>
<div className="col-span-2">
{servizio.isInterrotto ? (
<span className="text-red-500">Interrotto</span>
) : servizio.isOkAcconto && servizio.decorrenza !== null ? (
<span className="text-green-500">Attivo</span>
) : (
<span className="text-neutral-500">Inattivo</span>
)}
</div>
<div className="hidden text-sm sm:col-span-2 sm:contents sm:text-base">
{servizio.created_at.toLocaleDateString("it", {
month: "short",
day: "2-digit",
})}
</div>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-up mt-2 overflow-hidden transition-all duration-300 ease-in-out">
<ServizioProvider
isAdmin={isAdmin}
userId={userId}
servizioId={servizio.servizio_id}
servizio={servizio}
>
<ServizioContent />
</ServizioProvider>
</CollapsibleContent>
</Collapsible>
</ServizioProvider>
);
};

View file

@ -1,12 +1,10 @@
import { LoadingPage } from "~/components/loading";
import { AreaRiservataLayoutUserView } from "~/components/Layout"; import { AreaRiservataLayoutUserView } from "~/components/Layout";
import type { GetStaticProps } from "next"; import type { GetStaticProps } from "next";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import { api } from "~/utils/api";
import { zUserId } from "~/server/utils/zod_types"; import { zUserId } from "~/server/utils/zod_types";
import { ServizioContainer } from "~/components/servizio/main";
import { AnnunciRichiesti } from "~/components/servizio/servizio_annunci_accordions"; import { AnnunciRichiesti } from "~/components/servizio/servizio_annunci_accordions";
import { TabRicerca } from "~/components/tables/ricerca-table";
type RicercaUserProps = { type RicercaUserProps = {
userId: UsersId; userId: UsersId;
@ -15,8 +13,8 @@ type RicercaUserProps = {
const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({ const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
userId, userId,
}: RicercaUserProps) => { }: RicercaUserProps) => {
const { data: servizi, isLoading: loadingServizi } = // const { data: servizi, isLoading: loadingServizi } =
api.servizio.getAllServizioAnnunci.useQuery({ userId }); // api.servizio.getAllServizioAnnunci.useQuery({ userId });
return ( return (
<div className="space-y-5"> <div className="space-y-5">
@ -27,7 +25,8 @@ const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
</div> </div>
<div className="flex flex-col space-y-5"> <div className="flex flex-col space-y-5">
<AnnunciRichiesti userId={userId} /> <AnnunciRichiesti userId={userId} />
{loadingServizi ? ( <TabRicerca userId={userId} isAdmin />
{/* {loadingServizi ? (
<LoadingPage /> <LoadingPage />
) : ( ) : (
servizi?.map((servizio, idx) => ( servizi?.map((servizio, idx) => (
@ -38,7 +37,7 @@ const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
isAdmin isAdmin
/> />
)) ))
)} )} */}
</div> </div>
</div> </div>
); );

View file

@ -100,8 +100,31 @@
} }
} }
@keyframes collapsible-down {
0% {
height: 0;
}
100% {
height: var(--radix-collapsible-content-height);
}
}
@keyframes collapsible-up {
0% {
height: var(--radix-collapsible-content-height);
}
100% {
height: 0;
}
}
--animate-accordion-down: accordion-down 0.3s ease-out; --animate-accordion-down: accordion-down 0.3s ease-out;
--animate-accordion-up: accordion-up 0.3s ease-out; --animate-accordion-up: accordion-up 0.3s ease-out;
--animate-collapsible-down: collapsible-down 0.3s ease-out;
--animate-collapsible-up: collapsible-up 0.3s ease-in;
@keyframes caret-blink { @keyframes caret-blink {