diff --git a/apps/infoalloggi/package-lock.json b/apps/infoalloggi/package-lock.json index 276f940..7317115 100644 --- a/apps/infoalloggi/package-lock.json +++ b/apps/infoalloggi/package-lock.json @@ -62,6 +62,7 @@ "nodemailer": "^8.0.1", "nodemailer-html-to-text": "^3.2.0", "nuqs": "^2.8.6", + "pdf-lib": "^1.17.1", "pg": "^8.19.0", "postcss": "^8.5.6", "puppeteer-core": "^24.37.5", @@ -2995,6 +2996,24 @@ "win32" ] }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -11913,6 +11932,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -12037,6 +12062,24 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pdfjs-dist": { "version": "5.4.296", "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", diff --git a/apps/infoalloggi/package.json b/apps/infoalloggi/package.json index 7735748..81e88f2 100644 --- a/apps/infoalloggi/package.json +++ b/apps/infoalloggi/package.json @@ -75,6 +75,7 @@ "nodemailer": "^8.0.1", "nodemailer-html-to-text": "^3.2.0", "nuqs": "^2.8.6", + "pdf-lib": "^1.17.1", "pg": "^8.19.0", "postcss": "^8.5.6", "puppeteer-core": "^24.37.5", diff --git a/apps/infoalloggi/public/ricevuta_basic.pdf b/apps/infoalloggi/public/ricevuta_basic.pdf new file mode 100644 index 0000000..0920168 Binary files /dev/null and b/apps/infoalloggi/public/ricevuta_basic.pdf differ diff --git a/apps/infoalloggi/src/components/area-riservata/ordini.tsx b/apps/infoalloggi/src/components/area-riservata/ordini.tsx index 3e580ac..bd40a01 100644 --- a/apps/infoalloggi/src/components/area-riservata/ordini.tsx +++ b/apps/infoalloggi/src/components/area-riservata/ordini.tsx @@ -14,6 +14,7 @@ import { } from "~/components/custom_ui/credenza"; import { Button } from "~/components/ui/button"; import { FormEditOrder } from "~/forms/FormEditOrdine"; +import { genRicevutaUfficio } from "~/hooks/useRicevutaUfficio"; import { formatCurrency } from "~/lib/utils"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { Prezziario } from "~/schemas/public/Prezziario"; @@ -90,6 +91,34 @@ const AdminOrdiniModal = ({ )} + { + if (!data.isActive || !data.codice_fiscale) return; + genRicevutaUfficio({ + date: + data.paymentmethod !== "manual" && data.paid_at + ? data.paid_at.toLocaleDateString("it-IT") + : data.created_at.toLocaleDateString("it-IT"), + codice_fiscale: data.codice_fiscale, + indirizzo: data.indirizzo, + nominativo: data.username, + pack1_codice: data.packid, + pack1_qnt: "1", + pack1_iva: "22", + pack1_importo: (data.amount_cent / 100) + .toFixed(2) + .replace(".", ","), + totale: formatCurrency(data.amount_cent / 100), + pack1_titolo: + prezzi.find((p) => p.idprezziario === data.packid) + ?.nome_it || "", + }); + }} + variant="orange" + > + Ricevuta Ufficio + sendReceipt({ ordineId: data.ordine_id })} diff --git a/apps/infoalloggi/src/components/tables/richieste-table.tsx b/apps/infoalloggi/src/components/tables/richieste-table.tsx index 8a31c28..752e284 100644 --- a/apps/infoalloggi/src/components/tables/richieste-table.tsx +++ b/apps/infoalloggi/src/components/tables/richieste-table.tsx @@ -6,9 +6,14 @@ import { CircleEllipsis, ClockFading, ExternalLink, + NotebookPen, SearchX, } from "lucide-react"; import Link from "next/link"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import LoadingButton from "~/components/custom_ui/loading-button"; +import { Textarea } from "~/components/custom_ui/textarea"; import { AnnuncioCardContent } from "~/components/servizio/annuncio_card"; import { Badge } from "~/components/ui/badge"; import { Button } from "~/components/ui/button"; @@ -34,7 +39,9 @@ import { import { UserAvatar } from "~/components/user_avatar"; import { cn } from "~/lib/utils"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; +import type { UsersId } from "~/schemas/public/Users"; import type { RichiesteData } from "~/server/controllers/servizio.controller"; +import { api } from "~/utils/api"; type RichiesteTableProps = { data: RichiesteData[]; @@ -45,6 +52,7 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => { const columns_titles = { id: "ID Ordine", username: "Utente", + note: "Note", decorrenza: "Decorrenza", status: "Status", tipologia: "Tipo", @@ -65,6 +73,18 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => { type Column = (typeof tabledata)[number]; + const utils = api.useUtils(); + const { mutate, isPending } = api.users.editUser.useMutation({ + onSuccess: async () => { + toast.success("Note aggiornate con successo"); + await utils.users.invalidate(); + await utils.servizio.invalidate(); + }, + onError: (error) => { + toast.error(error.message); + }, + }); + const columns: ColumnDef[] = [ { accessorKey: "id", @@ -100,6 +120,24 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => { /> ), }, + { + accessorKey: "note", + cell: ({ row }) => { + const note = row.original.note; + return ( + + ); + }, + header: ({ column }) => ( + + ), + enableSorting: false, + }, { accessorKey: "decorrenza", cell: ({ row }) => { @@ -338,3 +376,60 @@ export const RichiesteTable = ({ data }: RichiesteTableProps) => { ); }; + +const NotePopover = ({ + userId, + note, + mutate, + isPending, +}: { + userId: UsersId; + note: string | null; + mutate: (data: { id: UsersId; data: { note: string | null } }) => void; + isPending: boolean; +}) => { + const [newNote, setNewNote] = useState(note); + + return ( + + + + + + + {note && note.trim().length > 0 && ( + + + + )} + + + + + Annunci Inseriti + Desc + + + setNewNote(e.target.value)} + placeholder="" + value={newNote || ""} + /> + mutate({ id: userId, data: { note: newNote } })} + variant="success" + > + Salva + + + + + ); +}; diff --git a/apps/infoalloggi/src/hooks/useRicevutaUfficio.ts b/apps/infoalloggi/src/hooks/useRicevutaUfficio.ts new file mode 100644 index 0000000..90ebd4b --- /dev/null +++ b/apps/infoalloggi/src/hooks/useRicevutaUfficio.ts @@ -0,0 +1,78 @@ +import { degrees, PDFDocument } from "pdf-lib"; + +type RicevutaData = { + codice_fiscale: string; + date: string; + indirizzo: string; + nominativo: string; + pack1_codice: string; + pack1_titolo: string; + pack1_qnt: string; + pack1_importo: string; + pack1_iva: string; + totale: string; +}; +export const genRicevutaUfficio = async (data: RicevutaData) => { + try { + // 1. Fetch the PDF template + const formUrl = "/ricevuta_basic.pdf"; + const formPdfBytes = await fetch(formUrl).then((res) => res.arrayBuffer()); + + // 2. Load the PDFDocument + const pdfDoc = await PDFDocument.load(formPdfBytes); + + // 3. Get the form + const form = pdfDoc.getForm(); + console.log(form.getFields().map((field) => field.getName())); + + // 4. Safely fill fields with Type Casting + // We use try/catch or optional checks in case the field names change in the PDF + const codiceFiscaleField = form.getTextField("codice_fiscale"); + const dataField = form.getTextField("data"); + const indirizzoField = form.getTextField("indirizzo"); + const nominativoField = form.getTextField("nominativo"); + const pack1CodiceField = form.getTextField("pack1_codice"); + const pack1TitoloField = form.getTextField("pack1_titolo"); + const pack1QntField = form.getTextField("pack1_qnt"); + const pack1ImportoField = form.getTextField("undefined.pack1_importo"); + const pack1IvaField = form.getTextField("pack1_iva"); + const totaleField = form.getTextField("undefined.totale"); + + if (codiceFiscaleField) codiceFiscaleField.setText(data.codice_fiscale); + if (dataField) dataField.setText(data.date); + if (indirizzoField) indirizzoField.setText(data.indirizzo); + if (nominativoField) nominativoField.setText(data.nominativo); + if (pack1CodiceField) pack1CodiceField.setText(data.pack1_codice); + if (pack1TitoloField) pack1TitoloField.setText(data.pack1_titolo); + if (pack1QntField) pack1QntField.setText(data.pack1_qnt); + if (pack1ImportoField) pack1ImportoField.setText(data.pack1_importo); + if (pack1IvaField) pack1IvaField.setText(data.pack1_iva); + if (totaleField) totaleField.setText(data.totale); + + // 5. Flatten the form (prevents users from editing the values in the preview) + //form.flatten(); + + const pages = pdfDoc.getPages(); + pages[0]?.setRotation(degrees(180)); + + // 6. Serialize to Uint8Array + const pdfBytes: Uint8Array = await pdfDoc.save(); + + // 7. Create Blob and Open in New Tab + const blob = new Blob([pdfBytes.buffer as ArrayBuffer], { + type: "application/pdf", + }); + const url = URL.createObjectURL(blob); + + const newTab = window.open(url, "_blank"); + + // Focus the new tab if it was blocked or loses focus + if (newTab) { + newTab.focus(); + } else { + alert("Please allow popups to view the PDF"); + } + } catch (error) { + console.error("Error generating PDF:", error); + } +}; diff --git a/apps/infoalloggi/src/lib/utils.ts b/apps/infoalloggi/src/lib/utils.ts index 2596f74..064a754 100644 --- a/apps/infoalloggi/src/lib/utils.ts +++ b/apps/infoalloggi/src/lib/utils.ts @@ -5,6 +5,10 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export const Capitalize = (str: string | null | undefined) => { + if (!str) return ""; + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); +}; export const redirectTo500 = { redirect: { destination: "/500", diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index 98103c9..bd3e91d 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -1448,7 +1448,7 @@ export const getOrdineRicevutaData = async ({ } }; export type RichiesteData = Servizio & - Pick & { + Pick & { annunci_servizio: ServizioAnnuncioData[]; ordini_servizio: (Ordini & Pick)[]; }; @@ -1460,7 +1460,7 @@ export const getRichieste = async (): Promise => { .selectFrom("servizio") .innerJoin("users", "users.id", "servizio.user_id") .selectAll("servizio") - .select(["users.username"]) + .select(["users.username", "users.note"]) .select((eb) => [ jsonArrayFrom( eb diff --git a/apps/infoalloggi/src/server/services/ordini.service.ts b/apps/infoalloggi/src/server/services/ordini.service.ts index b4d6652..98a9aec 100644 --- a/apps/infoalloggi/src/server/services/ordini.service.ts +++ b/apps/infoalloggi/src/server/services/ordini.service.ts @@ -1,5 +1,7 @@ import { TRPCError } from "@trpc/server"; import type { Insertable } from "kysely"; +import { parseDBComune, parseDBNazione } from "~/lib/catasto"; +import { Capitalize } from "~/lib/utils"; import type OrdiniTable from "~/schemas/public/Ordini"; import type { Ordini, @@ -7,6 +9,7 @@ import type { OrdiniUpdate, } from "~/schemas/public/Ordini"; import type { Users, UsersId } from "~/schemas/public/Users"; +import { getComuni, getNazioni } from "~/server/controllers/catasto.controller"; import { db } from "~/server/db"; export type ServizioOrdiniTable = Omit; @@ -43,16 +46,36 @@ export const getOrdini = async ( export const getOrdineById = async (ordineId: OrdiniOrdineId) => { try { - return await db + const data = await db .selectFrom("ordini") .selectAll("ordini") .innerJoin("users", "users.id", "ordini.userid") .select("users.username") + .innerJoin("users_anagrafica", "users_anagrafica.userid", "users.id") + .select([ + "users_anagrafica.codice_fiscale", + "users_anagrafica.via_residenza", + "users_anagrafica.civico_residenza", + "users_anagrafica.comune_residenza", + "users_anagrafica.provincia_residenza", + "users_anagrafica.cap_residenza", + "users_anagrafica.nazione_residenza", + ]) .where("ordini.ordine_id", "=", ordineId) .orderBy("created_at", "desc") .executeTakeFirstOrThrow( () => new Error(`Ordine not found - ordineId: ${ordineId}`), ); + const comuni = await getComuni(); + const nazioni = await getNazioni(); + + const parsedComune = parseDBComune(data.comune_residenza, comuni); + const parsedNazione = parseDBNazione(data.nazione_residenza, nazioni); + + return { + ...data, + indirizzo: `${data.via_residenza} ${data.civico_residenza}, ${Capitalize(parsedComune?.nome)} (${data.provincia_residenza}) ${data.cap_residenza}, ${Capitalize(parsedNazione?.nome)}`, + }; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR",