feat: add AdminLavoraConferma and AdminContratto components for managing service confirmations and contracts

- Implemented AdminLavoraConferma component to handle confirmation processes with document management.
- Added AdminContratto component for managing rental contracts and related documents.
- Introduced shared components for document selection and display (DocInfo and DocCombo).
- Enhanced storage controller with a new function to retrieve storage ID from user storage ID.
- Updated database interactions to improve error handling and data retrieval.
This commit is contained in:
Marco Pedone 2025-11-06 17:03:42 +01:00
parent 17ecc9604f
commit 23525d8f14
19 changed files with 1900 additions and 1482 deletions

View file

@ -88,7 +88,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e
disponibile_da = $47,
permanenza = $48,
persone = $49,
updated_at = NOW()
media_updated_at = NOW()
WHERE codice = $46
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,
annuncio.Email, annuncio.Creato_il, annuncio.Modificato_il,
@ -155,7 +155,7 @@ func AnnunciInsert(annunci typesdefs.AnnunciParsed, resetAllBeforeUpdate bool) e
disponibile_da,
permanenza,
persone,
updated_at
media_updated_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, NOW())
`, annuncio.Locatore, annuncio.Numero, annuncio.Idlocatore,

View file

@ -0,0 +1,540 @@
import { format } from "date-fns";
import { CircleCheck, ClipboardPaste, Pen, Trash2, Wrench } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import z from "zod";
import { formatCurrency } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { LoadingPage } from "../loading";
import { Button } from "../ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import { UploadModal } from "../upload_modal";
import { DocCombo, DocInfo } from "./shared";
export const AdminLavoraConferma = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Open Lavora Conferma Section"
className="w-fit"
variant={data.hasConfermaAdmin ? "success" : "secondary"}
>
{data.hasConfermaAdmin ? (
<CircleCheck className="size-6" />
) : (
<Wrench className="size-6" />
)}
Conferma
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] w-full px-3 sm:max-w-4xl">
<CredenzaHeader>
<CredenzaTitle>Lavorazione conferma</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] w-full overflow-y-auto pb-5">
<div className="flex flex-col gap-8">
<DocSection />
<Caparra />
{servizio.tipologia === TipologiaPosizioneEnum.Transitorio && (
<MotivazioneSection />
)}
<SendConferma />
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Close Credenza" variant="outline">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const DocSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const utils = api.useUtils();
// const { mutate: updateServizio } = api.servizio.UpdateConfermaDoc.useMutation(
// {
// onError: (error) => {
// toast.error(error.message);
// },
// onSuccess: async () => {
// await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
// await utils.servizio.getServizio.invalidate({
// servizioId,
// });
// toast.success("Dati salvati con successo");
// },
// },
// );
const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
if (isLoading) return <LoadingPage />;
if (files === undefined) {
return <p>Errore nel caricamento dei documenti</p>;
}
return (
<Card>
<CardHeader>
<CardTitle>Documento di Conferma</CardTitle>
<CardDescription>
documento che l&apos;utente vedra alla conferma
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{data.doc_conferma_ref && (
<div className="flex flex-wrap items-start gap-3">
<DocInfo storageId={data.doc_conferma_ref}>
<Button
aria-label="Reset Conferma"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_conferma_added: false,
doc_conferma_ref: null,
});
}}
variant="destructive"
>
<Trash2 />
</Button>
</DocInfo>
</div>
)}
<div className="flex flex-wrap items-center gap-3">
<DocCombo
current={data.doc_conferma_ref}
files={files}
onSelect={async (f) => {
await updateServizioAnnunci({
doc_conferma_added: true,
doc_conferma_ref: f.id,
});
setUserStorage({
storageId: f.id,
userId,
});
}}
/>
<UploadModal isAdmin userId={userId} />
</div>
</CardContent>
</Card>
);
};
const CaparraSchema = z.object({
caparra_causale: z.string().nullable(),
caparra_iban: z.string().nullable(),
caparra_importo: z.string().nullable(),
caparra_intestazione: z.string().nullable(),
});
type CaparraObj = z.infer<typeof CaparraSchema>;
const parseCaparra = (ingest: string) => {
let parsedJson: unknown;
try {
parsedJson = JSON.parse(ingest);
} catch {
toast.error(
'Stringa non valida, clicca "copia stringa" nel gestionale e poi questo pulsante',
);
return undefined;
}
const parsed = CaparraSchema.safeParse(parsedJson);
if (parsed.success) {
return parsed.data;
}
console.error("Invalid caparra format:", parsed.error);
return undefined;
};
const Caparra = () => {
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [caparra, setCaparra] = useState<CaparraObj | undefined>({
caparra_causale: data.caparra_causale,
caparra_iban: data.caparra_iban,
caparra_importo: data.caparra_importo,
caparra_intestazione: data.caparra_intestazione,
});
const handleSave = async () => {
await updateServizioAnnunci({
caparra_causale: caparra?.caparra_causale || null,
caparra_iban: caparra?.caparra_iban || null,
caparra_importo: caparra?.caparra_importo || null,
caparra_intestazione: caparra?.caparra_intestazione || null,
});
};
return (
<Card>
<CardHeader>
<CardTitle>Dati Pagamento Caparra</CardTitle>
<CardDescription>
dati per il pagamento della caparra, da mostrare all&apos;utente
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-3">
<Button
aria-label="Import Caparra"
onClick={async () => {
const clipboardText = await navigator.clipboard.readText();
setCaparra(parseCaparra(clipboardText));
}}
variant="info"
>
<ClipboardPaste className="size-4" />
Dal gestionale
</Button>
<Button
aria-label="Reset Caparra"
onClick={async () => {
setCaparra({
caparra_causale: null,
caparra_iban: null,
caparra_importo: null,
caparra_intestazione: null,
});
}}
variant="destructive"
>
<Trash2 className="size-4" />
</Button>
</div>
{caparra ? (
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">Intestazione: </span>
{caparra.caparra_intestazione}{" "}
<Button
aria-label="Edit Intestazione"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'intestazione della caparra:",
caparra.caparra_intestazione || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev
? { ...prev, caparra_intestazione: newValue }
: undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">IBAN: </span>
{caparra.caparra_iban}{" "}
<Button
aria-label="Edit IBAN"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'IBAN della caparra:",
caparra.caparra_iban || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_iban: newValue } : undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Importo: </span>
{caparra.caparra_importo}{" "}
<Button
aria-label="Edit Importo"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'importo della caparra:",
caparra.caparra_importo || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_importo: newValue } : undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Causale: </span>
{caparra.caparra_causale}{" "}
<Button
aria-label="Edit Causale"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica la causale della caparra:",
caparra.caparra_causale || "",
);
if (newValue !== null) {
setCaparra((prev) =>
prev ? { ...prev, caparra_causale: newValue } : undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
</div>
) : (
<p>Inserisci la stringa dal gestionale e elabora</p>
)}
</CardContent>
<CardFooter>
<Button
aria-label="Save Caparra"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};
const MotivazioneSection = () => {
const { servizioId } = useServizio();
const { t } = useTranslation();
const { data: servizio } = api.servizio.getServizio.useQuery({ servizioId });
if (!servizio) return null;
return (
<Card>
<CardHeader>
<CardTitle>Motivazione di transitorietà</CardTitle>
<CardDescription>
Informazioni relative alla motivazione di transitorietà inserite
dall&apos;utente
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">Motivazione specificata: </span>
{servizio.motivazione_transitorio ? (
<span>
{
t.preferenze.motivazioni_mappings.find(
(m) => m.id === servizio.motivazione_transitorio,
)?.title
}
</span>
) : (
<span className="text-red-500 italic">Non inserita</span>
)}
</p>
<p>
<span className="font-semibold">
Data scadenza motivazione inserita:{" "}
</span>
{servizio.scadenza_motivazione_transitoria ? (
format(servizio.scadenza_motivazione_transitoria, "dd/MM/yyyy")
) : (
<span className="text-red-500 italic">Non inserita</span>
)}
</p>
<p>
<span className="font-semibold">Documento caricato: </span>
{servizio.doc_motivazione_ref ? (
<FileLink userStorageId={servizio.doc_motivazione_ref} />
) : (
<span className="text-red-500 italic">Non caricato</span>
)}
</p>
</div>
</CardContent>
</Card>
);
};
const FileLink = ({
userStorageId,
}: {
userStorageId: UsersStorageUserStorageId;
}) => {
const { data: storageId } =
api.storage.getStorageIdFromUserStorageId.useQuery({
userStorageId,
});
if (!storageId) return null;
return (
<Link
className="underline hover:text-primary"
href={`/area-riservata/allegato-view/${storageId}`}
rel="noreferrer"
target="_blank"
>
Vedi documento
</Link>
);
};
const SendConferma = () => {
const { servizio } = useServizio();
const { data } = useServizioAnnuncio();
const { t } = useTranslation();
const [check, setCheck] = useState(data.hasConfermaAdmin);
const utils = api.useUtils();
const { mutate: handleUpdate } =
api.servizio.adminUpdateConfermaStatus.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate();
await utils.servizio.getServizio.invalidate({
servizioId: servizio.servizio_id,
});
toast.success("Status aggiornato con successo");
},
onError: (error) => {
toast.error(`Errore durante l'aggiornamento: ${error.message}`);
},
});
return (
<Card>
<CardHeader>
<CardTitle>Lavorazione Conferma</CardTitle>
<CardDescription>
controlla i parametri di ricerca prima!
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-col gap-2">
<p>Tipologia servizio: {servizio.tipologia}</p>
{(() => {
if (servizio.tipologia === TipologiaPosizioneEnum.Transitorio) {
return (
<>
<p>
Permanenza specificata dall'utente:{" "}
{servizio.permanenza
? t.preferenze.permanenza[servizio.permanenza]
: "non inserita"}
</p>
<p>
Permanenza in descrizione: {(() => {
const match = data.desc_it?.match(/Permanenza:.*$/m);
return match
? `"${match[0]}"`
: "errore: testo non trovato in descrizione";
})()}
</p>
</>
);
}
return (
<>
<p>Budget (da preferenze): {formatCurrency(servizio.budget)}</p>
<p>
Canone Mensile (da descrizione):{" "}
{formatCurrency(data.prezzo / 100)}
</p>
</>
);
})()}
</div>
<div className="flex items-center gap-3">
<Switch
aria-label="Conferma Lavorata"
checked={check}
id="conferma"
onCheckedChange={(c) => {
setCheck(c);
}}
/>
<Label htmlFor="conferma">Conferma lavorata</Label>
</div>
</CardContent>
<CardFooter>
<Button
aria-label="Save Conferma Status"
onClick={() => {
handleUpdate({
servizioId: servizio.servizio_id,
annuncioId: data.id,
status: check,
});
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};

View file

@ -0,0 +1,512 @@
import { CircleCheck, ClipboardPaste, Pen, Trash2, Wrench } from "lucide-react";
import { useState } from "react";
import toast from "react-hot-toast";
import z from "zod";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import { api } from "~/utils/api";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import { LoadingPage } from "../loading";
import { Button } from "../ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
import { Separator } from "../ui/separator";
import { UploadModal } from "../upload_modal";
import { DocCombo, DocInfo } from "./shared";
export const AdminContratto = () => {
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button
aria-label="Open Contratto Section"
className="w-fit"
variant={data.doc_contratto_added ? "success" : "secondary"}
>
{data.doc_contratto_added ? (
<CircleCheck className="size-6" />
) : (
<Wrench className="size-6" />
)}
Contratto
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh] max-w-4xl px-3 sm:max-w-5xl">
<CredenzaHeader>
<CredenzaTitle>Sezione Contratto</CredenzaTitle>
<CredenzaDescription className="sr-only">
Contratto modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="flex flex-col gap-8">
<ContrattoSection />
<Separator />
<ContrattoImport />
<Separator />
<RicevutaSection />
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Close Credenza" variant="outline">
Chiudi
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const ContrattoSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_contratto_ref,
);
const utils = api.useUtils();
const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
const handleSave = async () => {
await updateServizioAnnunci({
doc_contratto_added: selectedDoc != null,
doc_contratto_ref: selectedDoc,
});
if (selectedDoc) {
setUserStorage({
storageId: selectedDoc,
userId,
});
}
};
if (isLoading) return <LoadingPage />;
if (files === undefined) {
return <p>Errore nel caricamento dei documenti</p>;
}
return (
<Card>
<CardHeader>
<CardTitle>Documento del Contratto</CardTitle>
<CardDescription>contratto di locazione</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedDoc && (
<div className="flex flex-wrap items-center gap-3">
<DocInfo storageId={selectedDoc}>
<Button
aria-label="Reset Contratto"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_contratto_added: false,
doc_contratto_ref: null,
});
setSelectedDoc(null);
}}
variant="destructive"
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
current={selectedDoc}
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
/>
<UploadModal isAdmin userId={userId} />
</CardContent>
<CardFooter>
<Button
aria-label="Save Contratto"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};
const ContrattoImportSchema = z.object({
contratto_decorrenza: z.string().nullable(),
contratto_scadenza: z.string().nullable(),
contratto_tipo: z
.enum([
"Transitorio",
"Agevolato",
"4 + 4",
"30 Giorni",
"Transi.Age",
"Foresteria",
"Commerciale",
])
.nullable(),
gestionale_id: z.number().nullable(),
});
interface ContrattoImportObj
extends Omit<
z.infer<typeof ContrattoImportSchema>,
"contratto_decorrenza" | "contratto_scadenza"
> {
contratto_decorrenza: Date | null;
contratto_scadenza: Date | null;
}
const parseContrattoImport = (ingest: string) => {
let parsedJson: unknown;
try {
parsedJson = JSON.parse(ingest);
} catch {
toast.error(
'Stringa non valida, clicca "copia stringa" nel gestionale e poi questo pulsante',
);
return undefined;
}
const parsed = ContrattoImportSchema.safeParse(parsedJson);
if (parsed.success) {
return {
...parsed.data,
contratto_decorrenza: parsed.data.contratto_decorrenza
? new Date(parsed.data.contratto_decorrenza)
: null,
contratto_scadenza: parsed.data.contratto_scadenza
? new Date(parsed.data.contratto_scadenza)
: null,
};
}
console.error("Invalid format:", parsed.error);
return undefined;
};
const ContrattoImport = () => {
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [contratto, setContratto] = useState<ContrattoImportObj | undefined>({
contratto_decorrenza: data.contratto_decorrenza
? new Date(data.contratto_decorrenza)
: null,
contratto_scadenza: data.contratto_scadenza
? new Date(data.contratto_scadenza)
: null,
contratto_tipo: data.contratto_tipo as ContrattoImportObj["contratto_tipo"],
gestionale_id: data.gestionale_id,
});
const handleSave = async () => {
await updateServizioAnnunci({
contratto_decorrenza: contratto?.contratto_decorrenza || null,
contratto_scadenza: contratto?.contratto_scadenza || null,
contratto_tipo: contratto?.contratto_tipo || null,
gestionale_id: contratto?.gestionale_id || null,
});
};
return (
<Card>
<CardHeader>
<CardTitle>Dati Contratto</CardTitle>
<CardDescription className="sr-only">desc</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
<Button
aria-label="Import Contratto"
onClick={async () => {
const clipboardText = await navigator.clipboard.readText();
setContratto(parseContrattoImport(clipboardText));
}}
variant="info"
>
<ClipboardPaste className="size-4" />
Clicca qui per importare la stringa dal gestionale
</Button>
<Button
aria-label="Reset Contratto"
onClick={() => {
setContratto({
contratto_decorrenza: null,
contratto_scadenza: null,
contratto_tipo: null,
gestionale_id: null,
});
}}
variant="destructive"
>
<Trash2 className="size-4" />
Reset
</Button>
</div>
{contratto ? (
<div className="flex flex-col gap-2">
<p>
<span className="font-semibold">ID Gestionale: </span>
{contratto.gestionale_id}{" "}
<Button
aria-label="Edit Gestionale ID"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica l'ID gestionale del contratto:",
contratto.gestionale_id?.toString() || "",
);
if (newValue !== null) {
setContratto((prev) =>
prev
? { ...prev, gestionale_id: parseInt(newValue) }
: undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Decorrenza: </span>
{contratto.contratto_decorrenza &&
new Date(contratto.contratto_decorrenza).toLocaleDateString(
"it-IT",
)}{" "}
<Button
aria-label="Edit Decorrenza"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica la decorrenza del contratto (DD/MM/YYYY):",
contratto.contratto_decorrenza
? new Date(
contratto.contratto_decorrenza,
).toLocaleDateString("it-IT")
: "",
);
if (newValue !== null) {
setContratto((prev) =>
prev
? {
...prev,
contratto_decorrenza: new Date(newValue),
}
: undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Scadenza: </span>
{contratto.contratto_scadenza &&
new Date(contratto.contratto_scadenza).toLocaleDateString(
"it-IT",
)}{" "}
<Button
aria-label="Edit Scadenza"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica la scadenza del contratto (DD/MM/YYYY):",
contratto.contratto_scadenza
? new Date(
contratto.contratto_scadenza,
).toLocaleDateString("it-IT")
: "",
);
if (newValue !== null) {
setContratto((prev) =>
prev
? {
...prev,
contratto_scadenza: new Date(newValue),
}
: undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
<p>
<span className="font-semibold">Tipo di contratto: </span>
{contratto.contratto_tipo}{" "}
<Button
aria-label="Edit Tipo di contratto"
className="h-6 px-1.5 [&_svg]:size-4"
onClick={() => {
const newValue = prompt(
"Modifica il tipo di contratto:",
contratto.contratto_tipo || "",
);
if (
newValue !== null &&
ContrattoImportSchema.shape.contratto_tipo.safeParse(
newValue,
).success
) {
setContratto((prev) =>
prev
? {
...prev,
contratto_tipo:
newValue as ContrattoImportObj["contratto_tipo"],
}
: undefined,
);
}
}}
size="sm"
>
<Pen />
</Button>
</p>
</div>
) : (
<p>Inserisci la stringa dal gestionale e elabora</p>
)}
</CardContent>
<CardFooter>
<Button
aria-label="Save Contratto"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};
const RicevutaSection = () => {
const { userId } = useServizio();
const { data, updateServizioAnnunci } = useServizioAnnuncio();
const [selectedDoc, setSelectedDoc] = useState<string | null>(
data.doc_registrazione_ref,
);
const utils = api.useUtils();
const { data: files, isLoading } = api.storage.getAll.useQuery();
const { mutate: setUserStorage } =
api.storage.addUserStorageEntry.useMutation({
onSuccess: async () => {
await utils.storage.retrieveUserFileData.invalidate({
userId,
});
await utils.storage.getAll.invalidate();
},
});
const handleSave = async () => {
await updateServizioAnnunci({
doc_registrazione_added: selectedDoc != null,
doc_registrazione_ref: selectedDoc,
});
if (selectedDoc) {
setUserStorage({
storageId: selectedDoc,
userId,
});
}
};
if (isLoading) return <LoadingPage />;
if (files === undefined) {
return <p>Errore nel caricamento dei documenti</p>;
}
return (
<Card>
<CardHeader>
<CardTitle>Ricevuta di Registrazione</CardTitle>
<CardDescription className="sr-only">desc</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{selectedDoc && (
<div className="flex flex-wrap items-center gap-3">
<DocInfo storageId={selectedDoc}>
<Button
aria-label="Reset Ricevuta"
className="flex items-center gap-3"
onClick={async () => {
await updateServizioAnnunci({
doc_registrazione_added: false,
doc_registrazione_ref: null,
});
setSelectedDoc(null);
}}
variant="destructive"
>
<Trash2 className="size-6" />
</Button>
</DocInfo>
</div>
)}
<DocCombo
current={selectedDoc}
files={files}
onSelect={(f) => {
setSelectedDoc(f.id);
}}
/>
<UploadModal isAdmin userId={userId} />
</CardContent>
<CardFooter>
<Button
aria-label="Save Ricevuta"
onClick={async () => {
await handleSave();
}}
variant="success"
>
Salva
</Button>
</CardFooter>
</Card>
);
};

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,28 @@
import { CircleCheck, Handshake } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import toast from "react-hot-toast";
import { AllegatoIframe } from "~/components/allegato-iframe";
import { LoadingPage } from "~/components/loading";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import { api } from "~/utils/api";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "../custom_ui/credenza";
import LoadingButton from "../custom_ui/loading-button";
import { Button } from "../ui/button";
import { type UploadCallback, UploadModal } from "../upload_modal";
export const FileSection = ({ storageId }: { storageId: string }) => {
const { data: fileInfos, isLoading } = api.storage.getFileInfos.useQuery({
@ -27,3 +47,236 @@ export const FileSection = ({ storageId }: { storageId: string }) => {
</>
);
};
export const ConfermaAnnuncioModal = () => {
const { servizio } = useServizio();
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const needsDocuments =
!servizio.skipDocMotivazione &&
!servizio.doc_motivazione &&
servizio.tipologia === TipologiaPosizioneEnum.Transitorio;
return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Conferma immobile" className="w-fit">
<Handshake className="size-6" /> Sono interessato a confermare
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle> {t.richieste.conf_title}</CredenzaTitle>
<CredenzaDescription className="sr-only">
{t.richieste.conf_desc}
</CredenzaDescription>
</CredenzaHeader>
{needsDocuments ? (
<DocCheckpoint />
) : (
<InvioConferma setOpen={setOpen} />
)}
</CredenzaContent>
</Credenza>
);
};
const InvioConferma = ({ setOpen }: { setOpen: (value: boolean) => void }) => {
const { userId, servizioId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const { t } = useTranslation();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.userSendConfermaIntent.useMutation(
{
onError: (err) => {
toast.error(err.message, { id: "updateRichiestaToast" });
},
onMutate: () => {
toast.loading("Conferma in corso", { id: "updateRichiestaToast" });
},
onSuccess: async () => {
toast.success("Richiesta confermata con successo", {
id: "updateRichiestaToast",
});
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
setOpen(false);
},
},
);
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<p>{t.richieste.conferma_txt}</p>
</CredenzaBody>
<CredenzaFooter>
<LoadingButton
aria-label="Conferma immobile"
loading={isPending}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant="success"
>
{t.conferma}
</LoadingButton>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza">Chiudi</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
const DocComponent = ({
docInfo,
cb,
}: {
docInfo: {
storageId: string;
ref: UsersStorageUserStorageId | null;
} | null;
cb: UploadCallback;
}) => {
const { isAdmin, userId } = useServizio();
if (docInfo?.storageId) {
return (
<div className="flex items-center gap-2 text-green-500">
<CircleCheck className="size-5" />
<span>Documento caricato</span>
</div>
);
}
return (
<UploadModal
cb_onUpload={cb}
isAdmin={isAdmin}
maxFiles={1}
userId={userId}
/>
);
};
const DocCheckpoint = () => {
const { t } = useTranslation();
const { userId, servizio } = useServizio();
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.updateServizio.useMutation({
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId,
});
await utils.storage.retrieveUserFileData.invalidate({ userId });
await utils.servizio.getServizio.invalidate({
servizioId: servizio.servizio_id,
});
},
onError: (err) => {
toast.error(`Errore aggiornamento servizio: ${err.message}`);
},
});
const handleUpload = (id: UsersStorageUserStorageId) => {
mutate({
servizioId: servizio.servizio_id,
data: { doc_motivazione_ref: id },
});
};
return (
<>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<div className="space-y-5">
<p>
Devi ancora caricare un tuo documento legato alla permanenza e
motivazione di transitorietà
</p>
<div className="flex flex-col gap-2">
<span>Documento:</span>
<DocComponent
cb={({ userStorageIds }) =>
userStorageIds.length > 0 &&
userStorageIds[0] &&
handleUpload(userStorageIds[0])
}
docInfo={servizio.doc_motivazione}
/>
</div>
<br />
<br />
<p className="text-muted-foreground text-sm">
I tuoi dati saranno conservati solamente per la durata del servizio,
in conformità con la nostra{" "}
<Link className="underline" href="/privacy-policy" target="_blank">
Informativa sulla Privacy
</Link>
</p>
</div>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi" disabled={isPending} variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</>
);
};
export const ConfermaInLavorazioneModal = () => {
const { t } = useTranslation();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button aria-label="Conferma in lavorazione" className="w-fit">
<CircleCheck className="size-7" />
Conferma in lavorazione
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle>In attesa di conferma</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="prose max-h-[80vh] overflow-auto pb-5">
<p>
Richiesta di conferma inviata il:{" "}
{data.user_confirmed_at &&
new Date(data.user_confirmed_at).toLocaleString("it", {
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
})}
</p>
<p>
La conferma è ancora in lavorazione. Stiamo verificando con i
proprietari le condizioni e dettagli della conferma per riferirteli.
Attendi una comunicazione.
</p>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};

View file

@ -1,27 +1,7 @@
import {
CircleCheck,
FileBadge,
FileText,
Handshake,
PackageCheck,
Pointer,
} from "lucide-react";
import { FileBadge, FileText, PackageCheck, Pointer } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import toast from "react-hot-toast";
import {
Credenza,
CredenzaBody,
CredenzaClose,
CredenzaContent,
CredenzaDescription,
CredenzaFooter,
CredenzaHeader,
CredenzaTitle,
CredenzaTrigger,
} from "~/components/custom_ui/credenza";
import LoadingButton from "~/components/custom_ui/loading-button";
import { Button, type ButtonConfigs } from "~/components/ui/button";
import {
Tooltip,
@ -30,49 +10,166 @@ import {
TooltipTrigger,
} from "~/components/ui/tooltip";
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { useServizio, useServizioAnnuncio } from "~/providers/ServizioProvider";
import type { AnnunciId } from "~/schemas/public/Annunci";
import { api } from "~/utils/api";
import { ConfermaAnnuncioModal, ConfermaInLavorazioneModal } from "./conferma";
import { ContattiProprietarioModal } from "./modale_contatti";
export const Interactions = () => {
const SezioneConferma = () => {
const { servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
const ContattiSbloccati = data.open_contatti_at !== null;
const ConfermaUtente = data.user_confirmed_at !== null;
const ConfermaLavorata = data.doc_conferma_added && data.hasConfermaAdmin;
const AccettatoConferma = data.accettato_conferma_at !== null;
if (ContattiSbloccati && !ConfermaUtente) {
return <ConfermaAnnuncioModal />;
}
if (ConfermaUtente && !data.hasConfermaAdmin) {
return <ConfermaInLavorazioneModal />;
}
if (AccettatoConferma) {
return (
<Link
aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Rivedi la conferma
</Button>
</Link>
);
}
if (ConfermaLavorata) {
if (data.doc_conferma_ref) {
return (
<Link
aria-label="Conferma immobile"
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
>
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Confermato - Procedi ora
</Button>
</Link>
);
}
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="w-fit cursor-not-allowed opacity-50"
variant="success"
>
<Pointer className="size-6" />
Confermato - Procedi ora
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>Se hai bisogno di una copia, contattaci via email o chat.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return null;
};
const PostConfermaSection = () => {
const { servizio, servizioId } = useServizio();
const { data, annuncioId } = useServizioAnnuncio();
const saldoPaid = servizio.isOkSaldo;
const consulenzaPaid = servizio.isOkConsulenza;
if (!saldoPaid) {
return (
<div className="flex flex-col gap-1">
<h3 className="font-semibold">
Questo annuncio è stato confermato, procedi al saldo:
</h3>
<SaldoButton annuncioId={annuncioId} />
</div>
);
}
if (!data.doc_contratto_added) {
return (
<div className="flex flex-col gap-1">
<h3 className="font-semibold text-green-500">
Ci siamo quasi! Stiamo preparando il contratto per te.
</h3>
<h3 className="font-semibold">
Una volta pronto, riceverai una notifica via email.
</h3>
</div>
);
}
return (
<div className="flex flex-col gap-3">
<ContattiProprietarioModal />
<div className="flex flex-wrap gap-3">
{data.open_contatti_at !== null && data.user_confirmed_at == null && (
<AnnuncioConfirm />
<div className="flex flex-col gap-2">
<h3 className="font-semibold">
{consulenzaPaid
? "Il contratto è pronto, puoi visualizzarlo:"
: "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"}
</h3>
<div className="flex flex-wrap gap-2">
{data.doc_contratto_ref ? (
<Link
aria-label="Visualizza contratto"
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-fit">
<FileText className="size-6" />
Visualizza il contratto
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileText className="size-6" />
Visualizza il contratto
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>Se hai bisogno di una copia, contattaci via email o chat.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{data.user_confirmed_at && !data.hasConfermaAdmin && (
<ConfermaLavorazioneModal />
)}
{data.doc_conferma_added &&
data.accettato_conferma_at == null &&
data.hasConfermaAdmin &&
(data.doc_conferma_ref ? (
{!consulenzaPaid && data.contratto_tipo && <SaldoConsulenzaButton />}
{data.doc_registrazione_added &&
(data.doc_registrazione_ref ? (
<Link
aria-label="Conferma immobile"
href={`/servizio/conferma-immobile/${servizioId}/${annuncioId}`}
aria-label="Visualizza registrazione"
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Confermato - Procedi ora
<Button className="w-fit">
<FileBadge className="size-6" />
Visualizza la registrazione
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
className="w-fit cursor-not-allowed opacity-50"
variant="success"
>
<Pointer className="size-6" />
Confermato - Procedi ora
<Button className="w-fit cursor-not-allowed opacity-50">
<FileBadge className="size-6" />
Visualizza la registrazione
</Button>
</TooltipTrigger>
<TooltipContent>
@ -84,118 +181,21 @@ export const Interactions = () => {
</Tooltip>
</TooltipProvider>
))}
{data.accettato_conferma_at != null && (
<Link
aria-label="Conferma immobile"
href={`/servizio/riapri-conferma/${servizioId}/${annuncioId}`}
>
<Button className="w-fit" variant="success">
<Pointer className="size-6" />
Rivedi la conferma
</Button>
</Link>
)}
</div>
{!servizio.isOkSaldo && data.accettato_conferma_at != null && (
<div className="flex flex-col gap-1">
<h3 className="font-semibold">
Questo annuncio è stato confermato, procedi al saldo:
</h3>
<SaldoButton annuncioId={annuncioId} />
</div>
)}
</div>
);
};
{servizio.isOkSaldo &&
data.accettato_conferma_at != null &&
data.doc_contratto_added == null && (
<div className="flex flex-col gap-1">
<h3 className="font-semibold text-green-500">
Ci siamo quasi! Stiamo preparando il contratto per te.
</h3>
<h3 className="font-semibold">
Una volta pronto, riceverai una notifica via email.
</h3>
</div>
)}
{servizio.isOkSaldo &&
data.accettato_conferma_at != null &&
data.doc_contratto_added && (
<div className="flex flex-col gap-2">
<h3 className="font-semibold">
{servizio.isOkConsulenza
? "Il contratto è pronto, puoi visualizzarlo:"
: "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:"}
</h3>
<div className="flex flex-wrap gap-2">
{data.doc_contratto_ref ? (
<Link
aria-label="Visualizza contratto"
href={`/servizio/contratto/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-fit">
<FileText className="size-6" />
Visualizza il contratto
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileText className="size-6" />
Visualizza il contratto
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>
Se hai bisogno di una copia, contattaci via email o
chat.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
export const Interactions = () => {
const { data } = useServizioAnnuncio();
{!servizio.isOkConsulenza && data.contratto_tipo && (
<SaldoConsulenzaButton />
)}
return (
<div className="flex flex-col gap-3">
<ContattiProprietarioModal />
{data.doc_registrazione_added &&
(data.doc_registrazione_ref ? (
<Link
aria-label="Visualizza registrazione"
href={`/servizio/contratto-registrazione/${servizioId}/${annuncioId}`}
target="_blank"
>
<Button className="w-fit">
<FileBadge className="size-6" />
Visualizza la registrazione
</Button>
</Link>
) : (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button className="w-fit cursor-not-allowed opacity-50">
<FileBadge className="size-6" />
Visualizza la registrazione
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Il documento non è più disponibile</p>
<p>
Se hai bisogno di una copia, contattaci via email o
chat.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
</div>
</div>
)}
<SezioneConferma />
{data.accettato_conferma_at != null && <PostConfermaSection />}
</div>
);
};
@ -282,113 +282,3 @@ const SaldoConsulenzaButton = ({
</Button>
);
};
const AnnuncioConfirm = () => {
const { userId, servizioId } = useServizio();
const { annuncioId } = useServizioAnnuncio();
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const utils = api.useUtils();
const { mutate, isPending } = api.servizio.userConfirmImmobile.useMutation({
onError: (err) => {
toast.error(err.message, { id: "updateRichiestaToast" });
},
onMutate: () => {
toast.loading("Conferma in corso", { id: "updateRichiestaToast" });
},
onSuccess: async () => {
toast.success("Richiesta confermata con successo", {
id: "updateRichiestaToast",
});
await utils.servizio.getAllServizioAnnunci.invalidate({ userId });
setOpen(false);
},
});
return (
<Credenza onOpenChange={setOpen} open={open}>
<CredenzaTrigger asChild>
<Button aria-label="Conferma immobile" className="w-fit">
<Handshake className="size-6" /> Conferma immobile
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle> {t.richieste.conf_title}</CredenzaTitle>
<CredenzaDescription className="sr-only">
{t.richieste.conf_desc}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="max-h-[80vh] overflow-auto pb-5">
<p>{t.richieste.conferma_txt}</p>
</CredenzaBody>
<CredenzaFooter>
<LoadingButton
aria-label="Conferma immobile"
loading={isPending}
onClick={() => {
mutate({
annuncioId,
servizioId,
});
}}
variant="success"
>
{t.conferma}
</LoadingButton>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza">Chiudi</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};
const ConfermaLavorazioneModal = () => {
const { t } = useTranslation();
const { data } = useServizioAnnuncio();
return (
<Credenza>
<CredenzaTrigger asChild>
<Button aria-label="Conferma in lavorazione" className="w-fit">
<CircleCheck className="size-7" />
Conferma in lavorazione
</Button>
</CredenzaTrigger>
<CredenzaContent className="max-h-[90vh]">
<CredenzaHeader>
<CredenzaTitle>In attesa di conferma</CredenzaTitle>
<CredenzaDescription className="sr-only">
Consegna status modal
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody className="prose max-h-[80vh] overflow-auto pb-5">
<p>
Richiesta di conferma inviata il:{" "}
{data.user_confirmed_at &&
new Date(data.user_confirmed_at).toLocaleString("it", {
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
month: "2-digit",
year: "numeric",
})}
</p>
<p>
La conferma è ancora in lavorazione. Stiamo verificando con i
proprietari le condizioni e dettagli della conferma per riferirteli.
Attendi una comunicazione.
</p>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button aria-label="Chiudi modal credenza" variant="outline">
{t.chiudi}
</Button>
</CredenzaClose>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
);
};

View file

@ -171,14 +171,19 @@ export const ServizioContent = () => {
(a) => a.accettato_conferma_at !== null,
);
if (servizio.isOkSaldo) {
if (servizio.isOkSaldo && annuncioConfermato.length > 0) {
return (
<ServizioCard
content={
<>
{annuncioConfermato.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizio.servizio_id}
userId={userId}
>
<AnnuncioCard />
</ServizioAnnuncioProvider>
);
@ -194,6 +199,10 @@ export const ServizioContent = () => {
);
}
const annunciPreContatti = servizio.annunci.filter(
(a) => a.open_contatti_at === null && a.user_confirmed_at === null,
);
const openContattiAnnunci = servizio.annunci.filter(
(a) => a.open_contatti_at !== null && a.user_confirmed_at === null,
);
@ -275,14 +284,19 @@ export const ServizioContent = () => {
<div className="flex flex-col gap-2 overflow-y-auto">
{annunciInConferma.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizio.servizio_id}
userId={userId}
>
<AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider>
);
})}
</div>
<AnnunciSelezionatiAccordion annunci={annunciNotInConferma} />
<AnnunciSelezionatiAccordion annunci={annunciPreContatti} />
<div className="flex items-center justify-center">
<AnnunciCompatibili />
</div>

View file

@ -131,7 +131,7 @@ const DocCheckpoint = () => {
});
const handleUpload = (
field: "doc_personale_fronte_ref" | "doc_personale_retro_ref",
id: string,
id: UsersStorageUserStorageId,
) => {
mutate({
servizioId: servizio.servizio_id,

View file

@ -196,7 +196,7 @@ export const AddAnnuncio = ({
};
const InterruzioneServizio = () => {
const { servizio, userId } = useServizio();
const { servizio, userId, isAdmin } = useServizio();
const beforeInterruzioneLimit =
servizio.decorrenza != null &&
@ -223,9 +223,14 @@ const InterruzioneServizio = () => {
<Button
aria-label="Request Interruzione"
className=""
disabled={!beforeInterruzioneLimit}
disabled={
!isAdmin &&
(!beforeInterruzioneLimit ||
servizio.isInterrotto ||
servizio.annunci.some((a) => a.user_confirmed_at !== null))
}
>
<ClockAlert /> <span> Richiedi Interruzione Servizio</span>
<ClockAlert /> <span>Interruzione Servizio</span>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
@ -255,7 +260,7 @@ const InterruzioneServizio = () => {
};
const EditPreferenze = () => {
const { servizioId, userId, isAdmin } = useServizio();
const { servizioId, userId, isAdmin, servizio } = useServizio();
const [openEditPreferenze, setOpenEditPreferenze] = useState(false);
const { data, isLoading } = api.servizio.getServizio.useQuery({ servizioId });
const utils = api.useUtils();
@ -287,7 +292,15 @@ const EditPreferenze = () => {
return (
<Credenza onOpenChange={setOpenEditPreferenze} open={openEditPreferenze}>
<CredenzaTrigger asChild>
<Button aria-label="Preferenze di ricerca" className="">
<Button
aria-label="Preferenze di ricerca"
className=""
disabled={
!isAdmin &&
(servizio.isInterrotto ||
servizio.annunci.some((a) => a.hasConfermaAdmin))
}
>
<Calculator />
Modifica Preferenze
</Button>

View file

@ -61,6 +61,7 @@ export const AnnunciInConfermaDialog = ({
}: {
annunci: ServizioData["annunci"];
}) => {
const { servizioId, userId } = useServizio();
return (
<DialogServizio
title="Annunci in conferma"
@ -84,7 +85,12 @@ export const AnnunciInConfermaDialog = ({
<div className="flex max-h-[80vh] flex-col gap-2 overflow-y-auto">
{annunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider>
);
@ -99,6 +105,7 @@ export const AnnunciSelezionatiDialog = ({
}: {
annunci: ServizioData["annunci"];
}) => {
const { servizioId, userId } = useServizio();
return (
<DialogServizio
title="Annunci selezionati"
@ -122,7 +129,12 @@ export const AnnunciSelezionatiDialog = ({
{annunci.length > 0 ? (
annunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-sky-500" />
</ServizioAnnuncioProvider>
);
@ -152,6 +164,7 @@ export const AnnunciInConfermaAccordion = ({
}: {
annunci: ServizioData["annunci"];
}) => {
const { servizioId, userId } = useServizio();
if (annunci.length === 0) {
return null; // If there are no announcements in confirmation, do not render this section
}
@ -166,7 +179,12 @@ export const AnnunciInConfermaAccordion = ({
<div className="flex flex-col flex-wrap gap-5">
{annunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-green-500" />
</ServizioAnnuncioProvider>
);
@ -183,6 +201,7 @@ export const AnnunciSelezionatiAccordion = ({
}: {
annunci: ServizioData["annunci"];
}) => {
const { servizioId, userId } = useServizio();
return (
<Accordion collapsible defaultValue="open" type="single">
<AccordionItem className="space-y-4" value="open">
@ -194,7 +213,12 @@ export const AnnunciSelezionatiAccordion = ({
{annunci.length > 0 ? (
annunci.map((data) => {
return (
<ServizioAnnuncioProvider data={data} key={data.id}>
<ServizioAnnuncioProvider
data={data}
key={data.id}
servizioId={servizioId}
userId={userId}
>
<AnnuncioCard className="border-2 border-sky-500" />
</ServizioAnnuncioProvider>
);
@ -210,7 +234,8 @@ export const AnnunciSelezionatiAccordion = ({
>
Annunci
<ExternalLink className="size-4" />
</Link>
</Link>{" "}
o tra gli annunci compatibili qui sotto.
</span>
</div>
)}

View file

@ -0,0 +1,116 @@
import { Check, ChevronsUpDown, ExternalLink } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { FileMetadataWithAdmin } from "~/server/services/storage.service";
import { api } from "~/utils/api";
import { ExtIcon } from "../area-riservata/allegati";
import { Button } from "../ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../ui/command";
import { Label } from "../ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
export const DocInfo = ({
storageId,
children,
}: {
storageId: string;
children: React.ReactNode;
}) => {
const { data: file_info } = api.storage.getFileInfos.useQuery({
storageId,
});
if (!file_info) return null;
return (
<div className="flex w-full flex-col gap-2">
<Label htmlFor="selected-file">File Selezionato: </Label>
<div className="flex w-full items-center gap-2 sm:max-w-[30rem]">
<div className="flex w-full flex-1">
<Link
aria-label="Open Selected File"
href={`/area-riservata/allegato-view/${storageId}`}
id="selected-file"
target="_blank"
>
<Button className="w-fit grow" variant="outline">
<ExtIcon mimeType={file_info.mimeType} />
<span className="max-w-40 truncate sm:max-w-80">
{file_info.originalName}
</span>
<ExternalLink className="size-4" />
</Button>
</Link>
</div>
{children}
</div>
</div>
);
};
export const DocCombo = ({
files,
onSelect,
current,
}: {
files: FileMetadataWithAdmin[];
onSelect: (file: FileMetadataWithAdmin) => void;
current: string | null;
}) => {
const [open, setOpen] = useState(false);
return (
<Popover onOpenChange={setOpen} open={open}>
<PopoverTrigger asChild>
<Button
aria-expanded={open}
aria-label="Select Existing File"
className="w-auto justify-between"
role="combobox"
variant="outline"
>
Seleziona file esistente...
<ChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Command>
<CommandInput placeholder="Cerca Documento..." />
<CommandList>
<CommandEmpty>Nessun file.</CommandEmpty>
<CommandGroup>
{files.map((file) => (
<CommandItem
key={file.id}
onSelect={(currentValue) => {
if (currentValue !== current) {
// biome-ignore lint/style/noNonNullAssertion: <exists>
onSelect(files.find((f) => f.id === currentValue)!);
}
setOpen(false);
}}
value={file.id}
>
<span className="flex items-center gap-2">
<ExtIcon mimeType={file.mimeType} />
<span className="block font-medium">
{file.originalName}
</span>
{current === file.id && (
<Check className="size-4 text-accent-foreground" />
)}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};

View file

@ -182,6 +182,7 @@ export const UploadComponent = ({
userId,
from_admin,
});
if (!res) continue;
userStorageIds.push(res.user_storage_id);
}
}

View file

@ -108,11 +108,11 @@ const FormNewServizioSchema = z.object({
reddito: z.string().nullable(),
sede_legale: z.string().nullable(),
sesso: z.string(),
skipOrdine: z.boolean(),
telefono: z.string(),
terrazzo: z.boolean(),
tipologia: z.custom<TipologiaPosizioneEnum>(),
scadenza_motivazione_transitoria: z.date().nullable(),
skipPayment: z.boolean(),
});
type FormNewServizioValues = z.infer<typeof FormNewServizioSchema>;
@ -308,7 +308,6 @@ export const FormNewServizioAcquisto = ({
sede_legale: initialData.anagrafica?.sede_legale || "",
sesso: initialData.anagrafica?.sesso || "M",
...initialData.servizio,
skipOrdine: false,
};
const { locale, t } = useTranslation();
@ -337,7 +336,7 @@ export const FormNewServizioAcquisto = ({
});
if (isAdmin) {
if (data) {
if (data.ordineIdForPayment) {
await router.push(`/area-riservata/admin/user-view/ordini/${userId}`);
} else {
await router.push(
@ -345,7 +344,11 @@ export const FormNewServizioAcquisto = ({
);
}
} else {
await router.push(`/servizio/pagamento/${data}`);
if (data.ordineIdForPayment) {
await router.push(`/servizio/pagamento/${data.ordineIdForPayment}`);
} else {
await router.push("/area-riservata/dashboard");
}
}
},
});
@ -391,9 +394,9 @@ export const FormNewServizioAcquisto = ({
reddito: fields.reddito,
terrazzo: fields.terrazzo,
tipologia: fields.tipologia,
skipPayment: fields.skipPayment,
},
servizioId: initialData.servizio.servizio_id,
skipOrdine: isAdmin ? fields.skipOrdine : false,
user: {
cognome: fields.cognome,
email: fields.email,
@ -1427,11 +1430,11 @@ export const FormNewServizioAcquisto = ({
{isAdmin && (
<FormField
control={form.control}
name="skipOrdine"
name="skipPayment"
render={({ field }) => (
<FormItem className="py-2">
<div className="flex items-center justify-start gap-x-4">
<FormLabel htmlFor="skipOrdine">
<FormLabel htmlFor="skipPayment">
Vuoi saltare la creazione dell&apos;ordine e impostare il
servizio come attivo?
</FormLabel>
@ -1439,7 +1442,7 @@ export const FormNewServizioAcquisto = ({
<Switch
checked={field.value}
className="data-[state=checked]:bg-neutral-700"
id="skipOrdine"
id="skipPayment"
onCheckedChange={(v) => {
field.onChange(v);
}}

View file

@ -83,24 +83,25 @@ const ConfirmSection = ({
const [letto, setLetto] = useState(false);
const [accettato, setAccettato] = useState(false);
const utils = api.useUtils();
const { mutate: conferma } = api.servizio.updateServizioAnnunci.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🔄",
});
return { toastId };
},
onSuccess: async (_error, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate();
const { mutate: conferma } =
api.servizio.userAcceptConfermaImmobile.useMutation({
onMutate: () => {
const toastId = toast.loading(t.caricamento, {
icon: "🔄",
});
return { toastId };
},
onSuccess: async (_data, _variables, context) => {
await utils.storage.retrieveUserFileData.invalidate();
toast.success("Confermato", {
icon: "👍",
id: context?.toastId,
});
toast.success("Confermato", {
icon: "👍",
id: context?.toastId,
});
await router.push("/area-riservata/dashboard");
},
});
await router.push("/area-riservata/dashboard");
},
});
return (
<>
<div className="flex flex-col items-start space-y-3 px-2">
@ -147,9 +148,6 @@ const ConfirmSection = ({
// Handle confirmation logic here
conferma({
annuncioId,
data: {
accettato_conferma_at: new Date(),
},
servizioId,
});
}}

View file

@ -1,9 +1,19 @@
import { createContext, useContext } from "react";
import toast from "react-hot-toast";
import type { AnnunciId } from "~/schemas/public/Annunci";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci";
import type { UsersId } from "~/schemas/public/Users";
import type { ServizioData } from "~/server/controllers/servizio.controller";
import { api } from "~/utils/api";
type ServizioProviderProps = {
isAdmin: boolean;
userId: UsersId;
servizioId: ServizioServizioId;
servizio: ServizioData;
children: React.ReactNode;
};
type ServizioContextProps = {
isAdmin: boolean;
userId: UsersId;
@ -14,20 +24,11 @@ type ServizioContextProps = {
const ServizioContext = createContext<ServizioContextProps | null>(null);
export const ServizioProvider = ({
isAdmin,
userId,
servizioId,
servizio,
children,
}: {
isAdmin: boolean;
userId: UsersId;
servizioId: ServizioServizioId;
servizio: ServizioData;
children: React.ReactNode;
}) => {
...props
}: ServizioProviderProps) => {
return (
<ServizioContext.Provider value={{ isAdmin, servizio, servizioId, userId }}>
<ServizioContext.Provider value={{ ...props }}>
{children}
</ServizioContext.Provider>
);
@ -41,23 +42,54 @@ export const useServizio = () => {
return servizioContext;
};
type ServizioAnnuncioProviderProps = {
data: ServizioData["annunci"][number];
servizioId: ServizioServizioId;
userId: UsersId;
children: React.ReactNode;
};
type ServizioAnnuncioContextProps = {
data: ServizioData["annunci"][number];
annuncioId: AnnunciId;
updateServizioAnnunci: (data: ServizioAnnunciUpdate) => Promise<void>;
};
const ServizioAnnuncioContext =
createContext<ServizioAnnuncioContextProps | null>(null);
export const ServizioAnnuncioProvider = ({
data,
servizioId,
userId,
children,
}: {
data: ServizioData["annunci"][number];
children: React.ReactNode;
}) => {
}: ServizioAnnuncioProviderProps) => {
const utils = api.useUtils();
const { mutateAsync } = api.servizio.updateServizioAnnunci.useMutation({
onError: (error) => {
toast.error(error.message);
},
onSuccess: async () => {
await utils.servizio.getAllServizioAnnunci.invalidate({
userId: userId,
});
await utils.servizio.getServizio.invalidate({
servizioId,
});
toast.success("Dati salvati con successo");
},
});
const updateServizioAnnunci = async (upd: ServizioAnnunciUpdate) => {
await mutateAsync({
annuncioId: data.id,
servizioId,
data: upd,
});
};
return (
<ServizioAnnuncioContext.Provider
value={{ annuncioId: data.annunci_id, data }}
value={{ annuncioId: data.annunci_id, data, updateServizioAnnunci }}
>
{children}
</ServizioAnnuncioContext.Provider>

View file

@ -20,6 +20,7 @@ import {
import {
addServizio,
addServizioAnnunci,
adminUpdateConfermaStatus,
deleteServizio,
deleteServizioAnnuncio,
getAllPagamenti,
@ -47,7 +48,8 @@ import {
updatePagamento,
updateServizio,
updateServizioAnnuncio,
userConfirmImmobile,
userAccettaConferma,
userSendConfermaIntent,
} from "~/server/controllers/servizio.controller";
import {
zAnnuncioId,
@ -256,7 +258,6 @@ export const servizioRouter = createTRPCRouter({
anagrafica: z.custom<UsersAnagraficaUpdate>(),
servizio: z.custom<ServizioUpdate>(),
servizioId: zServizioId,
skipOrdine: z.boolean().optional(),
user: z.custom<
Pick<Users, "nome" | "cognome" | "email" | "telefono" | "username">
>(),
@ -420,7 +421,22 @@ export const servizioRouter = createTRPCRouter({
servizioId: input.servizioId,
});
}),
userConfirmImmobile: protectedProcedure
adminUpdateConfermaStatus: adminProcedure
.input(
z.object({
annuncioId: zAnnuncioId,
servizioId: zServizioId,
status: z.boolean(),
}),
)
.mutation(async ({ input }) => {
return await adminUpdateConfermaStatus({
annuncioId: input.annuncioId,
servizioId: input.servizioId,
status: input.status,
});
}),
userSendConfermaIntent: protectedProcedure
.input(
z.object({
annuncioId: zAnnuncioId,
@ -428,7 +444,20 @@ export const servizioRouter = createTRPCRouter({
}),
)
.mutation(async ({ input }) => {
return await userConfirmImmobile({
return await userSendConfermaIntent({
annuncioId: input.annuncioId,
servizioId: input.servizioId,
});
}),
userAcceptConfermaImmobile: protectedProcedure
.input(
z.object({
annuncioId: zAnnuncioId,
servizioId: zServizioId,
}),
)
.mutation(async ({ input }) => {
return await userAccettaConferma({
annuncioId: input.annuncioId,
servizioId: input.servizioId,
});

View file

@ -1,6 +1,9 @@
import { TRPCError } from "@trpc/server";
import z from "zod";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import type {
NewUsersStorage,
UsersStorageUserStorageId,
} from "~/schemas/public/UsersStorage";
import {
adminProcedure,
createTRPCRouter,
@ -11,6 +14,7 @@ import {
deleteUserStorageEntry,
getAllWithFromAdmin,
getMultipleWithRelations,
getStorageIdFromUserStorageId,
purgeFileFromUserStorages,
retrieveUserFiles,
} from "~/server/controllers/storage.controller";
@ -83,4 +87,11 @@ export const storageRouter = createTRPCRouter({
.mutation(async ({ input }) => {
return await editFile(input.fileId, input.name);
}),
getStorageIdFromUserStorageId: protectedProcedure
.input(z.object({ userStorageId: z.custom<UsersStorageUserStorageId>() }))
.query(async ({ input }) => {
return await getStorageIdFromUserStorageId({
userStorageId: input.userStorageId,
});
}),
});

View file

@ -237,15 +237,13 @@ export const postAcquistoDataHandler = async ({
user,
anagrafica,
servizio,
skipOrdine = false,
}: {
servizioId: ServizioServizioId;
userId: UsersId;
user: Pick<Users, "nome" | "cognome" | "email" | "telefono">;
anagrafica: UsersAnagraficaUpdate;
servizio: ServizioUpdate;
skipOrdine?: boolean;
}) => {
}): Promise<{ ordineIdForPayment: OrdiniOrdineId | null }> => {
try {
const servizioData = await db.transaction().execute(async (tx) => {
// Update user and anagrafica data
@ -285,16 +283,18 @@ export const postAcquistoDataHandler = async ({
.executeTakeFirstOrThrow();
});
if (skipOrdine) {
if (servizioData.skipPayment) {
// If skipping order, set servizio as active with decorrenza today
await updateServizio({
data: {
decorrenza: new Date(),
isOkAcconto: true,
isOkSaldo: true,
isOkConsulenza: true,
},
servizioId,
});
return null;
return { ordineIdForPayment: null };
}
//cleanup unused payments
@ -350,7 +350,7 @@ export const postAcquistoDataHandler = async ({
.where("ordini.isActive", "=", false)
.where("ordine_id", "!=", existingOrder.ordine_id)
.execute();
return existingOrder.ordine_id;
return { ordineIdForPayment: existingOrder.ordine_id };
}
const order = await db
@ -364,7 +364,7 @@ export const postAcquistoDataHandler = async ({
.returning("ordine_id")
.executeTakeFirstOrThrow();
return order.ordine_id;
return { ordineIdForPayment: order.ordine_id };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
@ -790,44 +790,6 @@ export const updateServizioAnnuncio = async ({
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
//ADMIN CONFERMA LAVORATA EMAIL
if (data.hasConfermaAdmin) {
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`,
title: "Conferma lavorata per il servizio di ricerca affitto",
},
subject: "Conferma lavorata per il servizio di ricerca affitto",
to: utente.email,
userId: utente.id,
});
}
// USER ACCETTA CONFERMA EMAIL
if (data.accettato_conferma_at) {
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha accettato le condizioni di conferma`,
},
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it",
});
}
return true;
} catch (e) {
throw new TRPCError({
@ -1241,7 +1203,7 @@ export const sendServizioEmail = async ({
}
};
export const userConfirmImmobile = async ({
export const userSendConfermaIntent = async ({
servizioId,
annuncioId,
}: {
@ -1314,6 +1276,101 @@ export const userConfirmImmobile = async ({
});
}
};
export const adminUpdateConfermaStatus = async ({
servizioId,
annuncioId,
status,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
status: boolean;
}) => {
try {
await db
.updateTable("servizio_annunci")
.set({
hasConfermaAdmin: status,
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
//ADMIN CONFERMA LAVORATA EMAIL
if (status) {
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `La conferma è stata lavorata con successo. Puoi procedere con le prossime azioni.`,
title: "Conferma lavorata per il servizio di ricerca affitto",
},
subject: "Conferma lavorata per il servizio di ricerca affitto",
to: utente.email,
userId: utente.id,
});
}
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error updating conferma status from annuncio: ${(e as Error).message}`,
});
}
};
export const userAccettaConferma = async ({
servizioId,
annuncioId,
}: {
servizioId: ServizioServizioId;
annuncioId: AnnunciId;
}) => {
try {
await db
.updateTable("servizio_annunci")
.set({
accettato_conferma_at: new Date(),
})
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.execute();
const servizio = await getServizioById(servizioId);
const utente = await getUser({ db, userId: servizio.user_id });
// USER ACCETTA CONFERMA EMAIL
await NewMail({
mailType: "generic",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/admin/user-view/ricerca/${servizio.user_id}#${servizioId}-${annuncioId}`,
label: "Vai al servizio",
},
noreply: true,
testo: `L'utente ha accettato le condizioni di conferma per la locazione. Puoi procedere con le prossime azioni.`,
title: `Utente ${utente.username} ha accettato le condizioni di conferma`,
},
subject: `Utente ${utente.username} ha accettato le condizioni di conferma`,
to: "web@infoalloggi.it",
});
return true;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error accepting conferma from annuncio: ${(e as Error).message}`,
});
}
};
export const isOrdineAwaitingPayment = async (ordineId: OrdiniOrdineId) => {
try {
const ordine = await db
@ -1529,6 +1586,7 @@ export const getCompatibileAnnunci = async (
let qry = db
.selectFrom("annunci")
.distinctOn("annunci.id")
.select((eb) => [
"id",
"codice",

View file

@ -1,6 +1,9 @@
import { TRPCError } from "@trpc/server";
import type { UsersId } from "~/schemas/public/Users";
import type { NewUsersStorage } from "~/schemas/public/UsersStorage";
import type {
NewUsersStorage,
UsersStorageUserStorageId,
} from "~/schemas/public/UsersStorage";
import { db, type Querier } from "~/server/db";
import { fetchFiles, retriveFilesSafely } from "../services/storage.service";
@ -83,6 +86,26 @@ export const retrieveUserFiles = async ({ userId }: { userId: UsersId }) => {
}
};
export const getStorageIdFromUserStorageId = async ({
userStorageId,
}: {
userStorageId: UsersStorageUserStorageId;
}) => {
try {
const storage = await db
.selectFrom("users_storage")
.select("storageId")
.where("user_storage_id", "=", userStorageId)
.executeTakeFirstOrThrow();
return storage.storageId;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: `Error while getting storage ID from user storage ID: ${(e as Error).message}`,
});
}
};
export const getAllWithFromAdmin = async () => {
try {
const files = await db
@ -115,7 +138,7 @@ export const addUserStorage = async (data: NewUsersStorage) => {
.values(data)
.onConflict((oc) => oc.columns(["userId", "storageId"]).doNothing())
.returning("user_storage_id")
.executeTakeFirstOrThrow();
.executeTakeFirst();
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",