feat: add service management features and update Italian translations
- Introduced new service status translations in Italian. - Added service detail and management components for user and admin views. - Implemented API endpoints for fetching service data and announcements. - Enhanced service data handling with improved date parsing and formatting. - Created new pages for service details accessible by both users and admins. - Updated database queries to support new service functionalities.
This commit is contained in:
parent
6cd09cce1b
commit
ac1859e88f
27 changed files with 1446 additions and 786 deletions
|
|
@ -5,6 +5,7 @@ import { ContattoAnnuncio } from "~/components/annuncio-interactions/contatto_mo
|
||||||
import LoadingButton from "~/components/custom_ui/loading-button";
|
import LoadingButton from "~/components/custom_ui/loading-button";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
import { useAnnuncio } from "~/providers/AnnuncioProvider";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useServizio } from "~/providers/ServizioProvider";
|
import { useServizio } from "~/providers/ServizioProvider";
|
||||||
import type { SessionContextType } from "~/providers/SessionProvider";
|
import type { SessionContextType } from "~/providers/SessionProvider";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
|
|
@ -19,11 +20,13 @@ export const AnnuncioInteractions = ({
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { id, tipo } = useAnnuncio();
|
const { id, tipo } = useAnnuncio();
|
||||||
|
const { locale } = useTranslation();
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-md bg-red-500 p-2 text-center font-semibold text-base text-white">
|
<div className="w-full rounded-md bg-red-500 p-2 text-center font-semibold text-base text-white">
|
||||||
Richieste Temporaneamente disattivate
|
{locale === "it"
|
||||||
|
? "Richieste Temporaneamente disattivate"
|
||||||
|
: "Requests Temporarily Disabled"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +40,7 @@ export const AnnuncioInteractions = ({
|
||||||
userId={session.user.id}
|
userId={session.user.id}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span>Errore</span>
|
<span>{locale === "it" ? "Errore" : "Error"}</span>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
|
@ -70,6 +73,7 @@ const ServizioInteraction = ({
|
||||||
tipologia: string;
|
tipologia: string;
|
||||||
annuncioId: AnnunciId;
|
annuncioId: AnnunciId;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { locale } = useTranslation();
|
||||||
const { data: servizio, isLoading } =
|
const { data: servizio, isLoading } =
|
||||||
api.servizio.AnnuncioServizioTipologiaMatch.useQuery({
|
api.servizio.AnnuncioServizioTipologiaMatch.useQuery({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
|
|
@ -85,7 +89,11 @@ const ServizioInteraction = ({
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Non hai un servizio attivo</p>
|
<p>
|
||||||
|
{locale === "it"
|
||||||
|
? "Non hai un servizio attivo"
|
||||||
|
: "You do not have an active service"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -94,14 +102,21 @@ const ServizioInteraction = ({
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
<div className="w-full text-center font-semibold text-green-500">
|
<div className="w-full text-center font-semibold text-green-500">
|
||||||
Annuncio già salvato!
|
{locale === "it" ? "Annuncio già salvato!" : "Listing already saved!"}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
aria-label="Vai alla tua area riservata"
|
aria-label={
|
||||||
|
locale === "it"
|
||||||
|
? "Vai alla tua area riservata"
|
||||||
|
: "Go to your reserved area"
|
||||||
|
}
|
||||||
href={`/area-riservata/dashboard`}
|
href={`/area-riservata/dashboard`}
|
||||||
>
|
>
|
||||||
<Button className="w-full" variant="success">
|
<Button className="w-full" variant="success">
|
||||||
Vai alla tua area riservata <ArrowRight />
|
{locale === "it"
|
||||||
|
? "Vai alla tua area riservata"
|
||||||
|
: "Go to your reserved area"}{" "}
|
||||||
|
<ArrowRight />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -116,6 +131,7 @@ const InteressatoButtonUserOnly = ({
|
||||||
}: {
|
}: {
|
||||||
annuncioId: AnnunciId;
|
annuncioId: AnnunciId;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { locale } = useTranslation();
|
||||||
const { data: hasInterest, isLoading } =
|
const { data: hasInterest, isLoading } =
|
||||||
api.intrests.hasUserInterest.useQuery({
|
api.intrests.hasUserInterest.useQuery({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
|
|
@ -126,7 +142,11 @@ const InteressatoButtonUserOnly = ({
|
||||||
await utils.intrests.hasUserInterest.invalidate({
|
await utils.intrests.hasUserInterest.invalidate({
|
||||||
annuncioId,
|
annuncioId,
|
||||||
});
|
});
|
||||||
toast.success("Richiesta di interesse inviata con successo!");
|
toast.success(
|
||||||
|
locale === "it"
|
||||||
|
? "Richiesta di interesse inviata con successo!"
|
||||||
|
: "Interest request sent successfully!",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -134,7 +154,7 @@ const InteressatoButtonUserOnly = ({
|
||||||
return (
|
return (
|
||||||
<Button className="w-full" disabled>
|
<Button className="w-full" disabled>
|
||||||
<BadgePlus className="size-6" />
|
<BadgePlus className="size-6" />
|
||||||
Caricamento...
|
{locale === "it" ? "Caricamento..." : "Loading..."}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -142,14 +162,16 @@ const InteressatoButtonUserOnly = ({
|
||||||
return (
|
return (
|
||||||
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 font-semibold text-sm text-white">
|
<span className="flex w-full items-center justify-center gap-2 rounded-md bg-green-500 p-2 font-semibold text-sm text-white">
|
||||||
<BadgePlus className="inline size-5" />
|
<BadgePlus className="inline size-5" />
|
||||||
Hai già mandato una richiesta
|
{locale === "it"
|
||||||
|
? "Hai già mandato una richiesta"
|
||||||
|
: "You have already sent a request"}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Sono interessato"
|
aria-label={locale === "it" ? "Sono interessato" : "I'm interested"}
|
||||||
className="w-full text-lg md:text-base lg:text-lg"
|
className="w-full text-lg md:text-base lg:text-lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
add({
|
add({
|
||||||
|
|
@ -160,7 +182,7 @@ const InteressatoButtonUserOnly = ({
|
||||||
variant="success"
|
variant="success"
|
||||||
>
|
>
|
||||||
<BadgePlus className="size-6" />
|
<BadgePlus className="size-6" />
|
||||||
Sono interessato
|
{locale === "it" ? "Sono interessato" : "I'm interested"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -171,6 +193,7 @@ export const InteressatoButtonServizio = ({
|
||||||
annuncioId: AnnunciId;
|
annuncioId: AnnunciId;
|
||||||
}) => {
|
}) => {
|
||||||
const { isAdmin, servizioId, userId } = useServizio();
|
const { isAdmin, servizioId, userId } = useServizio();
|
||||||
|
const { locale } = useTranslation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const invalidateAll = async () => {
|
const invalidateAll = async () => {
|
||||||
|
|
@ -227,7 +250,11 @@ export const InteressatoButtonServizio = ({
|
||||||
variant="info"
|
variant="info"
|
||||||
>
|
>
|
||||||
<BadgePlus className="size-6" />
|
<BadgePlus className="size-6" />
|
||||||
{isAdmin ? "Aggiungi" : "Sono interessato"}
|
{isAdmin
|
||||||
|
? "Aggiungi"
|
||||||
|
: locale === "it"
|
||||||
|
? "Sono interessato"
|
||||||
|
: "I'm interested"}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { CircleHelp } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { AccordionComp } from "~/components/accordionComp";
|
import { AccordionComp } from "~/components/accordionComp";
|
||||||
import { LoadingPage } from "~/components/loading";
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { TabRicerca } from "~/components/tables/ricerca-table";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
|
@ -22,13 +21,14 @@ import {
|
||||||
CredenzaTitle,
|
CredenzaTitle,
|
||||||
CredenzaTrigger,
|
CredenzaTrigger,
|
||||||
} from "../custom_ui/credenza";
|
} from "../custom_ui/credenza";
|
||||||
|
import { ServizioList } from "../servizio/servizio";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
const TimeserieBarChart = dynamic(
|
const TimeserieBarChart = dynamic(
|
||||||
() => import("~/components/timeserieBarChart").then((mod) => mod.default),
|
() => import("~/components/timeserieBarChart").then((mod) => mod.default),
|
||||||
{
|
{
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <div className="h-[468px] animate-pulse bg-gray-100" />,
|
loading: () => <div className="h-117 animate-pulse bg-gray-100" />,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ const TimeserieBarChartStacked = dynamic(
|
||||||
import("~/components/timeserieBarChartStacked").then((mod) => mod.default),
|
import("~/components/timeserieBarChartStacked").then((mod) => mod.default),
|
||||||
{
|
{
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <div className="h-[468px] animate-pulse bg-gray-100" />,
|
loading: () => <div className="h-117 animate-pulse bg-gray-100" />,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -94,12 +94,18 @@ export const AdminDashboard = () => {
|
||||||
|
|
||||||
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
export const UserDashboard = ({ userId }: { userId: UsersId }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex w-full flex-1 flex-col items-start justify-center gap-4 overflow-auto p-2 md:gap-6">
|
<div className="mt-2 flex w-full flex-1 flex-col items-start justify-center gap-4 overflow-auto p-2 md:gap-6">
|
||||||
<h3 className="font-bold text-2xl">Le tue ricerche</h3>
|
<h3 className="font-bold text-2xl">Le tue ricerche</h3>
|
||||||
|
|
||||||
<div className="flex w-full flex-1 grow flex-col space-y-5">
|
<div className="flex w-full flex-1 grow flex-col space-y-5">
|
||||||
<TabRicerca isAdmin={false} userId={userId} />
|
<ServizioList isAdmin={false} servizi={data || []} userId={userId} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto">
|
<div className="mx-auto">
|
||||||
<Credenza>
|
<Credenza>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getStorageUrl } from "~/lib/storage_utils";
|
import { getStorageUrl } from "~/lib/storage_utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useSession } from "~/providers/SessionProvider";
|
import { useSession } from "~/providers/SessionProvider";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
|
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
|
||||||
|
|
@ -29,6 +30,7 @@ export const DocumentiPersonali = ({
|
||||||
userData: CompleteUserData;
|
userData: CompleteUserData;
|
||||||
}) => {
|
}) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { locale } = useTranslation();
|
||||||
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
|
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await utils.users.getClientProfilo.invalidate({
|
await utils.users.getClientProfilo.invalidate({
|
||||||
|
|
@ -56,12 +58,19 @@ export const DocumentiPersonali = ({
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Documenti personali</CardTitle>
|
<CardTitle>
|
||||||
|
{locale === "it" ? "Documenti personali" : "Personal Documents"}
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<p>I tuoi documenti sono necessari per usufruire del servizio.</p>
|
|
||||||
<p>
|
<p>
|
||||||
Nel caso di un unico documento (es: scansione fronte e retro del
|
{locale === "it"
|
||||||
documento) caricarlo 2 volte.
|
? "I tuoi documenti sono necessari per usufruire del servizio."
|
||||||
|
: "Your documents are required to use the service."}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{locale === "it"
|
||||||
|
? "Nel caso di un unico documento (es: scansione fronte e retro del documento) caricarlo 2 volte."
|
||||||
|
: "In case of a single document (e.g., front and back scan of the document), upload it twice."}
|
||||||
</p>
|
</p>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -100,10 +109,11 @@ export const DocumentiPersonali = ({
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
I tuoi dati saranno conservati solamente per la durata del servizio,
|
{locale === "it"
|
||||||
in conformità con la nostra{" "}
|
? "I tuoi dati saranno conservati solamente per la durata del servizio, in conformità con la nostra "
|
||||||
|
: "Your data will be stored only for the duration of the service, in accordance with our "}
|
||||||
<Link className="underline" href="/privacy-policy" target="_blank">
|
<Link className="underline" href="/privacy-policy" target="_blank">
|
||||||
Informativa sulla Privacy
|
{locale === "it" ? "Informativa sulla Privacy" : "Privacy Policy"}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|
@ -133,6 +143,7 @@ export const DocumentiUploadSection = ({
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
caricatoNote?: boolean;
|
caricatoNote?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { locale } = useTranslation();
|
||||||
const [imageError, setImageError] = useState(false);
|
const [imageError, setImageError] = useState(false);
|
||||||
if (document?.storageId) {
|
if (document?.storageId) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -141,7 +152,9 @@ export const DocumentiUploadSection = ({
|
||||||
{caricatoNote && (
|
{caricatoNote && (
|
||||||
<div className="flex items-center gap-2 text-green-500">
|
<div className="flex items-center gap-2 text-green-500">
|
||||||
<CircleCheck className="size-5" />
|
<CircleCheck className="size-5" />
|
||||||
<span>Documento caricato</span>
|
<span>
|
||||||
|
{locale === "it" ? "Documento caricato" : "Document uploaded"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
|
@ -165,7 +178,11 @@ export const DocumentiUploadSection = ({
|
||||||
src={getStorageUrl({ storageId: document.storageId })}
|
src={getStorageUrl({ storageId: document.storageId })}
|
||||||
title="doc"
|
title="doc"
|
||||||
>
|
>
|
||||||
<p>Il tuo browser non supporta gli iframe.</p>
|
<p>
|
||||||
|
{locale === "it"
|
||||||
|
? "Il tuo browser non supporta gli iframe."
|
||||||
|
: "Your browser does not support iframes."}
|
||||||
|
</p>
|
||||||
</iframe>
|
</iframe>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -175,23 +192,31 @@ export const DocumentiUploadSection = ({
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button size="sm" variant="outline">
|
<Button size="sm" variant="outline">
|
||||||
<span>Visualizza</span>
|
<span>{locale === "it" ? "Visualizza" : "View"}</span>
|
||||||
<ExternalLink className="size-4" />
|
<ExternalLink className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{onRemove && (
|
{onRemove && (
|
||||||
<Confirm
|
<Confirm
|
||||||
description="Sei sicuro di voler eliminare il documento?"
|
description={
|
||||||
|
locale === "it"
|
||||||
|
? "Sei sicuro di voler eliminare il documento?"
|
||||||
|
: "Are you sure you want to delete the document?"
|
||||||
|
}
|
||||||
onConfirm={onRemove}
|
onConfirm={onRemove}
|
||||||
title="Elimina Documento"
|
title={
|
||||||
|
locale === "it" ? "Elimina Documento" : "Delete Document"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Elimina documento"
|
aria-label={
|
||||||
|
locale === "it" ? "Elimina documento" : "Delete document"
|
||||||
|
}
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
<span>Rimuovi</span>
|
<span>{locale === "it" ? "Rimuovi" : "Remove"}</span>
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Confirm>
|
</Confirm>
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export default function ChatBottombar({
|
||||||
|
|
||||||
const isMobile = useMediaQuery("(min-width: 426px)");
|
const isMobile = useMediaQuery("(min-width: 426px)");
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[74px] w-full items-center justify-between gap-2 p-2">
|
<div className="flex min-h-18.5 w-full items-center justify-between gap-2 p-2">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<ChatAttachments chatUserData={chatUserData} userData={userData} />
|
<ChatAttachments chatUserData={chatUserData} userData={userData} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@ export const ChatSidebar = ({ chatId }: { chatId: ChatsChatid | null }) => {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="grid gap-1 rounded-lg group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
<nav className="grid gap-1 rounded-lg group-data-[collapsed=true]:justify-center group-data-[collapsed=true]:px-2">
|
||||||
{activeChats.map((chat) => (
|
{activeChats.map((chat) => (
|
||||||
<ContextMenu key={chat.userinfo.username}>
|
<ContextMenu key={chat.userinfo.username}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "~/components/ui/tooltip";
|
} from "~/components/ui/tooltip";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
|
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
@ -124,7 +125,7 @@ export const AdminActions = () => {
|
||||||
export const UserActions = () => {
|
export const UserActions = () => {
|
||||||
const { servizioId } = useServizio();
|
const { servizioId } = useServizio();
|
||||||
const { data, annuncioId } = useServizioAnnuncio();
|
const { data, annuncioId } = useServizioAnnuncio();
|
||||||
|
const { locale } = useTranslation();
|
||||||
const ContattiSbloccati = data.open_contatti_at !== null;
|
const ContattiSbloccati = data.open_contatti_at !== null;
|
||||||
const ConfermaUtente = data.user_confirmed_at !== null;
|
const ConfermaUtente = data.user_confirmed_at !== null;
|
||||||
const ConfermaLavorata = data.doc_conferma_added && data.hasConfermaAdmin;
|
const ConfermaLavorata = data.doc_conferma_added && data.hasConfermaAdmin;
|
||||||
|
|
@ -140,24 +141,30 @@ export const UserActions = () => {
|
||||||
|
|
||||||
{AccettatoConferma ? (
|
{AccettatoConferma ? (
|
||||||
<Link
|
<Link
|
||||||
aria-label="Conferma immobile"
|
aria-label={
|
||||||
|
locale === "it" ? "Rivedi la conferma" : "Review the confirmation"
|
||||||
|
}
|
||||||
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
|
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
|
||||||
>
|
>
|
||||||
<Button className="w-full sm:w-fit" variant="success">
|
<Button className="w-full sm:w-fit" variant="success">
|
||||||
<Pointer />
|
<Pointer />
|
||||||
Rivedi la conferma
|
{locale === "it"
|
||||||
|
? "Rivedi la conferma"
|
||||||
|
: "Review the confirmation"}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
ConfermaLavorata &&
|
ConfermaLavorata &&
|
||||||
(data.doc_conferma_ref ? (
|
(data.doc_conferma_ref ? (
|
||||||
<Link
|
<Link
|
||||||
aria-label="Conferma immobile"
|
aria-label={locale === "it" ? "Confermato" : "Confirmed"}
|
||||||
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
|
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
|
||||||
>
|
>
|
||||||
<Button className="w-full sm:w-fit" variant="success">
|
<Button className="w-full sm:w-fit" variant="success">
|
||||||
<Pointer />
|
<Pointer />
|
||||||
Confermato - Procedi ora
|
{locale === "it"
|
||||||
|
? "Confermato - Procedi ora"
|
||||||
|
: "Confirmed - Proceed now"}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -169,13 +176,21 @@ export const UserActions = () => {
|
||||||
variant="success"
|
variant="success"
|
||||||
>
|
>
|
||||||
<Pointer />
|
<Pointer />
|
||||||
Confermato - Procedi ora
|
{locale === "it"
|
||||||
|
? "Confermato - Procedi ora"
|
||||||
|
: "Confirmed - Proceed now"}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Il documento non è più disponibile</p>
|
|
||||||
<p>
|
<p>
|
||||||
Se hai bisogno di una copia, contattaci via email o chat.
|
{locale === "it"
|
||||||
|
? "Il documento non è più disponibile"
|
||||||
|
: "The document is no longer available"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{locale === "it"
|
||||||
|
? "Se hai bisogno di una copia, contattaci via email o chat."
|
||||||
|
: "If you need a copy, contact us via email or chat."}
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -192,15 +207,26 @@ export const UserActions = () => {
|
||||||
const PostConfermaSection = () => {
|
const PostConfermaSection = () => {
|
||||||
const { servizio, servizioId } = useServizio();
|
const { servizio, servizioId } = useServizio();
|
||||||
const { data, annuncioId } = useServizioAnnuncio();
|
const { data, annuncioId } = useServizioAnnuncio();
|
||||||
|
const { locale } = useTranslation();
|
||||||
const saldoPaid = servizio.isOkSaldo;
|
const saldoPaid = servizio.isOkSaldo;
|
||||||
const consulenzaPaid = servizio.isOkConsulenza;
|
const consulenzaPaid = servizio.isOkConsulenza;
|
||||||
|
|
||||||
|
const doc_non_disponibile =
|
||||||
|
locale === "it"
|
||||||
|
? "Il documento non è più disponibile"
|
||||||
|
: "The document is no longer available";
|
||||||
|
const contatta_supporto =
|
||||||
|
locale === "it"
|
||||||
|
? "Se hai bisogno di una copia, contattaci via email o chat."
|
||||||
|
: "If you need a copy, contact us via email or chat.";
|
||||||
|
|
||||||
if (!saldoPaid) {
|
if (!saldoPaid) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
Questo annuncio è stato confermato, procedi al saldo:
|
{locale === "it"
|
||||||
|
? "Questo annuncio è stato confermato, procedi al saldo:"
|
||||||
|
: "This listing has been confirmed, proceed to payment:"}
|
||||||
</h3>
|
</h3>
|
||||||
<SaldoButton annuncioId={annuncioId} />
|
<SaldoButton annuncioId={annuncioId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -210,10 +236,14 @@ const PostConfermaSection = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h3 className="font-semibold text-green-500">
|
<h3 className="font-semibold text-green-500">
|
||||||
Ci siamo quasi! Stiamo preparando il contratto per te.
|
{locale === "it"
|
||||||
|
? "Ci siamo quasi! Stiamo preparando il contratto per te."
|
||||||
|
: "Almost there! We are preparing the contract for you."}
|
||||||
</h3>
|
</h3>
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
Una volta pronto, riceverai una notifica via email.
|
{locale === "it"
|
||||||
|
? "Una volta pronto, riceverai una notifica via email."
|
||||||
|
: "Once ready, you will receive a notification via email."}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -223,19 +253,27 @@ const PostConfermaSection = () => {
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{consulenzaPaid
|
{consulenzaPaid
|
||||||
|
? locale === "it"
|
||||||
? "Il contratto è pronto, puoi visualizzarlo:"
|
? "Il contratto è pronto, puoi visualizzarlo:"
|
||||||
: "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"}
|
: "The contract is ready, you can view it:"
|
||||||
|
: locale === "it"
|
||||||
|
? "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"
|
||||||
|
: "The contract is ready, you can view it and proceed to payment:"}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{data.doc_contratto_ref ? (
|
{data.doc_contratto_ref ? (
|
||||||
<Link
|
<Link
|
||||||
aria-label="Visualizza contratto"
|
aria-label={
|
||||||
|
locale === "it" ? "Visualizza contratto" : "View contract"
|
||||||
|
}
|
||||||
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
|
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button className="w-full sm:w-fit">
|
<Button className="w-full sm:w-fit">
|
||||||
<FileText />
|
<FileText />
|
||||||
Visualizza il contratto
|
{locale === "it"
|
||||||
|
? "Visualizza il contratto"
|
||||||
|
: "View the contract"}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -244,12 +282,14 @@ const PostConfermaSection = () => {
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button className="w-fit cursor-not-allowed opacity-50">
|
<Button className="w-fit cursor-not-allowed opacity-50">
|
||||||
<FileText />
|
<FileText />
|
||||||
Visualizza il contratto
|
{locale === "it"
|
||||||
|
? "Visualizza il contratto"
|
||||||
|
: "View the contract"}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Il documento non è più disponibile</p>
|
<p>{doc_non_disponibile}</p>
|
||||||
<p>Se hai bisogno di una copia, contattaci via email o chat.</p>
|
<p>{contatta_supporto}</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
@ -260,13 +300,17 @@ const PostConfermaSection = () => {
|
||||||
{data.doc_registrazione_added &&
|
{data.doc_registrazione_added &&
|
||||||
(data.doc_registrazione_ref ? (
|
(data.doc_registrazione_ref ? (
|
||||||
<Link
|
<Link
|
||||||
aria-label="Visualizza registrazione"
|
aria-label={
|
||||||
|
locale === "it" ? "Visualizza registrazione" : "View recording"
|
||||||
|
}
|
||||||
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
|
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button className="w-fit">
|
<Button className="w-fit">
|
||||||
<FileBadge />
|
<FileBadge />
|
||||||
Visualizza la registrazione
|
{locale === "it"
|
||||||
|
? "Visualizza la registrazione"
|
||||||
|
: "View the recording"}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -275,14 +319,14 @@ const PostConfermaSection = () => {
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button className="w-fit cursor-not-allowed opacity-50">
|
<Button className="w-fit cursor-not-allowed opacity-50">
|
||||||
<FileBadge />
|
<FileBadge />
|
||||||
Visualizza la registrazione
|
{locale === "it"
|
||||||
|
? "Visualizza la registrazione"
|
||||||
|
: "View the recording"}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Il documento non è più disponibile</p>
|
<p>{doc_non_disponibile}</p>
|
||||||
<p>
|
<p>{contatta_supporto}</p>
|
||||||
Se hai bisogno di una copia, contattaci via email o chat.
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
@ -301,24 +345,30 @@ export const SaldoButton = ({
|
||||||
variant?: ButtonConfigs["variant"];
|
variant?: ButtonConfigs["variant"];
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { locale } = useTranslation();
|
||||||
const { servizioId } = useServizio();
|
const { servizioId } = useServizio();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutate } = api.servizio.setupSaldoServizio.useMutation({
|
const { mutate } = api.servizio.setupSaldoServizio.useMutation({
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
toast.loading("Caricamento ...", { id: "preparazioneSaldo" });
|
toast.loading(locale === "it" ? "Caricamento ..." : "Loading ...", {
|
||||||
},
|
|
||||||
onSuccess: async (data) => {
|
|
||||||
toast.success("Pagamento saldo preparato", {
|
|
||||||
id: "preparazioneSaldo",
|
id: "preparazioneSaldo",
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
toast.success(
|
||||||
|
locale === "it" ? "Pagamento saldo preparato" : "Payment prepared",
|
||||||
|
{
|
||||||
|
id: "preparazioneSaldo",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await router.push(`/servizio/pagamento/${data}`);
|
await router.push(`/servizio/pagamento/${data}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Procedi al saldo"
|
aria-label={locale === "it" ? "Procedi al saldo" : "Proceed to payment"}
|
||||||
className={cn("w-fit", className)}
|
className={cn("w-fit", className)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutate({
|
mutate({
|
||||||
|
|
@ -329,7 +379,7 @@ export const SaldoButton = ({
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
<PackageCheck />
|
<PackageCheck />
|
||||||
Procedi al saldo
|
{locale === "it" ? "Procedi al saldo" : "Proceed to payment"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -341,25 +391,35 @@ const SaldoConsulenzaButton = ({
|
||||||
variant?: ButtonConfigs["variant"];
|
variant?: ButtonConfigs["variant"];
|
||||||
className?: string;
|
className?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const { locale } = useTranslation();
|
||||||
const { annuncioId } = useServizioAnnuncio();
|
const { annuncioId } = useServizioAnnuncio();
|
||||||
const { servizioId } = useServizio();
|
const { servizioId } = useServizio();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutate } = api.servizio.setupSaldoConsulenzaServizio.useMutation({
|
const { mutate } = api.servizio.setupSaldoConsulenzaServizio.useMutation({
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
toast.loading("Caricamento ...", { id: "preparazioneSaldo" });
|
toast.loading(locale === "it" ? "Caricamento ..." : "Loading ...", {
|
||||||
},
|
|
||||||
onSuccess: async (data) => {
|
|
||||||
toast.success("Pagamento saldo preparato", {
|
|
||||||
id: "preparazioneSaldo",
|
id: "preparazioneSaldo",
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
toast.success(
|
||||||
|
locale === "it" ? "Pagamento saldo preparato" : "Payment prepared",
|
||||||
|
{
|
||||||
|
id: "preparazioneSaldo",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await router.push(`/servizio/pagamento/${data}`);
|
await router.push(`/servizio/pagamento/${data}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
aria-label="Procedi al saldo consulenza"
|
aria-label={
|
||||||
|
locale === "it"
|
||||||
|
? "Procedi al saldo consulenza"
|
||||||
|
: "Proceed to payment for the consultation"
|
||||||
|
}
|
||||||
className={cn("w-fit", className)}
|
className={cn("w-fit", className)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutate({
|
mutate({
|
||||||
|
|
@ -370,7 +430,9 @@ const SaldoConsulenzaButton = ({
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
<PackageCheck />
|
<PackageCheck />
|
||||||
Procedi al saldo della consulenza
|
{locale === "it"
|
||||||
|
? "Procedi al saldo della consulenza"
|
||||||
|
: "Proceed to payment for the consultation"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export const AnnuncioDisplay = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid @md:grid-cols-[12rem_1fr] grid-cols-[7rem_1fr] gap-x-4 gap-y-2 rounded-md border border-card bg-card md:max-w-3xl lg:gap-x-4 xl:gap-x-6",
|
"grid @md:grid-cols-[12rem_1fr] grid-cols-[7rem_1fr] gap-x-0 gap-y-2 rounded-md bg-card md:max-w-3xl lg:gap-x-4 xl:gap-x-6",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -127,7 +127,7 @@ export const AnnuncioDisplay = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-1 flex w-full max-w-xl flex-wrap gap-5 md:col-span-1 md:mt-4 md:pr-8">
|
<div className="col-span-1 flex w-full max-w-xl flex-wrap gap-5 pt-2 md:col-span-1 md:mt-4 md:pr-8">
|
||||||
<span className="text-wrap">{data.titolo_it}</span>
|
<span className="text-wrap">{data.titolo_it}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,21 +51,22 @@ export const AnnuncioDettaglio = ({ data }: { data: AnnuncioRicerca }) => {
|
||||||
className="w-full"
|
className="w-full"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
<Expand /> Dettaglio
|
<Expand /> {locale === "it" ? "Dettaglio" : "Detail"}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
<CredenzaContent className="max-w-xl sm:max-w-6xl md:max-h-[90vh] md:overflow-y-auto">
|
<CredenzaContent className="max-w-xl sm:max-w-6xl md:max-h-[90vh] md:overflow-y-auto">
|
||||||
<CredenzaHeader className="flex-row items-center justify-between gap-4 md:justify-start">
|
<CredenzaHeader className="flex-row items-center justify-between gap-4 md:justify-start">
|
||||||
<CredenzaTitle className="text-xl">
|
<CredenzaTitle className="text-xl">
|
||||||
Annuncio {data.codice}
|
{locale === "it" ? "Annuncio" : "Listing"} {data.codice}
|
||||||
</CredenzaTitle>
|
</CredenzaTitle>
|
||||||
<Link href={`/annuncio/${data.codice}`} target="_blank">
|
<Link href={`/annuncio/${data.codice}`} target="_blank">
|
||||||
<Button size="sm" variant="info">
|
<Button size="sm" variant="info">
|
||||||
<span>Pagina completa</span> <ExternalLink />
|
<span>{locale === "it" ? "Pagina completa" : "Full page"}</span>{" "}
|
||||||
|
<ExternalLink />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<CredenzaDescription className="sr-only">
|
<CredenzaDescription className="sr-only">
|
||||||
Dettaglio
|
{locale === "it" ? "Dettaglio" : "Detail"}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="h-full overflow-auto pb-5">
|
<CredenzaBody className="h-full overflow-auto pb-5">
|
||||||
|
|
@ -83,7 +84,9 @@ export const AnnuncioDettaglio = ({ data }: { data: AnnuncioRicerca }) => {
|
||||||
<TipoBadge tipo={data.tipo} />
|
<TipoBadge tipo={data.tipo} />
|
||||||
{data.stato === "Trattativa" && (
|
{data.stato === "Trattativa" && (
|
||||||
<Badge className="bg-violet-500 font-semibold text-md">
|
<Badge className="bg-violet-500 font-semibold text-md">
|
||||||
Annuncio in Trattativa
|
{locale === "it"
|
||||||
|
? "Annuncio in Trattativa"
|
||||||
|
: "Listing in Negotiation"}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { CheckCircle } from "lucide-react";
|
import { CheckCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useServizio } from "~/providers/ServizioProvider";
|
import { useServizio } from "~/providers/ServizioProvider";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { CardAnnuncio } from "../annuncio_card";
|
import { CardAnnuncio } from "../annuncio_card";
|
||||||
|
|
@ -23,6 +24,7 @@ import { AnnuncioDettaglio } from "./annuncio_dettaglio";
|
||||||
|
|
||||||
export const AnnunciCompatibili = () => {
|
export const AnnunciCompatibili = () => {
|
||||||
const { servizioId, isAdmin } = useServizio();
|
const { servizioId, isAdmin } = useServizio();
|
||||||
|
const { locale } = useTranslation();
|
||||||
const [adminOverride, setAdminOverride] = useState(false);
|
const [adminOverride, setAdminOverride] = useState(false);
|
||||||
const { data, isLoading } = api.servizio.getCompatibileAnnunci.useQuery({
|
const { data, isLoading } = api.servizio.getCompatibileAnnunci.useQuery({
|
||||||
servizioId,
|
servizioId,
|
||||||
|
|
@ -32,13 +34,17 @@ export const AnnunciCompatibili = () => {
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="w-full bg-purple-500 hover:bg-purple-600 sm:w-fit dark:text-white">
|
<Button className="w-full bg-purple-500 hover:bg-purple-600 sm:w-fit dark:text-white">
|
||||||
Esplora Annunci Compatibili
|
{locale === "it"
|
||||||
|
? "Esplora Annunci Compatibili"
|
||||||
|
: "Explore Compatible Listings"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-xl p-2 sm:max-w-5xl sm:p-6 md:max-w-7xl 2xl:max-w-400">
|
<DialogContent className="max-w-xl p-2 sm:max-w-5xl sm:p-6 md:max-w-7xl 2xl:max-w-400">
|
||||||
<DialogHeader className="flex-row flex-wrap items-center gap-5 pt-6">
|
<DialogHeader className="flex-row flex-wrap items-center gap-5 pt-6">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Annunci compatibili con i parametri del servizio
|
{locale === "it"
|
||||||
|
? "Annunci compatibili con i parametri del servizio"
|
||||||
|
: "Listings compatible with the service parameters"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="flex flex-wrap items-center gap-x-2">
|
<div className="flex flex-wrap items-center gap-x-2">
|
||||||
|
|
@ -63,8 +69,9 @@ export const AnnunciCompatibili = () => {
|
||||||
<>
|
<>
|
||||||
{!data || data.length === 0 ? (
|
{!data || data.length === 0 ? (
|
||||||
<span>
|
<span>
|
||||||
Nessun annuncio compatibile trovato. Amplia la ricerca per
|
{locale === "it"
|
||||||
trovare annunci compatibili.
|
? "Nessun annuncio compatibile trovato. Amplia la ricerca per trovare annunci compatibili."
|
||||||
|
: "No compatible listings found. Broaden your search to find compatible listings."}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative z-0 mx-auto grid max-w-full grid-flow-row grid-cols-1 gap-14 sm:grid-cols-2 sm:gap-6 sm:gap-y-10 lg:grid-cols-3 2xl:grid-cols-4">
|
<div className="relative z-0 mx-auto grid max-w-full grid-flow-row grid-cols-1 gap-14 sm:grid-cols-2 sm:gap-6 sm:gap-y-10 lg:grid-cols-3 2xl:grid-cols-4">
|
||||||
|
|
@ -89,7 +96,11 @@ export const AnnunciCompatibili = () => {
|
||||||
className="flex h-9 w-full items-center justify-center gap-2 text-base [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
className="flex h-9 w-full items-center justify-center gap-2 text-base [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
|
||||||
variant="success"
|
variant="success"
|
||||||
>
|
>
|
||||||
<span>Richiesta inviata</span>
|
<span>
|
||||||
|
{locale === "it"
|
||||||
|
? "Richiesta inviata"
|
||||||
|
: "Request sent"}
|
||||||
|
</span>
|
||||||
<CheckCircle />
|
<CheckCircle />
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ const InvioConferma = ({ closeModal }: { closeModal: () => void }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocCheckpoint = () => {
|
const DocCheckpoint = () => {
|
||||||
const { t } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const { userId, servizio, isAdmin, servizioId } = useServizio();
|
const { userId, servizio, isAdmin, servizioId } = useServizio();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
|
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
|
||||||
|
|
@ -172,8 +172,9 @@ const DocCheckpoint = () => {
|
||||||
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<p>
|
<p>
|
||||||
Devi ancora caricare un tuo documento legato alla permanenza e
|
{locale === "it"
|
||||||
motivazione di transitorietà
|
? "Devi ancora caricare un tuo documento legato alla permanenza e motivazione di transitorietà"
|
||||||
|
: "You still need to upload a document related to your stay and reason for transience"}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<DocumentiUploadSection
|
<DocumentiUploadSection
|
||||||
|
|
@ -182,17 +183,18 @@ const DocCheckpoint = () => {
|
||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
isPending={isPending}
|
isPending={isPending}
|
||||||
onUpload={handleUpload}
|
onUpload={handleUpload}
|
||||||
title="Documento:"
|
title={locale === "it" ? "Documento:" : "Document:"}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
I tuoi dati saranno conservati solamente per la durata del servizio,
|
{locale === "it"
|
||||||
in conformità con la nostra{" "}
|
? "I tuoi dati saranno conservati solamente per la durata del servizio, in conformità con la nostra "
|
||||||
|
: "Your data will be stored only for the duration of the service, in accordance with our "}
|
||||||
<Link className="underline" href="/privacy-policy" target="_blank">
|
<Link className="underline" href="/privacy-policy" target="_blank">
|
||||||
Informativa sulla Privacy
|
{locale === "it" ? "Informativa sulla Privacy" : "Privacy Policy"}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -209,7 +211,7 @@ const DocCheckpoint = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ConfermaInLavorazioneModal = () => {
|
export const ConfermaInLavorazioneModal = () => {
|
||||||
const { t } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const { data } = useServizioAnnuncio();
|
const { data } = useServizioAnnuncio();
|
||||||
return (
|
return (
|
||||||
<Credenza>
|
<Credenza>
|
||||||
|
|
@ -219,19 +221,27 @@ export const ConfermaInLavorazioneModal = () => {
|
||||||
className="w-full sm:w-fit"
|
className="w-full sm:w-fit"
|
||||||
>
|
>
|
||||||
<CircleCheck />
|
<CircleCheck />
|
||||||
Conferma in lavorazione
|
{locale === "it"
|
||||||
|
? "Conferma in lavorazione"
|
||||||
|
: "Confirmation in progress"}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
<CredenzaContent className="max-h-[90vh]">
|
<CredenzaContent className="max-h-[90vh]">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>In attesa di conferma</CredenzaTitle>
|
<CredenzaTitle>
|
||||||
|
{locale === "it"
|
||||||
|
? "In attesa di conferma"
|
||||||
|
: "Awaiting confirmation"}
|
||||||
|
</CredenzaTitle>
|
||||||
<CredenzaDescription className="sr-only">
|
<CredenzaDescription className="sr-only">
|
||||||
Consegna status modal
|
Consegna status modal
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="prose dark:prose-invert max-h-[80vh] overflow-auto pb-5">
|
<CredenzaBody className="prose dark:prose-invert max-h-[80vh] overflow-auto pb-5">
|
||||||
<p>
|
<p>
|
||||||
Richiesta di conferma inviata il:{" "}
|
{locale === "it"
|
||||||
|
? "Richiesta di conferma inviata il:"
|
||||||
|
: "Confirmation request sent on:"}{" "}
|
||||||
{data.user_confirmed_at &&
|
{data.user_confirmed_at &&
|
||||||
new Date(data.user_confirmed_at).toLocaleString("it", {
|
new Date(data.user_confirmed_at).toLocaleString("it", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
|
|
@ -242,9 +252,9 @@ export const ConfermaInLavorazioneModal = () => {
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
La conferma è ancora in lavorazione. Stiamo verificando con i
|
{locale === "it"
|
||||||
proprietari le condizioni e dettagli della conferma per riferirteli.
|
? "La conferma è ancora in lavorazione. Stiamo verificando con i proprietari le condizioni e dettagli della conferma per riferirteli. Attendi una comunicazione."
|
||||||
Attendi una comunicazione.
|
: "The confirmation is still in progress. We are checking with the owners the conditions and details of the confirmation to report them to you. Please wait for a communication."}
|
||||||
</p>
|
</p>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,372 +0,0 @@
|
||||||
import { add } from "date-fns";
|
|
||||||
import { ArrowRight, Package, PackageCheck, Trash2 } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
import { SaldoButton } from "~/components/servizio/annuncio_actions";
|
|
||||||
import {
|
|
||||||
AnnuncioCard,
|
|
||||||
AnnuncioDisplay,
|
|
||||||
} from "~/components/servizio/annuncio_card";
|
|
||||||
import { ServizioActions } from "~/components/servizio/servizio_actions";
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "~/components/ui/card";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import {
|
|
||||||
ServizioAnnuncioProvider,
|
|
||||||
useServizio,
|
|
||||||
} from "~/providers/ServizioProvider";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { Confirm } from "../confirm";
|
|
||||||
import { AlarmClockSVG } from "../svgs";
|
|
||||||
import { AnnunciCompatibili } from "./compatibili_dialog";
|
|
||||||
|
|
||||||
const ServizioCard = ({
|
|
||||||
cardClassName,
|
|
||||||
header,
|
|
||||||
headerClassName,
|
|
||||||
content,
|
|
||||||
contentClassName,
|
|
||||||
}: {
|
|
||||||
cardClassName?: string;
|
|
||||||
header: React.ReactNode;
|
|
||||||
headerClassName?: string;
|
|
||||||
content: React.ReactNode;
|
|
||||||
contentClassName?: string;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={cn(
|
|
||||||
"gap-0 rounded-none rounded-b-md border-primary border-t-0 pb-0",
|
|
||||||
cardClassName,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardHeader
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col items-start justify-between gap-3 space-y-0 px-4 pb-8 sm:flex-row",
|
|
||||||
headerClassName,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{header}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className={cn("space-y-4 p-4 pt-0", contentClassName)}>
|
|
||||||
{content}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ServizioContent = () => {
|
|
||||||
const { servizio, isAdmin, userId, servizioId } = useServizio();
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
},
|
|
||||||
onSuccess: async () => {
|
|
||||||
toast.success("Servizio modificato con successo");
|
|
||||||
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
|
|
||||||
await utils.servizio.getPacksSolution.invalidate({ servizioId });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const { mutate } = api.servizio.removeAnnuncioServizio.useMutation({
|
|
||||||
onError: (error) => {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("Errore durante la rimozione dell'annuncio");
|
|
||||||
},
|
|
||||||
onSuccess: async () => {
|
|
||||||
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
|
|
||||||
await utils.servizio.getCompatibileAnnunci.invalidate({
|
|
||||||
servizioId,
|
|
||||||
});
|
|
||||||
toast.success("Annuncio rimosso");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const router = useRouter();
|
|
||||||
useEffect(() => {
|
|
||||||
const id = router.asPath.split("#")[1];
|
|
||||||
if (id) {
|
|
||||||
document.getElementById(id)?.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (servizio.isInterrotto) {
|
|
||||||
return (
|
|
||||||
<ServizioCard
|
|
||||||
cardClassName="gap-4"
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<p className="font-medium text-lg">Servizio interrotto</p>
|
|
||||||
<p>
|
|
||||||
Non è possibile riutilizzare questo servizio, poichè è stato
|
|
||||||
interrotto.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-xl sm:text-2xl">
|
|
||||||
<Package className="size-6 text-primary" />
|
|
||||||
Cerco Affitto {servizio.tipologia}
|
|
||||||
</CardTitle>
|
|
||||||
|
|
||||||
<CardDescription className="space-y-2 pl-2 text-foreground/70">
|
|
||||||
<p>
|
|
||||||
aggiunto il{" "}
|
|
||||||
{servizio.created_at.toLocaleDateString("it", {
|
|
||||||
day: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!servizio.isOkAcconto || servizio.decorrenza === null) {
|
|
||||||
return (
|
|
||||||
<ServizioCard
|
|
||||||
content={
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-4">
|
|
||||||
<div className="flex w-full max-w-md flex-col items-center justify-center gap-4 text-center">
|
|
||||||
<AlarmClockSVG className="size-46" />
|
|
||||||
<h1 className="text-xl">Servizio non ancora attivato</h1>
|
|
||||||
<Link
|
|
||||||
aria-label="Attiva Servizio"
|
|
||||||
className="w-full"
|
|
||||||
href={`/servizio/onboard/${servizio.servizio_id}`}
|
|
||||||
>
|
|
||||||
<Button className="w-full text-lg" variant="success">
|
|
||||||
<span>Attiva il Servizio Ora!</span>
|
|
||||||
<ArrowRight />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{isAdmin && (
|
|
||||||
<Button
|
|
||||||
className="w-full"
|
|
||||||
onClick={() =>
|
|
||||||
updateServizio({
|
|
||||||
data: {
|
|
||||||
decorrenza: new Date(),
|
|
||||||
isOkAcconto: true,
|
|
||||||
},
|
|
||||||
servizioId: servizio.servizio_id,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
variant="success"
|
|
||||||
>
|
|
||||||
Attiva Manualmente (Azione Admin)
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{servizio.annunci.length > 0 && (
|
|
||||||
<div className="w-full space-y-2">
|
|
||||||
<p>
|
|
||||||
Sono già stati selezionati {servizio.annunci.length} annunci
|
|
||||||
che potrebbero interessarti.
|
|
||||||
</p>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
{servizio.annunci.map((data) => (
|
|
||||||
<div className="group relative" key={data.id}>
|
|
||||||
<AnnuncioDisplay
|
|
||||||
className="bg-[#e6e9ec]/50 dark:bg-card dark:outline dark:outline-foreground/10"
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{isAdmin && (
|
|
||||||
<Confirm
|
|
||||||
description="Sei sicuro di voler rimuovere questo annuncio?"
|
|
||||||
onConfirm={() => {
|
|
||||||
mutate({
|
|
||||||
annuncioId: data.annunci_id,
|
|
||||||
servizioId: servizio.servizio_id,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
title="Rimuovi Annuncio"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
aria-label="Rimuovi"
|
|
||||||
className="absolute top-0 right-0 hidden group-hover:flex"
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
>
|
|
||||||
<Trash2 />
|
|
||||||
</Button>
|
|
||||||
</Confirm>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{servizio.annunci.length % 2 === 1 && (
|
|
||||||
<div className="hidden size-full rounded-md border border-(--pattern-fg) bg-[repeating-linear-gradient(45deg,var(--pattern-fg)_0,var(--pattern-fg)_1px,transparent_0,transparent_50%)] bg-size-[10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] sm:block dark:[--pattern-fg:#e1e1e1]" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
contentClassName={cn(!isAdmin && "gap-0")}
|
|
||||||
header={
|
|
||||||
<div className="flex w-full flex-col gap-2">
|
|
||||||
<ServizioActions />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
headerClassName={cn(!isAdmin && "pb-0")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
new Date() >
|
|
||||||
add(servizio.decorrenza, {
|
|
||||||
days: 60,
|
|
||||||
}) &&
|
|
||||||
!servizio.isOkSaldo
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<ServizioCard
|
|
||||||
cardClassName="gap-4"
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
<p className="font-medium text-lg">Servizio Scaduto</p>
|
|
||||||
<p>
|
|
||||||
Non è possibile riutilizzare questo servizio, poichè il periodo di
|
|
||||||
scelta degli annunci è scaduto (60 giorni dalla data di
|
|
||||||
decorrenza) e non è stato confermato nessun immobile.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
<>
|
|
||||||
<ServizioActions />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
headerClassName="pb-0"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const annuncioConfermato = servizio.annunci.filter(
|
|
||||||
(a) => a.accettato_conferma_at !== null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (servizio.isOkSaldo && annuncioConfermato.length > 0) {
|
|
||||||
return (
|
|
||||||
<ServizioCard
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{annuncioConfermato.map((data) => {
|
|
||||||
return (
|
|
||||||
<ServizioAnnuncioProvider
|
|
||||||
data={data}
|
|
||||||
key={data.id}
|
|
||||||
servizioId={servizio.servizio_id}
|
|
||||||
userId={userId}
|
|
||||||
>
|
|
||||||
<AnnuncioCard />
|
|
||||||
</ServizioAnnuncioProvider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
<>
|
|
||||||
<ServizioActions />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ServizioCard
|
|
||||||
cardClassName="gap-y-2"
|
|
||||||
content={
|
|
||||||
<>
|
|
||||||
{annuncioConfermato.length > 0 && (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<PackageCheck className="size-6 text-primary" />
|
|
||||||
<span className="font-medium">Saldi in attesa:</span>
|
|
||||||
</div>
|
|
||||||
{servizio.annunci
|
|
||||||
.filter((a) => a.accettato_conferma_at !== null)
|
|
||||||
.map((a) => (
|
|
||||||
<div
|
|
||||||
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
|
|
||||||
key={a.annunci_id}
|
|
||||||
>
|
|
||||||
<h3>
|
|
||||||
{a.codice} - confermato il:{" "}
|
|
||||||
{a.accettato_conferma_at &&
|
|
||||||
new Date(a.accettato_conferma_at).toLocaleDateString(
|
|
||||||
"it",
|
|
||||||
{
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
month: "2-digit",
|
|
||||||
year: "numeric",
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<SaldoButton
|
|
||||||
annuncioId={a.annunci_id}
|
|
||||||
className="border-none"
|
|
||||||
variant={"outline"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{servizio.annunci.map((data) => {
|
|
||||||
return (
|
|
||||||
<ServizioAnnuncioProvider
|
|
||||||
data={data}
|
|
||||||
key={data.id}
|
|
||||||
servizioId={servizio.servizio_id}
|
|
||||||
userId={userId}
|
|
||||||
>
|
|
||||||
<AnnuncioCard />
|
|
||||||
</ServizioAnnuncioProvider>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-center">
|
|
||||||
<AnnunciCompatibili />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
<>
|
|
||||||
<ServizioActions />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
headerClassName="pb-0"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const ContattiProprietarioModal = () => {
|
||||||
const { servizio } = useServizio();
|
const { servizio } = useServizio();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const { data } = useServizioAnnuncio();
|
const { data } = useServizioAnnuncio();
|
||||||
const { t } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const isUnlocked = data.open_contatti_at != null;
|
const isUnlocked = data.open_contatti_at != null;
|
||||||
|
|
||||||
const needsDocuments =
|
const needsDocuments =
|
||||||
|
|
@ -49,7 +49,7 @@ export const ContattiProprietarioModal = () => {
|
||||||
variant="success"
|
variant="success"
|
||||||
>
|
>
|
||||||
<CircleUser />
|
<CircleUser />
|
||||||
Contatti
|
{locale === "it" ? "Contatti" : "Contacts"}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -58,7 +58,7 @@ export const ContattiProprietarioModal = () => {
|
||||||
variant="warning"
|
variant="warning"
|
||||||
>
|
>
|
||||||
<CircleUser />
|
<CircleUser />
|
||||||
Ottieni i contatti
|
{locale === "it" ? "Ottieni i contatti" : "Get contacts"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
|
|
@ -82,7 +82,7 @@ export const ContattiProprietarioModal = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocCheckpoint = () => {
|
const DocCheckpoint = () => {
|
||||||
const { t } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
const { userId, servizio, isAdmin } = useServizio();
|
const { userId, servizio, isAdmin } = useServizio();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
|
const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({
|
||||||
|
|
@ -115,10 +115,15 @@ const DocCheckpoint = () => {
|
||||||
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
|
||||||
<div className="flex flex-col flex-wrap gap-4">
|
<div className="flex flex-col flex-wrap gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold">Carica i tuoi documenti</p>
|
<p className="font-semibold">
|
||||||
|
{locale === "it"
|
||||||
|
? "Carica i tuoi documenti"
|
||||||
|
: "Upload your documents"}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
se si ha una scansione unica fronte/retro dei doc, caricarla due
|
{locale === "it"
|
||||||
volte.
|
? "se si ha una scansione unica fronte/retro dei doc, caricarla due volte."
|
||||||
|
: "if you have a single front/back scan of the documents, upload it twice."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -157,10 +162,11 @@ const DocCheckpoint = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
I tuoi dati saranno conservati solamente per la durata del servizio,
|
{locale === "it"
|
||||||
in conformità con la nostra{" "}
|
? "I tuoi dati saranno conservati solamente per la durata del servizio, in conformità con la nostra "
|
||||||
|
: "Your data will be stored only for the duration of the service, in accordance with our "}
|
||||||
<Link className="underline" href="/privacy-policy" target="_blank">
|
<Link className="underline" href="/privacy-policy" target="_blank">
|
||||||
Informativa sulla Privacy
|
{locale === "it" ? "Informativa sulla Privacy" : "Privacy Policy"}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -179,7 +185,7 @@ const DocCheckpoint = () => {
|
||||||
const PreUnlock = () => {
|
const PreUnlock = () => {
|
||||||
const { userId, servizioId } = useServizio();
|
const { userId, servizioId } = useServizio();
|
||||||
const { annuncioId } = useServizioAnnuncio();
|
const { annuncioId } = useServizioAnnuncio();
|
||||||
const { t } = useTranslation();
|
const { t, locale } = useTranslation();
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
|
@ -227,8 +233,11 @@ const PreUnlock = () => {
|
||||||
<div className="flex items-center gap-2 rounded-md bg-yellow-200 p-2">
|
<div className="flex items-center gap-2 rounded-md bg-yellow-200 p-2">
|
||||||
<TriangleAlert className="size-8" />
|
<TriangleAlert className="size-8" />
|
||||||
<p>
|
<p>
|
||||||
Il proprietario riceverà la notifica alle ore 9.00{" "}
|
{locale === "it"
|
||||||
{currentHour >= 20 && "di domani"}
|
? "Il proprietario riceverà la notifica alle ore 9.00"
|
||||||
|
: "The owner will receive the notification at 9:00 AM"}{" "}
|
||||||
|
{currentHour >= 20 &&
|
||||||
|
(locale === "it" ? "di domani" : "tomorrow")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@ export const ServiziHeader = ({ userId }: { userId: UsersId }) => {
|
||||||
const [editData, setEditData] = useState<Servizio | undefined>(undefined);
|
const [editData, setEditData] = useState<Servizio | undefined>(undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex w-full flex-wrap items-center justify-between gap-2">
|
||||||
|
<h3 className="font-semibold text-xl">Servizi cliente</h3>
|
||||||
<NewServizioModal
|
<NewServizioModal
|
||||||
editData={editData}
|
editData={editData}
|
||||||
open={open}
|
open={open}
|
||||||
|
|
|
||||||
448
apps/infoalloggi/src/components/servizio/servizio.tsx
Normal file
448
apps/infoalloggi/src/components/servizio/servizio.tsx
Normal file
|
|
@ -0,0 +1,448 @@
|
||||||
|
import { add } from "date-fns";
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
CalendarClock,
|
||||||
|
CircleCheck,
|
||||||
|
CircleCheckBig,
|
||||||
|
CircleMinus,
|
||||||
|
ClockFading,
|
||||||
|
type LucideIcon,
|
||||||
|
PackageCheck,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { cn } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import {
|
||||||
|
ServizioAnnuncioProvider,
|
||||||
|
ServizioProvider,
|
||||||
|
useServizio,
|
||||||
|
} from "~/providers/ServizioProvider";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import type { ServizioData } from "~/server/controllers/servizio.controller";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { Confirm } from "../confirm";
|
||||||
|
import { AlarmClockSVG } from "../svgs";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../ui/card";
|
||||||
|
import { SaldoButton } from "./annuncio_actions";
|
||||||
|
import { AnnuncioCard, AnnuncioDisplay } from "./annuncio_card";
|
||||||
|
import { AnnunciCompatibili } from "./compatibili_dialog";
|
||||||
|
import {
|
||||||
|
AdminActions,
|
||||||
|
ServizioBasicActions,
|
||||||
|
ServizioPacksInfos,
|
||||||
|
} from "./servizio_actions";
|
||||||
|
|
||||||
|
export const ServizioList = ({
|
||||||
|
servizi,
|
||||||
|
isAdmin,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
servizi: ServizioData[];
|
||||||
|
isAdmin: boolean;
|
||||||
|
userId: UsersId;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
if (servizi.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="m-4 rounded-md bg-yellow-100 p-4 text-yellow-900">
|
||||||
|
{t.servizio.nessun_servizio}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (servizi.length === 1 && servizi[0]) {
|
||||||
|
return <Servizio isAdmin={isAdmin} servizio={servizi[0]} userId={userId} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{servizi.map((servizio) => (
|
||||||
|
<ServizioLinkCard
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
key={servizio.servizio_id}
|
||||||
|
servizio={servizio}
|
||||||
|
userId={userId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ServizioLinkCard = ({
|
||||||
|
servizio,
|
||||||
|
isAdmin,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
servizio: ServizioData;
|
||||||
|
isAdmin: boolean;
|
||||||
|
userId: UsersId;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
let status: keyof typeof t.servizio.status = "attivo";
|
||||||
|
let StatusIcon: LucideIcon = CircleCheck;
|
||||||
|
|
||||||
|
if (servizio.isInterrotto) {
|
||||||
|
status = "interrotto";
|
||||||
|
StatusIcon = Trash2;
|
||||||
|
} else if (!servizio.isOkAcconto) {
|
||||||
|
status = "in_attesa";
|
||||||
|
StatusIcon = ClockFading;
|
||||||
|
} else if (servizio.decorrenza) {
|
||||||
|
const now = new Date();
|
||||||
|
const decorrenza = new Date(servizio.decorrenza);
|
||||||
|
if (now > add(decorrenza, { days: 60 }) && !servizio.isOkSaldo) {
|
||||||
|
status = "scaduto";
|
||||||
|
StatusIcon = CircleMinus;
|
||||||
|
}
|
||||||
|
if (servizio.isOkSaldo) {
|
||||||
|
status = "completato";
|
||||||
|
StatusIcon = CircleCheckBig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="group"
|
||||||
|
href={
|
||||||
|
isAdmin
|
||||||
|
? `/area-riservata/admin/user-view/servizio/${userId}/${servizio.servizio_id}`
|
||||||
|
: `/area-riservata/servizio/${servizio.servizio_id}`
|
||||||
|
}
|
||||||
|
key={servizio.servizio_id}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
status === "attivo" && "border-green-500",
|
||||||
|
status === "in_attesa" && "border-yellow-500",
|
||||||
|
status === "interrotto" && "border-red-500",
|
||||||
|
status === "scaduto" && "border-gray-500",
|
||||||
|
status === "completato" && "border-blue-500",
|
||||||
|
"transition-shadow hover:bg-muted/15 hover:shadow-md",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="group-hover:underline">
|
||||||
|
{t.servizio.servizio_titolo} {servizio.tipologia}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="">
|
||||||
|
{t.servizio.servizio_creato_il}{" "}
|
||||||
|
{servizio.created_at.toLocaleDateString("it-IT")}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col flex-wrap gap-4 sm:flex-row sm:justify-between">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2",
|
||||||
|
status === "attivo" && "text-green-500",
|
||||||
|
status === "in_attesa" && "text-yellow-500",
|
||||||
|
status === "interrotto" && "text-red-500",
|
||||||
|
status === "scaduto" && "text-gray-500",
|
||||||
|
status === "completato" && "text-blue-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<StatusIcon className="size-5" />
|
||||||
|
|
||||||
|
<span>{t.servizio.status[status]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 rounded-md text-muted-foreground group-hover:text-foreground">
|
||||||
|
<span>{t.servizio.vai_al_servizio}</span>
|
||||||
|
<ArrowRight />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Servizio = ({
|
||||||
|
servizio,
|
||||||
|
userId,
|
||||||
|
isAdmin,
|
||||||
|
}: {
|
||||||
|
servizio: ServizioData;
|
||||||
|
userId: UsersId;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ServizioProvider
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
servizio={servizio}
|
||||||
|
servizioId={servizio.servizio_id}
|
||||||
|
userId={userId}
|
||||||
|
>
|
||||||
|
<Content />
|
||||||
|
</ServizioProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = () => {
|
||||||
|
const { servizio } = useServizio();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dec_txt = servizio.decorrenza
|
||||||
|
? new Date(servizio.decorrenza).toLocaleDateString("it-IT")
|
||||||
|
: "N/A";
|
||||||
|
const end_txt = servizio.decorrenza
|
||||||
|
? new Date(add(servizio.decorrenza, { days: 60 })).toLocaleDateString(
|
||||||
|
"it-IT",
|
||||||
|
)
|
||||||
|
: "N/A";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
<div className="flex flex-col flex-wrap items-start justify-between gap-4 sm:flex-row">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-xl">
|
||||||
|
{t.servizio.servizio_titolo} {servizio.tipologia}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-muted-foreground text-sm">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CalendarClock className="size-5" />
|
||||||
|
<span>{t.servizio.durata_servizio}</span>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{dec_txt} - {end_txt}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<AdminActions />
|
||||||
|
<ServizioBasicActions className="w-full sm:justify-end" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ServizioPacksInfos />
|
||||||
|
<Main />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Main = () => {
|
||||||
|
const { servizio, isAdmin, userId, servizioId } = useServizio();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
toast.success("Servizio modificato con successo");
|
||||||
|
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
|
||||||
|
await utils.servizio.getPacksSolution.invalidate({ servizioId });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: removeAnnuncioServizio } =
|
||||||
|
api.servizio.removeAnnuncioServizio.useMutation({
|
||||||
|
onError: (error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error("Errore durante la rimozione dell'annuncio");
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
|
||||||
|
await utils.servizio.getCompatibileAnnunci.invalidate({
|
||||||
|
servizioId,
|
||||||
|
});
|
||||||
|
toast.success("Annuncio rimosso");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (servizio.isInterrotto) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="font-medium text-lg">{t.servizio.servizio_interrotto}</p>
|
||||||
|
<p>{t.servizio.servizio_interrotto_desc}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!servizio.isOkAcconto || servizio.decorrenza === null) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-4">
|
||||||
|
<div className="flex w-full max-w-md flex-col items-center justify-center gap-4 text-center">
|
||||||
|
<AlarmClockSVG className="size-46" />
|
||||||
|
<h1 className="text-xl">{t.servizio.non_attivo}</h1>
|
||||||
|
<Link
|
||||||
|
aria-label="Attiva Servizio"
|
||||||
|
className="w-full"
|
||||||
|
href={`/servizio/onboard/${servizio.servizio_id}`}
|
||||||
|
>
|
||||||
|
<Button className="w-full text-lg" variant="success">
|
||||||
|
<span>{t.servizio.attiva_cta}</span>
|
||||||
|
<ArrowRight />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={() =>
|
||||||
|
updateServizio({
|
||||||
|
data: {
|
||||||
|
decorrenza: new Date(),
|
||||||
|
isOkAcconto: true,
|
||||||
|
},
|
||||||
|
servizioId: servizio.servizio_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
variant="success"
|
||||||
|
>
|
||||||
|
Attiva Manualmente (Azione Admin)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{servizio.annunci.length > 0 && (
|
||||||
|
<div className="w-full space-y-2">
|
||||||
|
<p>
|
||||||
|
{t.servizio.per_te_1} {servizio.annunci.length}{" "}
|
||||||
|
{t.servizio.per_te_2}
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
{servizio.annunci.map((data) => (
|
||||||
|
<div className="group relative" key={data.id}>
|
||||||
|
<AnnuncioDisplay
|
||||||
|
className="bg-[#e6e9ec]/50 dark:bg-card dark:outline dark:outline-foreground/10"
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isAdmin && (
|
||||||
|
<Confirm
|
||||||
|
description="Sei sicuro di voler rimuovere questo annuncio?"
|
||||||
|
onConfirm={() => {
|
||||||
|
removeAnnuncioServizio({
|
||||||
|
annuncioId: data.annunci_id,
|
||||||
|
servizioId: servizio.servizio_id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title="Rimuovi Annuncio"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
aria-label="Rimuovi"
|
||||||
|
className="absolute top-0 right-0 hidden group-hover:flex"
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
</Button>
|
||||||
|
</Confirm>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{servizio.annunci.length % 2 === 1 && (
|
||||||
|
<div className="hidden size-full rounded-md border border-(--pattern-fg) bg-[repeating-linear-gradient(45deg,var(--pattern-fg)_0,var(--pattern-fg)_1px,transparent_0,transparent_50%)] bg-size-[10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] sm:block dark:[--pattern-fg:#e1e1e1]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
new Date() >
|
||||||
|
add(servizio.decorrenza, {
|
||||||
|
days: 60,
|
||||||
|
}) &&
|
||||||
|
!servizio.isOkSaldo
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="font-medium text-lg">{t.servizio.scaduto}</p>
|
||||||
|
<p>{t.servizio.scaduto_desc}</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const annuncioConfermato = servizio.annunci.filter(
|
||||||
|
(a) => a.accettato_conferma_at !== null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (servizio.isOkSaldo && annuncioConfermato.length > 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{annuncioConfermato.map((data) => {
|
||||||
|
return (
|
||||||
|
<ServizioAnnuncioProvider
|
||||||
|
data={data}
|
||||||
|
key={data.id}
|
||||||
|
servizioId={servizio.servizio_id}
|
||||||
|
userId={userId}
|
||||||
|
>
|
||||||
|
<AnnuncioCard className="max-w-full" />
|
||||||
|
</ServizioAnnuncioProvider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{annuncioConfermato.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PackageCheck className="size-6 text-primary" />
|
||||||
|
<span className="font-medium">{t.servizio.saldi_in_attesa}</span>
|
||||||
|
</div>
|
||||||
|
{servizio.annunci
|
||||||
|
.filter((a) => a.accettato_conferma_at !== null)
|
||||||
|
.map((a) => (
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 rounded-md bg-sky-400 p-2"
|
||||||
|
key={a.annunci_id}
|
||||||
|
>
|
||||||
|
<h3>
|
||||||
|
{a.codice} -{" "}
|
||||||
|
{a.accettato_conferma_at &&
|
||||||
|
new Date(a.accettato_conferma_at).toLocaleDateString("it", {
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
<SaldoButton
|
||||||
|
annuncioId={a.annunci_id}
|
||||||
|
className="border-none"
|
||||||
|
variant={"outline"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-flow-row grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
{servizio.annunci.map((data) => {
|
||||||
|
return (
|
||||||
|
<ServizioAnnuncioProvider
|
||||||
|
data={data}
|
||||||
|
key={data.id}
|
||||||
|
servizioId={servizio.servizio_id}
|
||||||
|
userId={userId}
|
||||||
|
>
|
||||||
|
<AnnuncioCard />
|
||||||
|
</ServizioAnnuncioProvider>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{servizio.annunci.length % 2 === 1 && (
|
||||||
|
<div className="hidden size-full rounded-md border border-(--pattern-fg) bg-[repeating-linear-gradient(45deg,var(--pattern-fg)_0,var(--pattern-fg)_1px,transparent_0,transparent_50%)] bg-size-[10px_10px] bg-fixed opacity-10 [--pattern-fg:#696969] sm:block dark:[--pattern-fg:#e1e1e1]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<AnnunciCompatibili />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
import { add } from "date-fns";
|
import { add } from "date-fns";
|
||||||
import {
|
import {
|
||||||
Bug,
|
Bug,
|
||||||
CalendarClock,
|
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
|
||||||
ClockAlert,
|
ClockAlert,
|
||||||
Cog,
|
Cog,
|
||||||
|
Euro,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Info,
|
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Trash2,
|
Trash2,
|
||||||
UserCircle,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
@ -60,6 +57,7 @@ import {
|
||||||
} from "~/forms/FormEditServizioAdmin";
|
} from "~/forms/FormEditServizioAdmin";
|
||||||
import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio";
|
import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio";
|
||||||
import { cn, formatCurrency } from "~/lib/utils";
|
import { cn, formatCurrency } from "~/lib/utils";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
import { useServizio } from "~/providers/ServizioProvider";
|
import { useServizio } from "~/providers/ServizioProvider";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||||
|
|
@ -67,16 +65,12 @@ import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { Confirm } from "../confirm";
|
import { Confirm } from "../confirm";
|
||||||
import { Progress } from "../ui/progress";
|
|
||||||
|
|
||||||
export const ServizioActions = () => {
|
export const AdminActions = () => {
|
||||||
const { servizio, isAdmin, userId } = useServizio();
|
const { servizio, isAdmin, userId } = useServizio();
|
||||||
|
if (!isAdmin) return null;
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col flex-wrap justify-between gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
{isAdmin && (
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="font-semibold">Azioni Admin:</span>
|
|
||||||
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
|
<AddAnnuncio servizioId={servizio.servizio_id} userId={userId} />
|
||||||
<EditServizioAdmin />
|
<EditServizioAdmin />
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -88,100 +82,18 @@ export const ServizioActions = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<ServizioInfos />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServizioInfos = () => {
|
export const ServizioBasicActions = ({ className }: { className?: string }) => {
|
||||||
const { servizio } = useServizio();
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const openContattiAnnunci = servizio.annunci.filter(
|
|
||||||
(a) => a.open_contatti_at !== null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!servizio.decorrenza) return null;
|
|
||||||
const dec_txt = new Date(servizio.decorrenza).toLocaleDateString("it-IT");
|
|
||||||
const end_txt = new Date(
|
|
||||||
add(servizio.decorrenza, { days: 60 }),
|
|
||||||
).toLocaleDateString("it-IT");
|
|
||||||
return (
|
|
||||||
<Collapsible className="space-y-2" onOpenChange={setOpen} open={open}>
|
|
||||||
<CollapsibleTrigger asChild>
|
|
||||||
{open ? (
|
|
||||||
<Button
|
|
||||||
className="w-full justify-between data-[state=open]:bg-accent dark:data-[state=open]:bg-input/50"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<Info />
|
|
||||||
<span>Informazioni servizio</span>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<ChevronUp />
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button className="w-full justify-between" variant="outline">
|
|
||||||
<div className="flex items-center gap-6">
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<UserCircle className="size-4 text-primary" />
|
|
||||||
<span>{openContattiAnnunci.length} / 10</span>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<CalendarClock className="size-4 text-primary" />
|
|
||||||
<span>{end_txt}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span>Dettagli</span>
|
|
||||||
<ChevronDown />
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="mb-4 flex w-full flex-col gap-4 rounded-md border bg-card p-2 sm:items-start sm:gap-6">
|
|
||||||
<ServizioBasicActions />
|
|
||||||
<div className="flex w-full flex-col gap-4 sm:flex-row sm:items-start sm:gap-6">
|
|
||||||
<div className="flex flex-2 flex-col gap-2 sm:w-1/2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserCircle className="text-primary" />
|
|
||||||
<span className="font-medium">Contatti sbloccati</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm">{openContattiAnnunci.length} / 10</span>
|
|
||||||
</div>
|
|
||||||
<Progress className="h-2" value={openContattiAnnunci.length * 10} />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col flex-wrap gap-2 sm:w-1/2 sm:flex-row sm:items-center">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CalendarClock className="text-primary" />
|
|
||||||
<span className="font-medium">Durata servizio:</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{dec_txt} - {end_txt}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ServizioPacksInfos />
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ServizioBasicActions = ({ className }: { className?: string }) => {
|
|
||||||
const { servizio, isAdmin } = useServizio();
|
const { servizio, isAdmin } = useServizio();
|
||||||
|
|
||||||
const canUserEdit =
|
const canUserEdit = servizio.isOkAcconto && servizio.decorrenza !== null;
|
||||||
servizio.isOkAcconto &&
|
/*&&
|
||||||
servizio.decorrenza !== null &&
|
|
||||||
new Date() <= add(new Date(servizio.decorrenza), { days: 60 }) &&
|
new Date() <= add(new Date(servizio.decorrenza), { days: 60 }) &&
|
||||||
!servizio.annunci.some((a) => a.user_confirmed_at !== null) &&
|
!servizio.annunci.some((a) => a.user_confirmed_at !== null) &&
|
||||||
!servizio.isInterrotto &&
|
!servizio.isInterrotto &&
|
||||||
!servizio.annunci.some((a) => a.user_confirmed_at !== null);
|
!servizio.annunci.some((a) => a.user_confirmed_at !== null);*/
|
||||||
|
|
||||||
if (isAdmin || canUserEdit) {
|
if (isAdmin || canUserEdit) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -196,23 +108,37 @@ const ServizioBasicActions = ({ className }: { className?: string }) => {
|
||||||
|
|
||||||
export const ServizioPacksInfos = () => {
|
export const ServizioPacksInfos = () => {
|
||||||
const { servizio } = useServizio();
|
const { servizio } = useServizio();
|
||||||
|
const { t } = useTranslation();
|
||||||
const { data: packs } = api.servizio.getPacksSolution.useQuery({
|
const { data: packs } = api.servizio.getPacksSolution.useQuery({
|
||||||
servizioId: servizio.servizio_id,
|
servizioId: servizio.servizio_id,
|
||||||
});
|
});
|
||||||
const title = "Prezzi:";
|
|
||||||
const sub = "(basati sui parametri di ricerca)";
|
|
||||||
if (!packs) return null;
|
if (!packs) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full space-y-2">
|
<Collapsible className="flex flex-col gap-2">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<CollapsibleTrigger asChild>
|
||||||
<h3 className="font-semibold text-lg">{title}</h3>
|
<Button
|
||||||
<span className="text-muted-foreground text-sm">{sub}</span>
|
className="h-fit w-fit flex-wrap items-center py-1"
|
||||||
</div>
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Euro className="size-5" />
|
||||||
|
|
||||||
|
<h3 className="font-semibold text-lg">{t.servizio.prezzi.title}</h3>
|
||||||
|
|
||||||
|
<ChevronDown className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
<CollapsibleContent className="flex flex-col gap-2">
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{t.servizio.prezzi.sub}
|
||||||
|
</span>
|
||||||
<div className="flex w-full flex-col gap-4 md:flex-row">
|
<div className="flex w-full flex-col gap-4 md:flex-row">
|
||||||
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
||||||
{packs.acconto.success ? (
|
{packs.acconto.success ? (
|
||||||
<>
|
<>
|
||||||
<span className="font-medium">{packs.acconto.pack.nome_it}</span>
|
<span className="font-medium">
|
||||||
|
{packs.acconto.pack.nome_it}
|
||||||
|
</span>
|
||||||
<span className="text-muted-foreground text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
{packs.acconto.pack.desc_it}
|
{packs.acconto.pack.desc_it}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -249,10 +175,11 @@ export const ServizioPacksInfos = () => {
|
||||||
{servizio.tipologia === TipologiaPosizioneEnum.Stabile && (
|
{servizio.tipologia === TipologiaPosizioneEnum.Stabile && (
|
||||||
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
<div className="flex flex-1 flex-col gap-2 rounded-md bg-secondary/50 p-2">
|
||||||
<>
|
<>
|
||||||
<span className="font-medium">Consulenza</span>
|
<span className="font-medium">
|
||||||
|
{t.servizio.prezzi.consulenza}
|
||||||
|
</span>
|
||||||
<span className="text-muted-foreground text-sm">
|
<span className="text-muted-foreground text-sm">
|
||||||
Il prezzo della consulenza varia in base alla tipologia
|
{t.servizio.prezzi.consulenza_desc}
|
||||||
contrattuale.{" "}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -260,7 +187,7 @@ export const ServizioPacksInfos = () => {
|
||||||
href={"/prezzi#contratti"}
|
href={"/prezzi#contratti"}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<span>Vedi prezzi contratti</span>
|
<span>{t.servizio.prezzi.gotoprezzi}</span>
|
||||||
<ExternalLink className="size-4 stroke-foreground" />
|
<ExternalLink className="size-4 stroke-foreground" />
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -268,7 +195,8 @@ export const ServizioPacksInfos = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -390,7 +318,7 @@ const AddAnnuncio = ({
|
||||||
|
|
||||||
const InterruzioneServizio = () => {
|
const InterruzioneServizio = () => {
|
||||||
const { servizio, userId, isAdmin } = useServizio();
|
const { servizio, userId, isAdmin } = useServizio();
|
||||||
|
const { t } = useTranslation();
|
||||||
const beforeInterruzioneLimit =
|
const beforeInterruzioneLimit =
|
||||||
servizio.decorrenza != null &&
|
servizio.decorrenza != null &&
|
||||||
new Date() <=
|
new Date() <=
|
||||||
|
|
@ -423,28 +351,23 @@ const InterruzioneServizio = () => {
|
||||||
servizio.annunci.some((a) => a.user_confirmed_at !== null))
|
servizio.annunci.some((a) => a.user_confirmed_at !== null))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ClockAlert /> <span>Interruzione</span>
|
<ClockAlert /> <span>{t.servizio.interruzione.btn}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>{t.servizio.interruzione.title}</AlertDialogTitle>
|
||||||
Sei sicuro di voler richiedere l'interruzione?
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Questa azione è irreversibile e comporta la disattivazione dei
|
{t.servizio.interruzione.description}
|
||||||
servizi attivati. Confermando l'azione recedi dal servizio
|
|
||||||
secondo le condizioni contrattuali. Verrai notificato via email
|
|
||||||
della conferma dell'avvenuta interruzione.
|
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Annulla</AlertDialogCancel>
|
<AlertDialogCancel>{t.annulla}</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
aria-label="Confirm Interruzione"
|
aria-label="Confirm Interruzione"
|
||||||
onClick={() => mutate({ servizioId: servizio.servizio_id })}
|
onClick={() => mutate({ servizioId: servizio.servizio_id })}
|
||||||
>
|
>
|
||||||
Continua
|
{t.conferma}
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|
@ -454,6 +377,7 @@ const InterruzioneServizio = () => {
|
||||||
|
|
||||||
const EditParametri = () => {
|
const EditParametri = () => {
|
||||||
const { servizioId, userId, isAdmin, servizio } = useServizio();
|
const { servizioId, userId, isAdmin, servizio } = useServizio();
|
||||||
|
const { t } = useTranslation();
|
||||||
const [openEditParametri, setOpenEditParametri] = useState(false);
|
const [openEditParametri, setOpenEditParametri] = 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();
|
||||||
|
|
@ -497,14 +421,14 @@ const EditParametri = () => {
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
>
|
>
|
||||||
<SlidersHorizontal />
|
<SlidersHorizontal />
|
||||||
Parametri
|
{t.servizio.parametri.btn}
|
||||||
</Button>
|
</Button>
|
||||||
</CredenzaTrigger>
|
</CredenzaTrigger>
|
||||||
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
|
<CredenzaContent className="max-h-[90vh] w-full sm:max-w-5xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>Parametri di ricerca</CredenzaTitle>
|
<CredenzaTitle>{t.servizio.parametri.title}</CredenzaTitle>
|
||||||
<CredenzaDescription className="sr-only">
|
<CredenzaDescription className="sr-only">
|
||||||
Parametri
|
{t.servizio.parametri.title}
|
||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
|
<CredenzaBody className="max-h-[80vh] w-full max-w-3xl overflow-y-auto pb-5 sm:max-w-5xl">
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import { LoadingPage } from "../loading";
|
||||||
|
|
||||||
export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
|
export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
|
||||||
const { data, isLoading } = api.intrests.getUserInterestsAnnunci.useQuery({
|
const { data, isLoading } = api.intrests.getUserInterestsAnnunci.useQuery({
|
||||||
|
|
@ -23,7 +24,7 @@ export const AnnunciRichiesti = ({ userId }: { userId: UsersId }) => {
|
||||||
toast.success("Annuncio rimosso con successo");
|
toast.success("Annuncio rimosso con successo");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
if (!data || data.length === 0) return null;
|
if (!data || data.length === 0) return null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
||||||
import { add } from "date-fns";
|
|
||||||
import { Building, ChevronDown, ChevronUp } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { LoadingPage } from "~/components/loading";
|
|
||||||
import { ServizioContent } from "~/components/servizio/main";
|
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from "~/components/ui/collapsible";
|
|
||||||
import { cn } from "~/lib/utils";
|
|
||||||
import { ServizioProvider } from "~/providers/ServizioProvider";
|
|
||||||
import type { UsersId } from "~/schemas/public/Users";
|
|
||||||
import type { ServizioData } from "~/server/controllers/servizio.controller";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
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-8">
|
|
||||||
{data.map((servizio) => {
|
|
||||||
return (
|
|
||||||
<Row
|
|
||||||
defaultOpen={
|
|
||||||
!servizio.isInterrotto ||
|
|
||||||
new Date() < add(servizio.created_at, { days: 60 })
|
|
||||||
}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
key={servizio.servizio_id}
|
|
||||||
servizio={servizio}
|
|
||||||
userId={userId}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Row = ({
|
|
||||||
servizio,
|
|
||||||
userId,
|
|
||||||
isAdmin,
|
|
||||||
defaultOpen = false,
|
|
||||||
}: {
|
|
||||||
servizio: ServizioData;
|
|
||||||
userId: UsersId;
|
|
||||||
isAdmin: boolean;
|
|
||||||
defaultOpen: boolean;
|
|
||||||
}) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
||||||
return (
|
|
||||||
<ServizioProvider
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
servizio={servizio}
|
|
||||||
servizioId={servizio.servizio_id}
|
|
||||||
userId={userId}
|
|
||||||
>
|
|
||||||
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex items-center rounded-md border border-primary bg-secondary py-2 shadow-sm hover:shadow-md",
|
|
||||||
isOpen && "rounded-b-none",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CollapsibleTrigger className="flex w-full cursor-pointer items-center justify-between gap-4 px-3 sm:px-4">
|
|
||||||
<div className="flex items-center gap-2 font-semibold">
|
|
||||||
<Building className="size-6 stroke-foreground" />
|
|
||||||
|
|
||||||
<span className="sm:text-lg">Affitto {servizio.tipologia}</span>
|
|
||||||
<span className="text-muted-foreground text-xs sm:text-sm">
|
|
||||||
{`${servizio.created_at.toLocaleDateString("it")}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="float-end flex items-center justify-start pl-2">
|
|
||||||
{isOpen ? <ChevronUp /> : <ChevronDown />}
|
|
||||||
</div>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CollapsibleContent className="mt-0 overflow-hidden transition-all duration-300 ease-in-out data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
|
|
||||||
<ServizioContent />
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
</ServizioProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1299,6 +1299,47 @@ The Stable Rent service, ideal for those with demonstrable work references and f
|
||||||
salva: "Save",
|
salva: "Save",
|
||||||
seleziona_placeholder: "Select . . .",
|
seleziona_placeholder: "Select . . .",
|
||||||
servizio: {
|
servizio: {
|
||||||
|
status: {
|
||||||
|
attivo: "Active",
|
||||||
|
in_attesa: "Pending activation",
|
||||||
|
interrotto: "Interrupted",
|
||||||
|
scaduto: "Expired",
|
||||||
|
completato: "Completed",
|
||||||
|
},
|
||||||
|
dettaglio_servizio: "Service Details",
|
||||||
|
nessun_servizio: "No service currently available",
|
||||||
|
servizio_titolo: "Rental Service",
|
||||||
|
servizio_creato_il: "Created on:",
|
||||||
|
vai_al_servizio: "Go to Service",
|
||||||
|
durata_servizio: "Service Duration:",
|
||||||
|
servizio_interrotto: "Service Interrupted",
|
||||||
|
servizio_interrotto_desc:
|
||||||
|
"This service cannot be reused as it has been interrupted.",
|
||||||
|
non_attivo: "Service not yet activated",
|
||||||
|
attiva_cta: "Activate the Service Now!",
|
||||||
|
per_te_1: "Already selected",
|
||||||
|
per_te_2: "ads that might interest you.",
|
||||||
|
scaduto: "Service Expired",
|
||||||
|
scaduto_desc:
|
||||||
|
"This service cannot be reused as the ad selection period has expired (60 days from the start date) and no property has been confirmed.",
|
||||||
|
saldi_in_attesa: "Pending Balances:",
|
||||||
|
confermato_il: "Confirmed on:",
|
||||||
|
prezzi: {
|
||||||
|
title: "Service Prices:",
|
||||||
|
sub: "(based on search parameters)",
|
||||||
|
consulenza: "Consultation",
|
||||||
|
consulenza_desc: "The consultation price varies based on the contract type.",
|
||||||
|
gotoprezzi: "See contract prices",
|
||||||
|
},
|
||||||
|
parametri:{
|
||||||
|
title: "Search Parameters",
|
||||||
|
btn: "Parameters",
|
||||||
|
},
|
||||||
|
interruzione: {
|
||||||
|
btn: "Interruption",
|
||||||
|
title: "Are you sure you want to request the interruption?",
|
||||||
|
description: "This action is irreversible and will deactivate the activated services. By confirming the action, you withdraw from the service according to the contractual conditions. You will be notified via email of the confirmation of the interruption.",
|
||||||
|
},
|
||||||
conferma: {
|
conferma: {
|
||||||
accetto: "I accept the terms and conditions.",
|
accetto: "I accept the terms and conditions.",
|
||||||
causale: "Payment reference:",
|
causale: "Payment reference:",
|
||||||
|
|
|
||||||
|
|
@ -855,6 +855,7 @@ export const it: LangDict = {
|
||||||
newPw: "Nuova password",
|
newPw: "Nuova password",
|
||||||
pwAggiornata: "Password aggiornata",
|
pwAggiornata: "Password aggiornata",
|
||||||
},
|
},
|
||||||
|
|
||||||
parametri: {
|
parametri: {
|
||||||
altriparams_desc: "Seleziona gli altri parametri",
|
altriparams_desc: "Seleziona gli altri parametri",
|
||||||
altriparams_label: "Altri parametri",
|
altriparams_label: "Altri parametri",
|
||||||
|
|
@ -1297,6 +1298,47 @@ export const it: LangDict = {
|
||||||
salva: "Salva",
|
salva: "Salva",
|
||||||
seleziona_placeholder: "Seleziona...",
|
seleziona_placeholder: "Seleziona...",
|
||||||
servizio: {
|
servizio: {
|
||||||
|
status: {
|
||||||
|
attivo: "Attivo",
|
||||||
|
interrotto: "Interrotto",
|
||||||
|
in_attesa: "In Attesa di Attivazione",
|
||||||
|
scaduto: "Scaduto",
|
||||||
|
completato: "Completato",
|
||||||
|
},
|
||||||
|
dettaglio_servizio: "Dettaglio Servizio",
|
||||||
|
nessun_servizio: "Nessun servizio attualmente inserito",
|
||||||
|
servizio_titolo: "Servizio Affitto",
|
||||||
|
servizio_creato_il: "Creato il:",
|
||||||
|
vai_al_servizio: "Vai al Servizio",
|
||||||
|
durata_servizio: "Durata servizio:",
|
||||||
|
servizio_interrotto: "Servizio interrotto",
|
||||||
|
servizio_interrotto_desc:
|
||||||
|
"Non è possibile riutilizzare questo servizio, poichè è stato interrotto.",
|
||||||
|
non_attivo: "Servizio non ancora attivato",
|
||||||
|
attiva_cta: "Attiva il Servizio Ora!",
|
||||||
|
per_te_1: "Sono già stati selezionati",
|
||||||
|
per_te_2: "annunci che potrebbero interessarti.",
|
||||||
|
scaduto: "Servizio scaduto",
|
||||||
|
scaduto_desc:
|
||||||
|
"Non è possibile riutilizzare questo servizio, poichè il periodo di scelta degli annunci è scaduto (60 giorni dalla data di decorrenza) e non è stato confermato nessun immobile.",
|
||||||
|
saldi_in_attesa: "Saldi in attesa:",
|
||||||
|
confermato_il: "confermato il:",
|
||||||
|
prezzi: {
|
||||||
|
title: "Prezzi del servizio:",
|
||||||
|
sub: "(basati sui parametri di ricerca)",
|
||||||
|
consulenza: "Consulenza",
|
||||||
|
consulenza_desc: "Il prezzo della consulenza varia in base alla tipologia contrattuale.",
|
||||||
|
gotoprezzi: "Vedi prezzi contratti",
|
||||||
|
},
|
||||||
|
parametri:{
|
||||||
|
title: "Parametri di ricerca",
|
||||||
|
btn: "Parametri",
|
||||||
|
},
|
||||||
|
interruzione: {
|
||||||
|
btn: "Interruzione",
|
||||||
|
title: "Sei sicuro di voler richiedere l'interruzione?",
|
||||||
|
description: "Questa azione è irreversibile e comporta la disattivazione dei servizi attivati. Confermando l'azione recedi dal servizio secondo le condizioni contrattuali. Verrai notificato via email della conferma dell'avvenuta interruzione.",
|
||||||
|
},
|
||||||
conferma: {
|
conferma: {
|
||||||
accetto: "Accetto i termini e condizioni.",
|
accetto: "Accetto i termini e condizioni.",
|
||||||
causale: "Causale:",
|
causale: "Causale:",
|
||||||
|
|
|
||||||
|
|
@ -769,6 +769,45 @@ export type LangDict = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
servizio: {
|
servizio: {
|
||||||
|
status: {
|
||||||
|
attivo: string;
|
||||||
|
interrotto: string;
|
||||||
|
in_attesa: string;
|
||||||
|
scaduto: string;
|
||||||
|
completato: string;
|
||||||
|
};
|
||||||
|
dettaglio_servizio: string;
|
||||||
|
nessun_servizio: string;
|
||||||
|
servizio_titolo: string;
|
||||||
|
servizio_creato_il: string;
|
||||||
|
vai_al_servizio: string;
|
||||||
|
durata_servizio: string;
|
||||||
|
servizio_interrotto: string;
|
||||||
|
servizio_interrotto_desc: string;
|
||||||
|
non_attivo: string;
|
||||||
|
attiva_cta: string;
|
||||||
|
per_te_1: string;
|
||||||
|
per_te_2: string;
|
||||||
|
scaduto: string;
|
||||||
|
scaduto_desc: string;
|
||||||
|
saldi_in_attesa: string;
|
||||||
|
confermato_il: string;
|
||||||
|
prezzi: {
|
||||||
|
title: string;
|
||||||
|
sub: string;
|
||||||
|
consulenza: string;
|
||||||
|
consulenza_desc: string;
|
||||||
|
gotoprezzi: string;
|
||||||
|
};
|
||||||
|
parametri: {
|
||||||
|
title: string;
|
||||||
|
btn: string;
|
||||||
|
};
|
||||||
|
interruzione: {
|
||||||
|
btn: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
conferma: {
|
conferma: {
|
||||||
titolo: string;
|
titolo: string;
|
||||||
descrizione: string;
|
descrizione: string;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import { AreaRiservataLayoutUserView } from "~/components/Layout";
|
import { AreaRiservataLayoutUserView } from "~/components/Layout";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
import { ServiziHeader } from "~/components/servizio/servizi_header";
|
import { ServiziHeader } from "~/components/servizio/servizi_header";
|
||||||
|
import { ServizioList } from "~/components/servizio/servizio";
|
||||||
import { AnnunciRichiesti } from "~/components/servizio/servizio_annunci_accordions";
|
import { AnnunciRichiesti } from "~/components/servizio/servizio_annunci_accordions";
|
||||||
import { TabRicerca } from "~/components/tables/ricerca-table";
|
|
||||||
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 { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
import { zUserId } from "~/server/utils/zod_types";
|
import { zUserId } from "~/server/utils/zod_types";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
type RicercaUserProps = {
|
type RicercaUserProps = {
|
||||||
userId: UsersId;
|
userId: UsersId;
|
||||||
|
|
@ -15,11 +17,17 @@ type RicercaUserProps = {
|
||||||
const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
const RicercaUser: NextPageWithLayout<RicercaUserProps> = ({
|
||||||
userId,
|
userId,
|
||||||
}: RicercaUserProps) => {
|
}: RicercaUserProps) => {
|
||||||
|
const { data, isLoading } = api.servizio.getAllServizioAnnunci.useQuery({
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
if (isLoading) return <LoadingPage />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<ServiziHeader userId={userId} />
|
<ServiziHeader userId={userId} />
|
||||||
<AnnunciRichiesti userId={userId} />
|
<AnnunciRichiesti userId={userId} />
|
||||||
<TabRicerca isAdmin userId={userId} />
|
|
||||||
|
<ServizioList isAdmin servizi={data || []} userId={userId} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import type { GetServerSideProps } from "next";
|
||||||
|
import { AreaRiservataLayoutUserView } from "~/components/Layout";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import { Servizio } from "~/components/servizio/servizio";
|
||||||
|
import { Status500 } from "~/components/status-page";
|
||||||
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||||
|
import type { UsersId } from "~/schemas/public/Users";
|
||||||
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
|
import { zServizioId, zUserId } from "~/server/utils/zod_types";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
type ServizioUserProps = {
|
||||||
|
servizioId: ServizioServizioId;
|
||||||
|
userId: UsersId;
|
||||||
|
};
|
||||||
|
const ServizioUser: NextPageWithLayout<ServizioUserProps> = ({
|
||||||
|
servizioId,
|
||||||
|
userId,
|
||||||
|
}: ServizioUserProps) => {
|
||||||
|
const { data, isLoading } = api.servizio.getServizioData.useQuery({
|
||||||
|
servizioId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingPage />;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <Status500 />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-8xl grow space-y-4">
|
||||||
|
<Servizio
|
||||||
|
isAdmin={true}
|
||||||
|
key={data.servizio_id}
|
||||||
|
servizio={data}
|
||||||
|
userId={userId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ServizioUser;
|
||||||
|
|
||||||
|
export const getServerSideProps = (async (context) => {
|
||||||
|
const user_id = context.params?.slug?.[0];
|
||||||
|
const servizio_id = context.params?.slug?.[1];
|
||||||
|
|
||||||
|
if (typeof servizio_id !== "string" || typeof user_id !== "string") {
|
||||||
|
console.error("Error: servizioId or userId is not a string");
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const parsedUserId = zUserId.safeParse(user_id);
|
||||||
|
if (!parsedUserId.success) {
|
||||||
|
console.error("Error parsing userId", parsedUserId.error);
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedServizioId = zServizioId.safeParse(servizio_id);
|
||||||
|
if (!parsedServizioId.success) {
|
||||||
|
console.error("Error parsing servizioId", parsedServizioId.error);
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const servizioId = parsedServizioId.data;
|
||||||
|
const userId = parsedUserId.data;
|
||||||
|
|
||||||
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
|
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||||
|
if (helper) {
|
||||||
|
await helper.trpc.servizio.getServizio.prefetch({
|
||||||
|
servizioId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
servizioId,
|
||||||
|
trpcState: helper.trpc.dehydrate(),
|
||||||
|
userId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: `/area-riservata/admin/utenti`,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies GetServerSideProps<ServizioUserProps>;
|
||||||
|
|
||||||
|
ServizioUser.getLayout = function getLayout(page) {
|
||||||
|
return <AreaRiservataLayoutUserView>{page}</AreaRiservataLayoutUserView>;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import type { GetServerSideProps } from "next";
|
||||||
|
import { AreaRiservataLayout } from "~/components/Layout";
|
||||||
|
import { LoadingPage } from "~/components/loading";
|
||||||
|
import { Servizio } from "~/components/servizio/servizio";
|
||||||
|
import { Status500 } from "~/components/status-page";
|
||||||
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import { useEnforcedSession } from "~/providers/SessionProvider";
|
||||||
|
import type { ServizioServizioId } from "~/schemas/public/Servizio";
|
||||||
|
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper";
|
||||||
|
import { zServizioId } from "~/server/utils/zod_types";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
type ServizioPageProps = {
|
||||||
|
servizioId: ServizioServizioId;
|
||||||
|
};
|
||||||
|
const ServizioPage: NextPageWithLayout<ServizioPageProps> = ({
|
||||||
|
servizioId,
|
||||||
|
}: ServizioPageProps) => {
|
||||||
|
const { user } = useEnforcedSession();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { data, isLoading } = api.servizio.getServizioData.useQuery({
|
||||||
|
servizioId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingPage />;
|
||||||
|
}
|
||||||
|
if (!data) {
|
||||||
|
return <Status500 />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-8xl grow space-y-4 p-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h3 className="font-bold text-2xl">{t.servizio.dettaglio_servizio}</h3>
|
||||||
|
</div>
|
||||||
|
<Servizio
|
||||||
|
isAdmin={false}
|
||||||
|
key={data.servizio_id}
|
||||||
|
servizio={data}
|
||||||
|
userId={user.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ServizioPage;
|
||||||
|
|
||||||
|
export const getServerSideProps = (async (context) => {
|
||||||
|
const servizio_id = context.params?.servizioId;
|
||||||
|
|
||||||
|
if (typeof servizio_id !== "string") {
|
||||||
|
console.error("Error: servizioId is not a string");
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedServizioId = zServizioId.safeParse(servizio_id);
|
||||||
|
if (!parsedServizioId.success) {
|
||||||
|
console.error("Error parsing servizioId", parsedServizioId.error);
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/500",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const servizioId = parsedServizioId.data;
|
||||||
|
|
||||||
|
const access_token = context.req.cookies.access_token;
|
||||||
|
|
||||||
|
const helper = await TrpcAuthedFetchingIstance({ access_token });
|
||||||
|
if (helper) {
|
||||||
|
await helper.trpc.servizio.getServizio.prefetch({
|
||||||
|
servizioId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
servizioId,
|
||||||
|
trpcState: helper.trpc.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: `/area-riservata/admin/utenti`,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies GetServerSideProps<ServizioPageProps>;
|
||||||
|
|
||||||
|
ServizioPage.getLayout = function getLayout(page) {
|
||||||
|
return <AreaRiservataLayout>{page}</AreaRiservataLayout>;
|
||||||
|
};
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
getServiziByUserId,
|
getServiziByUserId,
|
||||||
getServizioAnnuncio,
|
getServizioAnnuncio,
|
||||||
getServizioById,
|
getServizioById,
|
||||||
|
getServizioDataById,
|
||||||
getServizioOrdini,
|
getServizioOrdini,
|
||||||
getServizioPagamentoData,
|
getServizioPagamentoData,
|
||||||
getUserPagamenti,
|
getUserPagamenti,
|
||||||
|
|
@ -130,6 +131,15 @@ export const servizioRouter = createTRPCRouter({
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getAllServizioAnnunci(input.userId);
|
return await getAllServizioAnnunci(input.userId);
|
||||||
}),
|
}),
|
||||||
|
getServizioData: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
servizioId: zServizioId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getServizioDataById(input.servizioId);
|
||||||
|
}),
|
||||||
getAnnunciAvailableToAdd: adminProcedure
|
getAnnunciAvailableToAdd: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import type { PurchaseData } from "~/components/acquisto_receipt";
|
import type { PurchaseData } from "~/components/acquisto_receipt";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
import type { AnnunciId } from "~/schemas/public/Annunci";
|
import type { Annunci, AnnunciId } from "~/schemas/public/Annunci";
|
||||||
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
|
||||||
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
|
||||||
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
|
||||||
|
|
@ -20,13 +20,18 @@ import type {
|
||||||
} from "~/schemas/public/Prezziario";
|
} from "~/schemas/public/Prezziario";
|
||||||
import type {
|
import type {
|
||||||
NewServizio,
|
NewServizio,
|
||||||
|
Servizio,
|
||||||
ServizioServizioId,
|
ServizioServizioId,
|
||||||
ServizioUpdate,
|
ServizioUpdate,
|
||||||
} from "~/schemas/public/Servizio";
|
} from "~/schemas/public/Servizio";
|
||||||
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
|
import type {
|
||||||
|
ServizioAnnunci,
|
||||||
|
ServizioAnnunciUpdate,
|
||||||
|
} from "~/schemas/public/ServizioAnnunci";
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
import type { Users, UsersId } from "~/schemas/public/Users";
|
import type { Users, UsersId } from "~/schemas/public/Users";
|
||||||
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
|
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
|
||||||
|
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
|
||||||
import type { Context } from "~/server/api/trpc";
|
import type { Context } from "~/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
addEventToQueue,
|
addEventToQueue,
|
||||||
|
|
@ -41,7 +46,11 @@ import {
|
||||||
import { db } from "~/server/db";
|
import { db } from "~/server/db";
|
||||||
import { NewMail, type NewMailProps } from "~/server/services/mailer";
|
import { NewMail, type NewMailProps } from "~/server/services/mailer";
|
||||||
import { getUser } from "~/server/services/user.service";
|
import { getUser } from "~/server/services/user.service";
|
||||||
import { withImages, withVideos } from "~/utils/kysely-helper";
|
import {
|
||||||
|
parseNullableDateString,
|
||||||
|
withImages,
|
||||||
|
withVideos,
|
||||||
|
} from "~/utils/kysely-helper";
|
||||||
import { GetUserInterests } from "../services/interests.service";
|
import { GetUserInterests } from "../services/interests.service";
|
||||||
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
import { getPrezziarioByIdHandler } from "../services/prezziario.service";
|
||||||
import type { AnnuncioRicerca } from "./annunci.controller";
|
import type { AnnuncioRicerca } from "./annunci.controller";
|
||||||
|
|
@ -703,12 +712,74 @@ export const setupSaldoConsulenzaServizio = async ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServizioData = Awaited<
|
type DocRef = {
|
||||||
ReturnType<typeof getAllServizioAnnunci>
|
storageId: string;
|
||||||
>[number];
|
ref: UsersStorageUserStorageId | null;
|
||||||
export const getAllServizioAnnunci = async (userId: UsersId) => {
|
};
|
||||||
|
export type ServizioData = Pick<
|
||||||
|
Servizio,
|
||||||
|
| "servizio_id"
|
||||||
|
| "created_at"
|
||||||
|
| "decorrenza"
|
||||||
|
| "tipologia"
|
||||||
|
| "budget"
|
||||||
|
| "permanenza"
|
||||||
|
| "isInterrotto"
|
||||||
|
| "isOkAcconto"
|
||||||
|
| "isOkSaldo"
|
||||||
|
| "isOkConsulenza"
|
||||||
|
| "skipPayment"
|
||||||
|
| "skipControlloDoc"
|
||||||
|
| "skipDocMotivazione"
|
||||||
|
| "n_adulti"
|
||||||
|
| "n_minori"
|
||||||
|
| "entromese"
|
||||||
|
> & {
|
||||||
|
doc_personale_fronte: DocRef | null;
|
||||||
|
doc_personale_retro: DocRef | null;
|
||||||
|
doc_motivazione: DocRef | null;
|
||||||
|
annunci: (ServizioAnnunci &
|
||||||
|
Pick<
|
||||||
|
Annunci,
|
||||||
|
| "id"
|
||||||
|
| "codice"
|
||||||
|
| "comune"
|
||||||
|
| "provincia"
|
||||||
|
| "prezzo"
|
||||||
|
| "consegna"
|
||||||
|
| "numero_camere"
|
||||||
|
| "mq"
|
||||||
|
| "tipo"
|
||||||
|
| "titolo_it"
|
||||||
|
| "titolo_en"
|
||||||
|
| "desc_en"
|
||||||
|
| "desc_it"
|
||||||
|
| "modificato_il"
|
||||||
|
| "stato"
|
||||||
|
| "external_videos"
|
||||||
|
| "media_updated_at"
|
||||||
|
| "homepage"
|
||||||
|
| "web"
|
||||||
|
| "disponibile_da"
|
||||||
|
| "persone"
|
||||||
|
| "permanenza"
|
||||||
|
> & {
|
||||||
|
images: {
|
||||||
|
img: string;
|
||||||
|
thumb: string;
|
||||||
|
}[];
|
||||||
|
videos: {
|
||||||
|
thumb: string;
|
||||||
|
video: string;
|
||||||
|
}[];
|
||||||
|
})[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllServizioAnnunci = async (
|
||||||
|
userId: UsersId,
|
||||||
|
): Promise<ServizioData[]> => {
|
||||||
try {
|
try {
|
||||||
return await db
|
const data = await db
|
||||||
.selectFrom("servizio")
|
.selectFrom("servizio")
|
||||||
.where("user_id", "=", userId)
|
.where("user_id", "=", userId)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
|
|
@ -820,9 +891,36 @@ export const getAllServizioAnnunci = async (userId: UsersId) => {
|
||||||
),
|
),
|
||||||
).as("annunci"),
|
).as("annunci"),
|
||||||
])
|
])
|
||||||
|
|
||||||
.orderBy("servizio.created_at", "desc")
|
.orderBy("servizio.created_at", "desc")
|
||||||
.execute();
|
.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),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedData;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
|
@ -831,6 +929,160 @@ export const getAllServizioAnnunci = async (userId: UsersId) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServizioDataById = async (
|
||||||
|
servizioId: ServizioServizioId,
|
||||||
|
): Promise<ServizioData> => {
|
||||||
|
try {
|
||||||
|
const data = await db
|
||||||
|
.selectFrom("servizio")
|
||||||
|
.leftJoin(
|
||||||
|
"users_anagrafica",
|
||||||
|
"servizio.user_id",
|
||||||
|
"users_anagrafica.userid",
|
||||||
|
)
|
||||||
|
.select((eb) => [
|
||||||
|
"servizio.servizio_id",
|
||||||
|
"servizio.created_at",
|
||||||
|
"servizio.decorrenza",
|
||||||
|
"servizio.tipologia",
|
||||||
|
"servizio.budget",
|
||||||
|
"servizio.permanenza",
|
||||||
|
"servizio.isInterrotto",
|
||||||
|
"servizio.isOkAcconto",
|
||||||
|
"servizio.isOkSaldo",
|
||||||
|
"servizio.isOkConsulenza",
|
||||||
|
"servizio.skipPayment",
|
||||||
|
"servizio.skipControlloDoc",
|
||||||
|
"servizio.skipDocMotivazione",
|
||||||
|
"servizio.n_adulti",
|
||||||
|
"servizio.n_minori",
|
||||||
|
"servizio.entromese",
|
||||||
|
jsonObjectFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom("users_storage")
|
||||||
|
.select([
|
||||||
|
"users_storage.storageId as storageId",
|
||||||
|
"users_anagrafica.doc_personale_fronte_ref as ref",
|
||||||
|
])
|
||||||
|
.whereRef(
|
||||||
|
"users_storage.user_storage_id",
|
||||||
|
"=",
|
||||||
|
"users_anagrafica.doc_personale_fronte_ref",
|
||||||
|
),
|
||||||
|
).as("doc_personale_fronte"),
|
||||||
|
jsonObjectFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom("users_storage")
|
||||||
|
.select([
|
||||||
|
"users_storage.storageId as storageId",
|
||||||
|
"users_anagrafica.doc_personale_retro_ref as ref",
|
||||||
|
])
|
||||||
|
.whereRef(
|
||||||
|
"users_storage.user_storage_id",
|
||||||
|
"=",
|
||||||
|
"users_anagrafica.doc_personale_retro_ref",
|
||||||
|
),
|
||||||
|
).as("doc_personale_retro"),
|
||||||
|
jsonObjectFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom("users_storage")
|
||||||
|
.select([
|
||||||
|
"users_storage.storageId as storageId",
|
||||||
|
"servizio.doc_motivazione_ref as ref",
|
||||||
|
])
|
||||||
|
.whereRef(
|
||||||
|
"users_storage.user_storage_id",
|
||||||
|
"=",
|
||||||
|
"servizio.doc_motivazione_ref",
|
||||||
|
),
|
||||||
|
).as("doc_motivazione"),
|
||||||
|
jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom("servizio_annunci")
|
||||||
|
.selectAll("servizio_annunci")
|
||||||
|
.innerJoin("annunci", "annunci.id", "servizio_annunci.annunci_id")
|
||||||
|
.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(),
|
||||||
|
])
|
||||||
|
//ignora annunci che non sono disponibili
|
||||||
|
.where("annunci.web", "=", true)
|
||||||
|
.where("annunci.stato", "!=", "Sospeso")
|
||||||
|
.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_annunci.servizio_id",
|
||||||
|
"=",
|
||||||
|
"servizio.servizio_id",
|
||||||
|
),
|
||||||
|
).as("annunci"),
|
||||||
|
])
|
||||||
|
.orderBy("servizio.created_at", "desc")
|
||||||
|
.where("servizio.servizio_id", "=", servizioId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!data) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Servizio not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const formattedData = {
|
||||||
|
...data,
|
||||||
|
annunci: data.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),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
return formattedData;
|
||||||
|
} catch (e) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: `Servizio annunci not found: ${(e as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const addServizioAnnunci = async ({
|
export const addServizioAnnunci = async ({
|
||||||
servizioId,
|
servizioId,
|
||||||
annunci,
|
annunci,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ export const db = new Kysely<Database>({
|
||||||
new HandleEmptyInListsPlugin({
|
new HandleEmptyInListsPlugin({
|
||||||
strategy: replaceWithNoncontingentExpression,
|
strategy: replaceWithNoncontingentExpression,
|
||||||
}),
|
}),
|
||||||
|
//new ParseJSONResultsPlugin(),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { isValid } from "date-fns";
|
||||||
import { expressionBuilder } from "kysely";
|
import { expressionBuilder } from "kysely";
|
||||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
||||||
import type Database from "~/schemas/Database";
|
import type Database from "~/schemas/Database";
|
||||||
|
|
@ -52,3 +53,11 @@ export function withVideos() {
|
||||||
.orderBy("videos_refs.ordine", "asc"),
|
.orderBy("videos_refs.ordine", "asc"),
|
||||||
).as("videos");
|
).as("videos");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseNullableDateString(str: string | null): Date | null {
|
||||||
|
if (str === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const date = new Date(str);
|
||||||
|
return isValid(date) ? date : null;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue