diff --git a/apps/db/migrations/34_ordiniv2.up.sql b/apps/db/migrations/34_ordiniv2.up.sql new file mode 100644 index 0000000..d435a04 --- /dev/null +++ b/apps/db/migrations/34_ordiniv2.up.sql @@ -0,0 +1,28 @@ +ALTER TABLE IF EXISTS public.servizio +ADD COLUMN "onboardOk" boolean NOT NULL DEFAULT FALSE; + +ALTER TABLE IF EXISTS public.ordini +ADD COLUMN amount_cent integer NOT NULL; + +ALTER TABLE IF EXISTS public.ordini ADD COLUMN paymentmethod TEXT; + +ALTER TABLE IF EXISTS public.ordini +ADD COLUMN paid_at TIMESTAMP WITHOUT TIME ZONE; + +ALTER TABLE IF EXISTS public.ordini ADD COLUMN intent_id TEXT; + +ALTER TABLE IF EXISTS public.ordini +ADD COLUMN paymentstatus "PaymentStatusEnum"; + +ALTER TABLE IF EXISTS public.ordini +ADD COLUMN condizioni TEXT NOT NULL; + +ALTER TABLE IF EXISTS public.ordini +ADD COLUMN sconto integer NOT NULL DEFAULT 0; + +INSERT INTO + public.flags (id, VALUE) +VALUES ( + 'STRIPE_DISABLED'::TEXT, + FALSE::boolean + ); \ No newline at end of file diff --git a/apps/infoalloggi/biome.json b/apps/infoalloggi/biome.json index 25a0187..8b9e013 100644 --- a/apps/infoalloggi/biome.json +++ b/apps/infoalloggi/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.6/schema.json", "assist": { "actions": { "source": { diff --git a/apps/infoalloggi/next.config.ts b/apps/infoalloggi/next.config.ts index 8e98b17..e48cc25 100644 --- a/apps/infoalloggi/next.config.ts +++ b/apps/infoalloggi/next.config.ts @@ -28,6 +28,7 @@ async function createNextConfig(): Promise { scrollRestoration: true, // esmExternals: "loose", // This if react-pdf gives compilation issues }, + allowedDevOrigins: ["host.docker.internal"], generateBuildId: async () => buildId, devIndicators: false, headers: async () => { diff --git a/apps/infoalloggi/package-lock.json b/apps/infoalloggi/package-lock.json index 6b2959e..730fd51 100644 --- a/apps/infoalloggi/package-lock.json +++ b/apps/infoalloggi/package-lock.json @@ -80,7 +80,6 @@ "react-phone-number-input": "^3.4.14", "react-reverse-portal": "^2.3.0", "react-select": "^5.10.2", - "react-to-print": "^3.2.0", "react-use": "^17.6.0", "sharp": "^0.34.5", "stripe": "^20.4.0", @@ -96,11 +95,9 @@ "zod": "^4.1.12" }, "devDependencies": { - "@biomejs/biome": "^2.4.2", + "@biomejs/biome": "^2.4.6", "@hookform/devtools": "^4.4.0", "@react-email/preview-server": "^5.2.8", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@total-typescript/ts-reset": "^0.6.1", "@types/leaflet": "^1.9.21", "@types/node": "^24.2.0", @@ -542,9 +539,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.2.tgz", - "integrity": "sha512-vVE/FqLxNLbvYnFDYg3Xfrh1UdFhmPT5i+yPT9GE2nTUgI4rkqo5krw5wK19YHBd7aE7J6r91RRmb8RWwkjy6w==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.6.tgz", + "integrity": "sha512-QnHe81PMslpy3mnpL8DnO2M4S4ZnYPkjlGCLWBZT/3R9M6b5daArWMMtEfP52/n174RKnwRIf3oT8+wc9ihSfQ==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -558,20 +555,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.4.2", - "@biomejs/cli-darwin-x64": "2.4.2", - "@biomejs/cli-linux-arm64": "2.4.2", - "@biomejs/cli-linux-arm64-musl": "2.4.2", - "@biomejs/cli-linux-x64": "2.4.2", - "@biomejs/cli-linux-x64-musl": "2.4.2", - "@biomejs/cli-win32-arm64": "2.4.2", - "@biomejs/cli-win32-x64": "2.4.2" + "@biomejs/cli-darwin-arm64": "2.4.6", + "@biomejs/cli-darwin-x64": "2.4.6", + "@biomejs/cli-linux-arm64": "2.4.6", + "@biomejs/cli-linux-arm64-musl": "2.4.6", + "@biomejs/cli-linux-x64": "2.4.6", + "@biomejs/cli-linux-x64-musl": "2.4.6", + "@biomejs/cli-win32-arm64": "2.4.6", + "@biomejs/cli-win32-x64": "2.4.6" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.2.tgz", - "integrity": "sha512-3pEcKCP/1POKyaZZhXcxFl3+d9njmeAihZ17k8lL/1vk+6e0Cbf0yPzKItFiT+5Yh6TQA4uKvnlqe0oVZwRxCA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.6.tgz", + "integrity": "sha512-NW18GSyxr+8sJIqgoGwVp5Zqm4SALH4b4gftIA0n62PTuBs6G2tHlwNAOj0Vq0KKSs7Sf88VjjmHh0O36EnzrQ==", "cpu": [ "arm64" ], @@ -586,9 +583,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.2.tgz", - "integrity": "sha512-P7hK1jLVny+0R9UwyGcECxO6sjETxfPyBm/1dmFjnDOHgdDPjPqozByunrwh4xPKld8sxOr5eAsSqal5uKgeBg==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.6.tgz", + "integrity": "sha512-4uiE/9tuI7cnjtY9b07RgS7gGyYOAfIAGeVJWEfeCnAarOAS7qVmuRyX6d7JTKw28/mt+rUzMasYeZ+0R/U1Mw==", "cpu": [ "x64" ], @@ -603,9 +600,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.2.tgz", - "integrity": "sha512-DI3Mi7GT2zYNgUTDEbSjl3e1KhoP76OjQdm8JpvZYZWtVDRyLd3w8llSr2TWk1z+U3P44kUBWY3X7H9MD1/DGQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.6.tgz", + "integrity": "sha512-kMLaI7OF5GN1Q8Doymjro1P8rVEoy7BKQALNz6fiR8IC1WKduoNyteBtJlHT7ASIL0Cx2jR6VUOBIbcB1B8pew==", "cpu": [ "arm64" ], @@ -620,9 +617,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.2.tgz", - "integrity": "sha512-/x04YK9+7erw6tYEcJv9WXoBHcULI/wMOvNdAyE9S3JStZZ9yJyV67sWAI+90UHuDo/BDhq0d96LDqGlSVv7WA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.6.tgz", + "integrity": "sha512-F/JdB7eN22txiTqHM5KhIVt0jVkzZwVYrdTR1O3Y4auBOQcXxHK4dxULf4z43QyZI5tsnQJrRBHZy7wwtL+B3A==", "cpu": [ "arm64" ], @@ -637,9 +634,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.2.tgz", - "integrity": "sha512-GK2ErnrKpWFigYP68cXiCHK4RTL4IUWhK92AFS3U28X/nuAL5+hTuy6hyobc8JZRSt+upXt1nXChK+tuHHx4mA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.6.tgz", + "integrity": "sha512-oHXmUFEoH8Lql1xfc3QkFLiC1hGR7qedv5eKNlC185or+o4/4HiaU7vYODAH3peRCfsuLr1g6v2fK9dFFOYdyw==", "cpu": [ "x64" ], @@ -654,9 +651,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.2.tgz", - "integrity": "sha512-wbBmTkeAoAYbOQ33f6sfKG7pcRSydQiF+dTYOBjJsnXO2mWEOQHllKlC2YVnedqZFERp2WZhFUoO7TNRwnwEHQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.6.tgz", + "integrity": "sha512-C9s98IPDu7DYarjlZNuzJKTjVHN03RUnmHV5htvqsx6vEUXCDSJ59DNwjKVD5XYoSS4N+BYhq3RTBAL8X6svEg==", "cpu": [ "x64" ], @@ -671,9 +668,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.2.tgz", - "integrity": "sha512-k2uqwLYrNNxnaoiW3RJxoMGnbKda8FuCmtYG3cOtVljs3CzWxaTR+AoXwKGHscC9thax9R4kOrtWqWN0+KdPTw==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.6.tgz", + "integrity": "sha512-xzThn87Pf3YrOGTEODFGONmqXpTwUNxovQb72iaUOdcw8sBSY3+3WD8Hm9IhMYLnPi0n32s3L3NWU6+eSjfqFg==", "cpu": [ "arm64" ], @@ -688,9 +685,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.2.tgz", - "integrity": "sha512-9ma7C4g8Sq3cBlRJD2yrsHXB1mnnEBdpy7PhvFrylQWQb4PoyCmPucdX7frvsSBQuFtIiKCrolPl/8tCZrKvgQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.6.tgz", + "integrity": "sha512-7++XhnsPlr1HDbor5amovPjOH6vsrFOCdp93iKXhFn6bcMUI6soodj3WWKfgEO6JosKU1W5n3uky3WW9RlRjTg==", "cpu": [ "x64" ], @@ -5879,55 +5876,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@tiptap/core": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz", @@ -6461,13 +6409,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -6969,16 +6910,6 @@ "node": ">=10" } }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -8194,13 +8125,6 @@ "license": "BSD-3-Clause", "peer": true }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -10962,16 +10886,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -12407,41 +12321,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, "node_modules/prism-react-renderer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", @@ -13478,15 +13357,6 @@ } } }, - "node_modules/react-to-print": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/react-to-print/-/react-to-print-3.2.0.tgz", - "integrity": "sha512-IX2D0mebKMgYTBD6s5tf9B7YRL3RFWjRoevYK8JKgRwn94Rep7PFZyeOTGjCmXofKB1SKzvPSzDrAMG4I2PIwg==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ~19" - } - }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/apps/infoalloggi/package.json b/apps/infoalloggi/package.json index f228e74..f40dc55 100644 --- a/apps/infoalloggi/package.json +++ b/apps/infoalloggi/package.json @@ -93,7 +93,6 @@ "react-phone-number-input": "^3.4.14", "react-reverse-portal": "^2.3.0", "react-select": "^5.10.2", - "react-to-print": "^3.2.0", "react-use": "^17.6.0", "sharp": "^0.34.5", "stripe": "^20.4.0", @@ -109,11 +108,9 @@ "zod": "^4.1.12" }, "devDependencies": { - "@biomejs/biome": "^2.4.2", + "@biomejs/biome": "^2.4.6", "@hookform/devtools": "^4.4.0", "@react-email/preview-server": "^5.2.8", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@total-typescript/ts-reset": "^0.6.1", "@types/leaflet": "^1.9.21", "@types/node": "^24.2.0", @@ -139,4 +136,4 @@ "ct3aMetadata": { "initVersion": "7.9.0" } -} \ No newline at end of file +} diff --git a/apps/infoalloggi/src/components/acquisto_receipt.tsx b/apps/infoalloggi/src/components/acquisto_receipt.tsx index 272a9ad..bdc3ee5 100644 --- a/apps/infoalloggi/src/components/acquisto_receipt.tsx +++ b/apps/infoalloggi/src/components/acquisto_receipt.tsx @@ -3,12 +3,13 @@ import { format } from "date-fns"; import { it } from "date-fns/locale"; import Image from "next/image"; -import { useRef } from "react"; -import { useReactToPrint } from "react-to-print"; +import toast from "react-hot-toast"; import { Button } from "~/components/ui/button"; import { Card } from "~/components/ui/card"; import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe"; import { formatCurrency } from "~/lib/utils"; +import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; +import { api } from "~/utils/api"; export function ReceiptGenerator({ data, @@ -17,11 +18,36 @@ export function ReceiptGenerator({ data: PurchaseData; raw: boolean; }) { - const contentRef = useRef(null); - const reactToPrintFn = useReactToPrint({ - contentRef, - //print: async (target) => {console.log("Printing...", target.contentDocument);}, - }); + const { mutate: generatePdf } = + api.pagamenti.getRicevutaCortesiaPdf.useMutation({ + onMutate: () => { + toast.loading("Generazione PDF in corso...", { + id: "generate-pdf", + }); + }, + onSuccess: (res) => { + toast.success("PDF generato con successo!", { + id: "generate-pdf", + }); + + const blob = new Blob([res as unknown as ArrayBuffer], { + type: "application/pdf", + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `ricevuta-cortesia-${format(data.date, "yyyy-MM-dd")}.pdf`; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + }, + onError: (error) => { + toast.error(`Errore durante la generazione del PDF: ${error.message}`, { + id: "generate-pdf", + }); + }, + }); if (raw) { return ; } @@ -31,12 +57,12 @@ export function ReceiptGenerator({

Ricevuta di cortesia

-
-
- +
+
@@ -52,7 +78,7 @@ interface Item { } export interface PurchaseData { - paymentId: string; + ordineId: OrdiniOrdineId; date: Date; clienteNome: string; @@ -102,8 +128,8 @@ function Receipt({ data }: ReceiptProps) {
-

ID Pagamento

-

{data.paymentId}

+

ID Ordine

+

{data.ordineId}

Data

diff --git a/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx b/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx index 620b4d5..e95293d 100644 --- a/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx +++ b/apps/infoalloggi/src/components/annuncio-interactions/annuncio_interactions.tsx @@ -192,23 +192,13 @@ export const InteressatoButtonServizio = ({ }: { annuncioId: AnnunciId; }) => { - const { isAdmin, servizioId, userId } = useServizio(); + const { isAdmin, servizioId } = useServizio(); const { locale } = useTranslation(); const utils = api.useUtils(); const invalidateAll = async () => { await utils.intrests.getUserInterests.invalidate(); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({servizioId}); - await utils.servizio.getAnnunciAvailableToAdd.invalidate({ - servizioId, - }); - await utils.servizio.AnnuncioServizioTipologiaMatch.invalidate({ - userId, - }); - await utils.servizio.getCompatibileAnnunci.invalidate({ - servizioId, - }); + await utils.servizio.invalidate(); }; const { mutate: adminAdd, isPending: isAdminPending } = api.servizio.addServizioAnnunci.useMutation({ diff --git a/apps/infoalloggi/src/components/annuncio_card.tsx b/apps/infoalloggi/src/components/annuncio_card.tsx index b070d76..650d595 100644 --- a/apps/infoalloggi/src/components/annuncio_card.tsx +++ b/apps/infoalloggi/src/components/annuncio_card.tsx @@ -492,7 +492,10 @@ export const CarouselAnnuncio = ({ media: "image", }, })} - key={`videoplayer-${idx}-${openModal}`} + key={`videoplayer-${ + // biome-ignore lint/suspicious/noArrayIndexKey: + idx + }-${openModal}`} videoSrc={getStorageUrl({ storageId: video.video, params: { diff --git a/apps/infoalloggi/src/components/area-riservata/ordine_manuale.tsx b/apps/infoalloggi/src/components/area-riservata/ordine_manuale.tsx deleted file mode 100644 index 9672c29..0000000 --- a/apps/infoalloggi/src/components/area-riservata/ordine_manuale.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useState } from "react"; -import { FormNewOrder } from "~/forms/FormNewOrdine"; -import type { ServizioServizioId } from "~/schemas/public/Servizio"; -import type { UsersId } from "~/schemas/public/Users"; -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"; - -export const OrdineManualeDialog = ({ - userId, - servizioId, -}: { - userId: UsersId; - servizioId: ServizioServizioId; -}) => { - const [open, setOpen] = useState(false); - return ( - - - - - - - Nuovo Ordine Manuale - - nuovo ordine modal - - - - - - ); -}; - -const Content = ({ - userId, - servizioId, - setOpen, -}: { - userId: UsersId; - servizioId: ServizioServizioId; - setOpen: React.Dispatch>; -}) => { - const { data, isLoading } = api.prezziario.getPrezziario.useQuery(); - - if (isLoading) return ; - - if (!data) - return ( - <> - -

- Errore nel caricamento del prezziario. -

-
- - - - - - - - ); - - return ( - - ); -}; diff --git a/apps/infoalloggi/src/components/area-riservata/ordini.tsx b/apps/infoalloggi/src/components/area-riservata/ordini.tsx index a9a3b47..ccb8e41 100644 --- a/apps/infoalloggi/src/components/area-riservata/ordini.tsx +++ b/apps/infoalloggi/src/components/area-riservata/ordini.tsx @@ -1,7 +1,6 @@ -import { CircleCheck, CircleSlash, ReceiptText, Trash2 } from "lucide-react"; +import { CircleCheck, CircleSlash, ReceiptText } from "lucide-react"; import Link from "next/link"; -import toast from "react-hot-toast"; -import { Confirm } from "~/components/confirm"; +import type { Dispatch, SetStateAction } from "react"; import { Credenza, CredenzaBody, @@ -12,87 +11,93 @@ import { CredenzaHeader, CredenzaTitle, } from "~/components/custom_ui/credenza"; -import { PaymentsTable } from "~/components/tables/payments-table"; import { Button } from "~/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; -import { cn } from "~/lib/utils"; -import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; +import { FormEditOrder } from "~/forms/FormEditOrdine"; +import { formatCurrency } from "~/lib/utils"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; -import type { UsersId } from "~/schemas/public/Users"; +import type { Prezziario } from "~/schemas/public/Prezziario"; import { api } from "~/utils/api"; -export const OrdiniModal = ({ - isAdmin, - open, - setModalOpen, +export const AdminWrapperOrdiniModal = ({ ordineId, - userId, + setSelected, }: { - isAdmin: boolean; - open: boolean; - setModalOpen: (open: boolean) => void; - ordineId: OrdiniOrdineId; - userId: UsersId | undefined; + ordineId: OrdiniOrdineId | null; + setSelected: Dispatch>; }) => { - const utils = api.useUtils(); - const { mutate: remove } = api.servizio.removeOrder.useMutation({ - onError: (error) => { - toast.error(`Errore durante la rimozione ordine: ${error.message}`); - }, - onSuccess: async () => { - toast.success("Ordine rimosso con successo!"); - await utils.servizio.getOrdini.invalidate({ - userId, - }); - setModalOpen(false); - }, - }); - const { mutate: update } = api.servizio.updateOrder.useMutation({ - onError: (error) => { - toast.error(`Errore durante la modifica: ${error.message}`); - }, - onSuccess: async (data) => { - if (data.type === OrderTypeEnum.Acconto && data.isActive) { - updateServizio({ - data: { - decorrenza: new Date(), - isOkAcconto: true, - }, - servizioId: data.servizio_id, - }); - } - toast.success("Ordine aggiornato con successo!"); - await utils.servizio.getOrdini.invalidate({ - userId, - }); - await utils.servizio.getOrdineById.invalidate({ - ordineId, - }); - }, + const { data: prezzi, isLoading } = api.prezziario.getPrezziario.useQuery(); + if (isLoading || !ordineId || !prezzi) return null; + return ( + + ); +}; +const AdminOrdiniModal = ({ + prezzi, + setSelected, + ordineId, +}: { + prezzi: Prezziario[]; + setSelected: Dispatch>; + ordineId: OrdiniOrdineId; +}) => { + const { data } = api.servizio.getOrdineById.useQuery({ + ordineId, }); - const { mutate: updateServizio } = api.servizio.updateServizio.useMutation({ - onError: (error) => { - toast.error(error.message); - }, - onSuccess: async (data) => { - toast.success("Servizio modificato con successo"); - await utils.servizio.getUserServizi.invalidate(); - await utils.servizio.getPacksSolution.invalidate({ - servizioId: data.servizio_id, - }); - }, - }); + if (!data) return null; + return ( + { + if (!b) setSelected(null); + }} + open={ordineId !== null} + > + + + Dettagli Ordine + + Visualizza i dettagli e i pagamenti associati. + + + +

Id Ordine: {data.ordine_id}

+ {data.isActive && + data.paymentstatus === PaymentStatusEnum.success && ( +
+ + + +
+ )} + setSelected(null)} + data={data} + prezziario={prezzi} + /> +
+
+
+ ); +}; + +export const UserOrdiniModal = ({ + setSelected, + ordineId, +}: { + setSelected: (ordineId: OrdiniOrdineId | null) => void; + ordineId: OrdiniOrdineId; +}) => { const { data } = api.servizio.getOrdineById.useQuery({ ordineId, }); @@ -101,9 +106,9 @@ export const OrdiniModal = ({ return ( { - setModalOpen(b); + if (!b) setSelected(null); }} - open={open} + open={ordineId !== null} > @@ -115,12 +120,7 @@ export const OrdiniModal = ({

Id Ordine: {data.ordine_id}

- Data Ordine: - {new Date(data.created_at).toLocaleDateString("it-IT", { - day: "2-digit", - month: "2-digit", - year: "numeric", - })} + Data Ordine: {new Date(data.created_at).toLocaleDateString("it-IT")}

Tipologia: {data.type.toString()}

Pack ID: {data.packid}

@@ -138,11 +138,12 @@ export const OrdiniModal = ({ )}

- +

+ Importo: {formatCurrency(data.amount_cent / 100)}{" "} + {data.sconto > 0 && ({data.sconto}%)} +

{data.isActive && - data.pagamenti.find( - (p) => p.paymentstatus === PaymentStatusEnum.success, - ) && ( + data.paymentstatus === PaymentStatusEnum.success && (
)} - -
- - {isAdmin && ( -
- - - - - - Status - - { - update({ - data: { isActive: v === "true" }, - ordineId, - }); - }} - value={data.isActive ? "true" : "false"} - > - - Attivo - - - Non Attivo - - - - - - { - remove({ ordineId }); - }} - title="Elimina ordine" - > - - -
- )} + diff --git a/apps/infoalloggi/src/components/chat/chat-attachments.tsx b/apps/infoalloggi/src/components/chat/chat-attachments.tsx index 0ee8c6a..cc65b4d 100644 --- a/apps/infoalloggi/src/components/chat/chat-attachments.tsx +++ b/apps/infoalloggi/src/components/chat/chat-attachments.tsx @@ -93,7 +93,10 @@ export const ChatAttachments = ({ return (
  • + idx + }`} > {children} )} - + +
    + +
    {title} - {description} + + {description} +
    onCancel?.()}> {cancelText || "Annulla"} - onConfirm()} - > + onConfirm()} variant="destructive"> {confirmText || "Conferma"} diff --git a/apps/infoalloggi/src/components/custom_ui/dataTable-ViewToggle.tsx b/apps/infoalloggi/src/components/custom_ui/dataTable-ViewToggle.tsx index 731e4e7..679cada 100644 --- a/apps/infoalloggi/src/components/custom_ui/dataTable-ViewToggle.tsx +++ b/apps/infoalloggi/src/components/custom_ui/dataTable-ViewToggle.tsx @@ -38,7 +38,7 @@ export function DataTableViewOptions({ Filtro - + Filtra colonne {hidableColumns.map((column) => { diff --git a/apps/infoalloggi/src/components/pdf-viewer.tsx b/apps/infoalloggi/src/components/pdf-viewer.tsx index 8dcc9f7..d07b329 100644 --- a/apps/infoalloggi/src/components/pdf-viewer.tsx +++ b/apps/infoalloggi/src/components/pdf-viewer.tsx @@ -78,7 +78,10 @@ export const PDFViewer = ({ url }: { url: string }) => { > {Array.from(new Array(numPages), (_el, index) => ( + index + 1 + }`} pageNumber={index + 1} width={width} /> diff --git a/apps/infoalloggi/src/components/servizio/admin_conferma.tsx b/apps/infoalloggi/src/components/servizio/admin_conferma.tsx index 36ff685..579774a 100644 --- a/apps/infoalloggi/src/components/servizio/admin_conferma.tsx +++ b/apps/infoalloggi/src/components/servizio/admin_conferma.tsx @@ -52,7 +52,6 @@ import { } from "../ui/card"; import { Label } from "../ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { Switch } from "../ui/switch"; import { UploadModal } from "../upload_modal"; import { ServizioPacksInfos } from "./servizio_actions"; import { DocCombo, DocInfo } from "./shared"; @@ -106,7 +105,7 @@ export const AdminLavoraConferma = () => { }; const DebugActions = () => { - const { servizioId, userId } = useServizio(); + const { servizioId } = useServizio(); const { annuncioId } = useServizioAnnuncio(); const utils = api.useUtils(); const { mutate } = api.servizio.updateServizioAnnunci.useMutation({ @@ -114,10 +113,7 @@ const DebugActions = () => { toast.error(error.message); }, onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ - userId: userId, - }); - await utils.servizio.getServizioData.invalidate({ servizioId }); + await utils.servizio.invalidate(); toast.success("Dati salvati con successo"); }, @@ -166,6 +162,10 @@ const DebugActions = () => { const DocSection = () => { const { userId } = useServizio(); const { data, updateServizioAnnunci } = useServizioAnnuncio(); + const initialDoc = data.doc_conferma_ref; + const [selectedDoc, setSelectedDoc] = useState( + data.doc_conferma_ref, + ); const utils = api.useUtils(); @@ -181,6 +181,18 @@ const DocSection = () => { }, }); + const handleSave = async () => { + await updateServizioAnnunci({ + doc_conferma_added: selectedDoc != null, + doc_conferma_ref: selectedDoc, + }); + if (selectedDoc) { + setUserStorage({ + storageId: selectedDoc, + userId, + }); + } + }; if (isLoading) return ; if (files === undefined) { return

    Errore nel caricamento dei documenti

    ; @@ -194,55 +206,69 @@ const DocSection = () => { - {data.doc_conferma_ref && ( -
    - + {selectedDoc && ( +
    +
    )} -
    - { - await updateServizioAnnunci({ - doc_conferma_added: true, - doc_conferma_ref: f.id, - }); - - setUserStorage({ - storageId: f.id, - userId, - }); - }} - /> - { - if (storageIds.length === 1 && storageIds[0]) { - await updateServizioAnnunci({ - doc_conferma_added: true, - doc_conferma_ref: storageIds[0], - }); - } - }} - isAdmin - userId={userId} - /> -
    + { + setSelectedDoc(f.id); + }} + /> + { + if (storageIds.length === 1 && storageIds[0]) { + setSelectedDoc(storageIds[0]); + } + }} + isAdmin + userId={userId} + /> + + + {selectedDoc !== initialDoc ? ( +
    + + Salva modifiche +
    + ) : ( + initialDoc && + selectedDoc === initialDoc && ( +
    + + Dati salvati +
    + ) + )} +
    ); }; @@ -539,7 +565,7 @@ const TransitorioSection = () => { const { servizio } = useServizio(); const { data } = useServizioAnnuncio(); const { t } = useTranslation(); - const { servizioId, userId } = useServizio(); + const { servizioId } = useServizio(); const utils = api.useUtils(); const { mutate: update } = api.servizio.updateServizio.useMutation({ onError: (error) => { @@ -547,10 +573,7 @@ const TransitorioSection = () => { }, onSuccess: async () => { toast.success("Servizio modificato con successo"); - await utils.servizio.getUserServizi.invalidate(); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); - await utils.servizio.getPacksSolution.invalidate({ servizioId }); + await utils.servizio.invalidate(); }, }); @@ -604,7 +627,7 @@ const TransitorioSection = () => { }; const StabileSection = () => { - const { servizio, servizioId, userId } = useServizio(); + const { servizio, servizioId } = useServizio(); const { data } = useServizioAnnuncio(); const [value, setValue] = useState(servizio.budget); const utils = api.useUtils(); @@ -614,10 +637,7 @@ const StabileSection = () => { }, onSuccess: async () => { toast.success("Servizio modificato con successo"); - await utils.servizio.getUserServizi.invalidate(); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); - await utils.servizio.getPacksSolution.invalidate({ servizioId }); + await utils.servizio.invalidate(); }, }); @@ -664,7 +684,7 @@ const GeneralSection = () => { const { data } = useServizioAnnuncio(); const { t } = useTranslation(); - const { servizio, servizioId, userId } = useServizio(); + const { servizio, servizioId } = useServizio(); const utils = api.useUtils(); const { mutate: update } = api.servizio.updateServizio.useMutation({ onError: (error) => { @@ -672,10 +692,7 @@ const GeneralSection = () => { }, onSuccess: async () => { toast.success("Servizio modificato con successo"); - await utils.servizio.getUserServizi.invalidate(); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); - await utils.servizio.getPacksSolution.invalidate({ servizioId }); + await utils.servizio.invalidate(); }, }); @@ -795,11 +812,8 @@ const SendConferma = () => { const { mutate: handleUpdate } = api.servizio.adminUpdateConfermaStatus.useMutation({ onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate(); - await utils.servizio.getServizioData.invalidate({ - servizioId: servizio.servizio_id, - }); - + await utils.servizio.invalidate(); + toast.success("Status aggiornato con successo"); }, onError: (error) => { @@ -848,24 +862,28 @@ const SendConferma = () => { completi

    )} +

    + ATTENZIONE: Il saldo verrà aggiunto (se necessario), in base ai + parametri, all'invio della conferma +

    - { +
    diff --git a/apps/infoalloggi/src/components/servizio/admin_contratto.tsx b/apps/infoalloggi/src/components/servizio/admin_contratto.tsx index a496f41..43cefa1 100644 --- a/apps/infoalloggi/src/components/servizio/admin_contratto.tsx +++ b/apps/infoalloggi/src/components/servizio/admin_contratto.tsx @@ -278,7 +278,7 @@ const ContrattoImport = () => { desc -
    +
    ); }; @@ -277,7 +273,7 @@ const PostConfermaSection = () => { } if (!data.doc_contratto_added) { return ( -
    +

    {locale === "it" ? "Ci siamo quasi! Stiamo preparando il contratto per te." @@ -303,16 +299,17 @@ const PostConfermaSection = () => { ? "Il contratto è pronto, puoi visualizzarlo e procedere al saldo:" : "The contract is ready, you can view it and proceed to payment:"}

    -
    +
    {data.doc_contratto_ref ? ( - + + ); }; @@ -435,47 +417,27 @@ const SaldoConsulenzaButton = ({ className?: string; }) => { const { locale } = useTranslation(); - const { annuncioId } = useServizioAnnuncio(); const { servizioId } = useServizio(); - const router = useRouter(); - const { mutate } = api.servizio.setupSaldoConsulenzaServizio.useMutation({ - onMutate: () => { - toast.loading(locale === "it" ? "Caricamento ..." : "Loading ...", { - id: "preparazioneSaldo", - }); - }, - onSuccess: async (data) => { - toast.success( - locale === "it" ? "Pagamento saldo preparato" : "Payment prepared", - { - id: "preparazioneSaldo", - }, - ); - - await router.push(`/servizio/pagamento/${data}`); - }, - }); return ( - + + ); }; diff --git a/apps/infoalloggi/src/components/servizio/annuncio_card.tsx b/apps/infoalloggi/src/components/servizio/annuncio_card.tsx index fcdf161..ac0faba 100644 --- a/apps/infoalloggi/src/components/servizio/annuncio_card.tsx +++ b/apps/infoalloggi/src/components/servizio/annuncio_card.tsx @@ -102,7 +102,7 @@ export const AnnuncioDisplay = ({ return (
    diff --git a/apps/infoalloggi/src/components/servizio/conferma.tsx b/apps/infoalloggi/src/components/servizio/conferma.tsx index 4ea016b..f31b06d 100644 --- a/apps/infoalloggi/src/components/servizio/conferma.tsx +++ b/apps/infoalloggi/src/components/servizio/conferma.tsx @@ -90,7 +90,7 @@ export const ConfermaAnnuncioModal = () => { }; const InvioConferma = ({ closeModal }: { closeModal: () => void }) => { - const { userId, servizioId } = useServizio(); + const { servizioId } = useServizio(); const { annuncioId } = useServizioAnnuncio(); const { t } = useTranslation(); const utils = api.useUtils(); @@ -107,8 +107,7 @@ const InvioConferma = ({ closeModal }: { closeModal: () => void }) => { id: "updateRichiestaToast", }); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); + await utils.servizio.invalidate(); closeModal(); }, }, @@ -117,7 +116,11 @@ const InvioConferma = ({ closeModal }: { closeModal: () => void }) => { return ( <> +

    + Hai fatto la visita? Se no contatta il proprietario! +

    {t.richieste.conferma_txt}

    +

    {t.richieste.conf_alert}

    @@ -130,9 +133,9 @@ const InvioConferma = ({ closeModal }: { closeModal: () => void }) => { servizioId, }); }} - variant="success" + variant="info" > - {t.conferma} + {t.procedi} @@ -145,17 +148,11 @@ const InvioConferma = ({ closeModal }: { closeModal: () => void }) => { const DocCheckpoint = () => { const { t, locale } = useTranslation(); - const { userId, servizio, isAdmin, servizioId } = useServizio(); + const { userId, servizio, isAdmin } = useServizio(); const utils = api.useUtils(); const { mutate, isPending } = api.servizio.updateServizio.useMutation({ onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ - userId, - }); - await utils.servizio.getServizioData.invalidate({ servizioId }); - await utils.storage.retrieveUserFileData.invalidate({ userId }); - - await utils.servizio.getPacksSolution.invalidate({ servizioId }); + await utils.servizio.invalidate(); }, onError: (err) => { toast.error(`Errore aggiornamento servizio: ${err.message}`); diff --git a/apps/infoalloggi/src/components/servizio/modale_contatti.tsx b/apps/infoalloggi/src/components/servizio/modale_contatti.tsx index 2ac65c6..47c1cb2 100644 --- a/apps/infoalloggi/src/components/servizio/modale_contatti.tsx +++ b/apps/infoalloggi/src/components/servizio/modale_contatti.tsx @@ -87,12 +87,7 @@ const DocCheckpoint = () => { const utils = api.useUtils(); const { mutate, isPending } = api.users.editUserAnagrafica.useMutation({ onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ - userId, - }); - await utils.servizio.getServizioData.invalidate({ - servizioId: servizio.servizio_id, - }); + await utils.servizio.invalidate(); await utils.storage.retrieveUserFileData.invalidate({ userId }); }, onError: (err) => { @@ -183,7 +178,7 @@ const DocCheckpoint = () => { }; const PreUnlock = () => { - const { userId, servizioId } = useServizio(); + const { servizioId } = useServizio(); const { annuncioId } = useServizioAnnuncio(); const { t, locale } = useTranslation(); @@ -211,10 +206,7 @@ const PreUnlock = () => { toast.success("Successo", { id: "unlock-contacts", }); - await utils.servizio.getAllServizioAnnunci.invalidate({ - userId, - }); - await utils.servizio.getServizioData.invalidate({ servizioId }); + await utils.servizio.invalidate(); }, }, ); diff --git a/apps/infoalloggi/src/components/servizio/servizi_header.tsx b/apps/infoalloggi/src/components/servizio/servizi_header.tsx index 8383175..1716846 100644 --- a/apps/infoalloggi/src/components/servizio/servizi_header.tsx +++ b/apps/infoalloggi/src/components/servizio/servizi_header.tsx @@ -54,8 +54,7 @@ const NewServizioModal = ({ }, onSuccess: async () => { toast.success("Servizio creato con successo"); - await utils.servizio.getUserServizi.invalidate(); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); + await utils.servizio.invalidate(); setEditData(undefined); setOpen(false); @@ -66,20 +65,9 @@ const NewServizioModal = ({ onError: (error) => { toast.error(error.message); }, - onSuccess: async (data) => { + onSuccess: async () => { toast.success("Servizio modificato con successo"); - await utils.servizio.getUserServizi.invalidate(); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ - servizioId: data.servizio_id, - }); - - await utils.servizio.getCompatibileAnnunci.invalidate({ - servizioId: data.servizio_id, - }); - await utils.servizio.getPacksSolution.invalidate({ - servizioId: data.servizio_id, - }); + await utils.servizio.invalidate(); setEditData(undefined); setOpen(false); }, diff --git a/apps/infoalloggi/src/components/servizio/servizio.tsx b/apps/infoalloggi/src/components/servizio/servizio.tsx index 7370bb9..1abfac2 100644 --- a/apps/infoalloggi/src/components/servizio/servizio.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio.tsx @@ -1,12 +1,14 @@ import { add } from "date-fns"; import { ArrowRight, + BadgeCheck, CalendarClock, CircleCheck, CircleCheckBig, CircleMinus, Clock, ClockFading, + ExternalLink, type LucideIcon, PackageCheck, Trash2, @@ -20,11 +22,23 @@ import { ServizioProvider, useServizio, } from "~/providers/ServizioProvider"; +import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; 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 { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "../ui/alert-dialog"; import { Button } from "../ui/button"; import { Card, @@ -36,11 +50,7 @@ import { import { SaldoButton } from "./annuncio_actions"; import { AnnuncioCard, AnnuncioDisplay } from "./annuncio_card"; import { AnnunciCompatibili } from "./compatibili_dialog"; -import { - AdminActions, - ServizioBasicActions, - ServizioPacksInfos, -} from "./servizio_actions"; +import { ServizioInfos } from "./servizio_actions"; export const ServizioList = ({ servizi, @@ -189,18 +199,18 @@ const Content = () => { 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 (
    -

    +

    {t.servizio.servizio_titolo} {servizio.tipologia}

    @@ -217,51 +227,32 @@ const Content = () => { {t.servizio.durata_servizio}
    - {dec_txt} - {end_txt} + {servizio.decorrenza !== null + ? `${dec_txt} - ${end_txt}` + : "In attesa di attivazione"}
    -
    - - -
    +
    - +
    ); }; const Main = () => { - const { servizio, isAdmin, userId, servizioId } = useServizio(); + const { servizio, isAdmin } = 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.getServizioData.invalidate({ servizioId }); - 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"); + const { mutate: attivazioneSenzaPagamento } = + api.servizio.attivazioneSenzaPagamento.useMutation({ + onError: () => { + toast.error("Errore durante l'attivazione del servizio"); }, onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); - await utils.servizio.getCompatibileAnnunci.invalidate({ - servizioId, - }); - toast.success("Annuncio rimosso"); + toast.success("Servizio modificato con successo"); + await utils.servizio.invalidate(); }, }); @@ -273,93 +264,120 @@ const Main = () => { ); } - - if (!servizio.isOkAcconto || servizio.decorrenza === null) { + if (!servizio.onboardOk) { return ( -
    +

    {t.servizio.non_attivo}

    +
    - {isAdmin && ( -
    + ); + } + if (!servizio.isOkAcconto || servizio.decorrenza === null) { + const acconti = servizio.ordini.filter( + (o) => o.type === OrderTypeEnum.Acconto, + ); + + const activeAcconto = acconti.find((a) => a.isActive); + + return ( +
    +
    + +

    Parametri di ricerca salvati

    + {activeAcconto ? ( + + + + + + +
    + +
    + + Attivazione del Servizio + + + Procedendo all'attivazione del servizio, confermi che i + parametri di ricerca salvati sono corretti e accetti di + essere contattato per eventuali verifiche. + + + {activeAcconto?.testo_condizioni && ( +
    + + + +

    + Riceverai copia delle condizioni anche via email +

    +
    + )} +
    + + {t.annulla} + { + attivazioneSenzaPagamento({ + ordineId: activeAcconto.ordine_id, + }); + }} + > + Attiva il servizio + + + +
    +
    + ) : ( + - updateServizio({ - data: { - decorrenza: new Date(), - isOkAcconto: true, - }, - servizioId: servizio.servizio_id, - }) - } - variant="success" + href={`/servizio/pagamento/${servizio.servizio_id}?type=${OrderTypeEnum.Acconto}`} > - Attiva Manualmente (Azione Admin) - + + )}
    - {servizio.annunci.length > 0 && ( -
    -

    - {t.servizio.per_te_1} {servizio.annunci.length}{" "} - {t.servizio.per_te_2} -

    -
    - {servizio.annunci.map((data) => ( -
    - - {isAdmin && ( - { - removeAnnuncioServizio({ - annuncioId: data.annunci_id, - servizioId: servizio.servizio_id, - }); - }} - title="Rimuovi Annuncio" - > - - - )} -
    - ))} - {servizio.annunci.length % 2 === 1 && ( -
    - )} -
    -
    - )} +
    ); } @@ -390,7 +408,6 @@ const Main = () => { data={data} key={data.id} servizioId={servizio.servizio_id} - userId={userId} > @@ -434,26 +451,98 @@ const Main = () => { ))}
    )} -
    +
    {servizio.annunci.map((data) => { return ( ); })} {servizio.annunci.length % 2 === 1 && ( -
    +
    )}
    -
    +
    + {servizio.annunci.length === 0 && ( +

    Cerca un annuncio ora!

    + )}
    ); }; + +const AnnunciAnteprima = () => { + const { servizio, isAdmin } = useServizio(); + const { t } = useTranslation(); + const utils = api.useUtils(); + + const { mutate: removeAnnuncioServizio } = + api.servizio.removeAnnuncioServizio.useMutation({ + onError: () => { + toast.error("Errore durante la rimozione dell'annuncio dal servizio"); + }, + onSuccess: async () => { + await utils.servizio.invalidate(); + toast.success("Annuncio rimosso"); + }, + }); + + if (servizio.annunci.length > 0) { + return ( +
    +

    + {t.servizio.per_te_1} {servizio.annunci.length} {t.servizio.per_te_2} +

    +
    + {servizio.annunci.map((data) => ( +
    + + {isAdmin && ( + { + removeAnnuncioServizio({ + annuncioId: data.annunci_id, + servizioId: servizio.servizio_id, + }); + }} + title="Rimuovi Annuncio" + > + + + )} +
    + ))} + {servizio.annunci.length % 2 === 1 && ( +
    + )} +
    +
    + ); + } + return null; +}; diff --git a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx index b765f73..a78e0d8 100644 --- a/apps/infoalloggi/src/components/servizio/servizio_actions.tsx +++ b/apps/infoalloggi/src/components/servizio/servizio_actions.tsx @@ -6,12 +6,14 @@ import { Cog, Euro, ExternalLink, + Logs, Plus, Search, SlidersHorizontal, Trash2, } from "lucide-react"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useState } from "react"; import toast from "react-hot-toast"; import { @@ -55,6 +57,7 @@ import { type FormValues as EditFormValues, FormEditServizio, } from "~/forms/FormEditServizioAdmin"; +import { FormNewOrder } from "~/forms/FormNewOrdine"; import { FormNewServizio, type FormValues } from "~/forms/FormNewServizio"; import { cn, formatCurrency } from "~/lib/utils"; import { useTranslation } from "~/providers/I18nProvider"; @@ -65,27 +68,76 @@ import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import type { UsersId } from "~/schemas/public/Users"; import { api } from "~/utils/api"; import { Confirm } from "../confirm"; +import { Separator } from "../ui/separator"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../ui/table"; -export const AdminActions = () => { +export const ServizioInfos = () => { + return ( +
    + + + +
    + ); +}; + +const ServizioActions = () => { + const { isAdmin } = useServizio(); + return ( + + + + + + + Azioni Servizio + + Azioni disponibili per questo servizio + + +
    + {isAdmin && ( +
    + Azioni Admin + +
    + )} + + {isAdmin && ( +
    + Azioni Utente + +
    + )} + +
    +
    +
    + ); +}; + +const AdminActions = () => { const { servizio, isAdmin, userId } = useServizio(); if (!isAdmin) return null; return (
    - - -
    ); }; -export const ServizioBasicActions = ({ className }: { className?: string }) => { +const UserActions = ({ className }: { className?: string }) => { const { servizio, isAdmin } = useServizio(); const canUserEdit = servizio.isOkAcconto && servizio.decorrenza !== null; @@ -106,6 +158,229 @@ export const ServizioBasicActions = ({ className }: { className?: string }) => { return null; }; +const ServizioPrezzo = () => { + const { servizio } = useServizio(); + const { t } = useTranslation(); + const { data: packs } = api.servizio.getPacksSolution.useQuery({ + servizioId: servizio.servizio_id, + }); + if (!packs) return null; + return ( + + + + + + + Prezzi Servizio + + Prezzi disponibili per questo servizio + + +
    + + {t.servizio.prezzi.sub} + +
    +
    + {packs.acconto.success ? ( + <> + + {packs.acconto.pack.nome_it} + + + {packs.acconto.pack.desc_it} + + + {formatCurrency(packs.acconto.pack.prezzo_cent / 100)} + + + COD: {packs.acconto.pack.idprezziario} + + + ) : ( + {packs.acconto.message} + )} +
    + +
    + {packs.saldo.success ? ( + <> + + {packs.saldo.pack.nome_it} + + + {packs.saldo.pack.desc_it} + + + {formatCurrency(packs.saldo.pack.prezzo_cent / 100)} + + + COD: {packs.saldo.pack.idprezziario} + + + ) : ( + {packs.saldo.message} + )} +
    + {servizio.tipologia === TipologiaPosizioneEnum.Stabile && ( +
    + <> + + {t.servizio.prezzi.consulenza} + + + {t.servizio.prezzi.consulenza_desc} + + + + {t.servizio.prezzi.gotoprezzi} + + + + +
    + )} +
    +
    +
    +
    + ); +}; +const ServizioOrders = () => { + const { isAdmin, servizio } = useServizio(); + const [openOrders, setOpenOrders] = useState(false); + const [openEdit, setOpenEdit] = useState(false); + + const swapToEdit = () => { + setOpenOrders(false); + setOpenEdit(true); + }; + + const swapToOrders = () => { + setOpenEdit(false); + setOpenOrders(true); + }; + + return ( + <> + + + + + + + Ordini Servizio + + Ordini disponibili per questo servizio + + {isAdmin && ( + + )} + +
    + + + + Data + ID + Importo + Status + + + + + {servizio.ordini.length === 0 && ( + + + + )} + {servizio.ordini.map((ordine) => ( + + + {ordine.created_at.toLocaleDateString("it-IT")} + + + {ordine.packid} + + + {formatCurrency(ordine.amount_cent / 100)} + + + {ordine.isActive ? "Attivo" : "Non attivo"} + + + + + + + + ))} + +
    + Nessun ordine disponibile +
    +
    +
    +
    + + + + Nuovo Ordine + nuovo + + + + + + ); +}; + +const AddOrderDialogContent = ({ swapBack }: { swapBack: () => void }) => { + const { servizioId, userId } = useServizio(); + const { data, isLoading } = api.prezziario.getPrezziario.useQuery(); + + if (isLoading) return ; + + if (!data) + return ( + <> + Errore nel caricamento del prezziario. + + ); + + return ( + + ); +}; + export const ServizioPacksInfos = () => { const { servizio } = useServizio(); const { t } = useTranslation(); @@ -203,7 +478,6 @@ export const ServizioPacksInfos = () => { const AddAnnuncio = ({ servizioId, - userId, }: { servizioId: ServizioServizioId; userId: UsersId; @@ -226,15 +500,7 @@ const AddAnnuncio = ({ toast.error("Errore durante l'aggiunta dell'annuncio"); }, onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); - await utils.servizio.getAnnunciAvailableToAdd.invalidate({ - servizioId, - }); - await utils.servizio.getCompatibileAnnunci.invalidate({ - servizioId, - }); - await utils.servizio.getUserServizi.invalidate({ userId }); + await utils.servizio.invalidate(); toast.success("Annuncio aggiunto"); setOpenModal(false); setSelectedAnnunci([]); @@ -319,7 +585,7 @@ const AddAnnuncio = ({ }; const InterruzioneServizio = () => { - const { servizio, userId, isAdmin } = useServizio(); + const { servizio, isAdmin } = useServizio(); const { t } = useTranslation(); const beforeInterruzioneLimit = servizio.decorrenza != null && @@ -335,10 +601,7 @@ const InterruzioneServizio = () => { toast.error("Errore durante la richiesta di interruzione"); }, onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ - servizioId: servizio.servizio_id, - }); + await utils.servizio.invalidate(); toast.success("Interruzione richiesta"); }, }); @@ -381,7 +644,7 @@ const InterruzioneServizio = () => { }; const EditParametri = () => { - const { servizioId, userId, isAdmin, servizio } = useServizio(); + const { servizioId, isAdmin, servizio } = useServizio(); const { t } = useTranslation(); const [openEditParametri, setOpenEditParametri] = useState(false); @@ -393,13 +656,8 @@ const EditParametri = () => { }, onSuccess: async () => { toast.success("Servizio modificato con successo"); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); + await utils.servizio.invalidate(); - await utils.servizio.getCompatibileAnnunci.invalidate({ - servizioId, - }); - await utils.servizio.getPacksSolution.invalidate({ servizioId }); setOpenEditParametri(false); }, }); @@ -452,22 +710,15 @@ const EditParametri = () => { const EditServizioAdmin = () => { const { servizioId, userId, servizio } = useServizio(); const [open, setOpen] = useState(false); - const utils = api.useUtils(); - + const router = useRouter(); const { mutate: update } = 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.getServizioData.invalidate({ servizioId }); - await utils.servizio.getCompatibileAnnunci.invalidate({ - servizioId, - }); - await utils.servizio.getPacksSolution.invalidate({ servizioId }); + await utils.servizio.invalidate(); setOpen(false); }, }); @@ -475,10 +726,14 @@ const EditServizioAdmin = () => { onError: (error) => { toast.error(error.message); }, + onMutate: async () => { + await router.push(`/area-riservata/admin/user-view/ricerca/${userId}`); + }, + onSuccess: async () => { toast.success("Servizio rimosso con successo"); - await utils.servizio.getAllServizioAnnunci.invalidate({ userId }); - await utils.servizio.getServizioData.invalidate({ servizioId }); + + await utils.servizio.invalidate(); }, }); diff --git a/apps/infoalloggi/src/components/status-page.tsx b/apps/infoalloggi/src/components/status-page.tsx index c43348c..94997ba 100644 --- a/apps/infoalloggi/src/components/status-page.tsx +++ b/apps/infoalloggi/src/components/status-page.tsx @@ -7,7 +7,7 @@ export const Status500 = () => { const router = useRouter(); const { t } = useTranslation(); return ( -
    +

    diff --git a/apps/infoalloggi/src/components/tables/orders-table.tsx b/apps/infoalloggi/src/components/tables/orders-table.tsx index 22313f3..808cb7b 100644 --- a/apps/infoalloggi/src/components/tables/orders-table.tsx +++ b/apps/infoalloggi/src/components/tables/orders-table.tsx @@ -1,8 +1,11 @@ import type { ColumnDef } from "@tanstack/react-table"; -import { CircleCheck, ExternalLink } from "lucide-react"; +import { Circle, CircleCheck, ExternalLink } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; -import { OrdiniModal } from "~/components/area-riservata/ordini"; +import { + AdminWrapperOrdiniModal, + UserOrdiniModal, +} from "~/components/area-riservata/ordini"; import { DataTable } from "~/components/custom_ui/data-table"; import { DataTableColumnHeader } from "~/components/custom_ui/dataTable-header"; import type { @@ -10,28 +13,47 @@ import type { SearchFiltro, } from "~/components/custom_ui/dataTable-toolbar"; import { Button } from "~/components/ui/button"; +import { formatCurrency } from "~/lib/utils"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; import type { UsersId } from "~/schemas/public/Users"; -import type { OrdiniWPagamenti } from "~/server/controllers/ordini.controller"; +import type { Ordini_w_Utente } from "~/server/controllers/ordini.controller"; type PaymentsTableProps = { - data: OrdiniWPagamenti[]; + data: Ordini_w_Utente[]; isAdmin: boolean; userId?: UsersId; + filter: OrdiniOrdineId | null; }; -export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { +const StatusMapping = (status: PaymentStatusEnum | null) => { + switch (status) { + case PaymentStatusEnum.success: + return "Successo"; + case PaymentStatusEnum.processing: + return "In lavorazione"; + case PaymentStatusEnum.failed: + return "Errore"; + default: + return "Non completato"; + } +}; + +export const OrdiniTable = ({ + data, + isAdmin, + userId, + filter, +}: PaymentsTableProps) => { const tabledata = data.map((row) => { return { actions: "Azioni", created_at: row.created_at, id: row.ordine_id, - isActive: row.isActive, - isPaid: row.pagamenti.some( - (p) => p.paymentstatus === PaymentStatusEnum.success, - ), + status: row.isActive, + payment_status: row.paymentstatus, + importo: row.amount_cent, packid: row.packid, tipo: row.type, userId: row.userid, @@ -40,16 +62,19 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { }); const columns_titles = { + id: "ID Ordine", actions: "Azioni", created_at: "Creato il", - isPaid: "Pagato", + importo: "Importo", + status: "Status", + payment_status: "Pagamento", packid: "ID Pacchetto", tipo: "Tipo", username: "Utente", }; - const isPaidOptions = [ - { label: "Pagato", value: "true" }, - { label: "Non pagato", value: "false" }, + const statusOptions = [ + { label: "Attivo", value: "true" }, + { label: "Non attivo", value: "false" }, ]; const type_options = [ @@ -59,7 +84,6 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { { label: "Altro", value: OrderTypeEnum.Altro }, ]; - const [modalOpen, setModalOpen] = useState(false); const [selectedOrdine, setSelectedOrdine] = useState( null, ); @@ -67,6 +91,13 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { type Column = (typeof tabledata)[number]; const columns: ColumnDef[] = [ + { + accessorKey: "id", + cell: () => null, + enableHiding: false, + header: () => null, + }, + { accessorKey: "username", cell: ({ row }) => { @@ -125,30 +156,35 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { ), }, { - accessorKey: "isPaid", + accessorKey: "importo", cell: ({ row }) => { - const isPaid = row.original.isPaid; - const isActive = row.original.isActive; - if (isActive) { + return {formatCurrency(row.original.importo / 100)}; + }, + header: ({ column }) => ( + + ), + enableHiding: false, + sortingFn: (a, b, id) => { + const aValue = a.getValue(id) as number; + const bValue = b.getValue(id) as number; + return aValue - bValue; + }, + }, + { + accessorKey: "status", + cell: ({ row }) => { + if (!row.original.status) { return ( - - - {isPaid ? "Pagato" : "Attivato da Admin"} - - ); - } - if (isPaid) { - return ( - - - Pagato + + + Non attivo ); } return ( - + - Non pagato + Attivo ); }, @@ -157,10 +193,25 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { return value.includes(row.getValue(id)); }, header: ({ column }) => ( - + ), }, + { + accessorKey: "payment_status", + cell: ({ row }) => { + return {StatusMapping(row.original.payment_status)}; + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)); + }, + header: ({ column }) => ( + + ), + }, { cell: ({ row }) => { return ( @@ -168,13 +219,11 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { aria-label="Apri Dettagli Ordine" onClick={() => { setSelectedOrdine(row.original.id); - setModalOpen(true); }} size="sm" variant="outline" > - - Apri Dettagli + ); }, @@ -183,23 +232,17 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { }, ]; - const searchFiltro: SearchFiltro = - isAdmin && !userId - ? { - columnName: "username", - placeholder: "Cerca utente...", - } - : { - columnName: "packid", - placeholder: "Cerca pacchetto...", - }; + const searchFiltro: SearchFiltro = { + columnName: "id", + placeholder: "Cerca ordine...", + }; const pinnedFiltri: PinnedFiltro[] = [ { - columnName: "isPaid", + columnName: "status", - options: isPaidOptions, - title: "Stato Pagamento", + options: statusOptions, + title: "Stato Ordine", }, { columnName: "tipo", @@ -223,18 +266,25 @@ export const OrdiniTable = ({ data, isAdmin, userId }: PaymentsTableProps) => { tipo: true, username: isAdmin && !userId, }} + defaultFilterState={ + filter ? [{ id: "id", value: filter.toString() }] : [] + } defaultSort={[{ desc: true, id: "created_at" }]} pinnedFiltri={pinnedFiltri} searchColumn={searchFiltro} /> - {selectedOrdine && ( - + ) : ( + selectedOrdine && ( + + ) )}

    ); diff --git a/apps/infoalloggi/src/components/tables/payments-table.tsx b/apps/infoalloggi/src/components/tables/payments-table.tsx deleted file mode 100644 index 01168f1..0000000 --- a/apps/infoalloggi/src/components/tables/payments-table.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import type { ColumnDef } from "@tanstack/react-table"; -import { EllipsisVertical, Trash2, Wrench } from "lucide-react"; -import { DataTable } from "~/components/custom_ui/data-table"; -import { DataTableColumnHeader } from "~/components/custom_ui/dataTable-header"; -import type { - PinnedFiltro, - SearchFiltro, -} from "~/components/custom_ui/dataTable-toolbar"; -import { Button } from "~/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from "~/components/ui/dropdown-menu"; -import { formatCurrency } from "~/lib/utils"; -import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; -import type { OrdiniWPagamenti } from "~/server/controllers/ordini.controller"; -import { api } from "~/utils/api"; - -type PaymentsTableProps = { - data: OrdiniWPagamenti["pagamenti"]; - isAdmin: boolean; - isUserView?: boolean; -}; - -const StatusMapping = (status: PaymentStatusEnum | null) => { - switch (status) { - case PaymentStatusEnum.success: - return "Successo"; - case PaymentStatusEnum.processing: - return "In lavorazione"; - case PaymentStatusEnum.failed: - return "Errore"; - default: - return "Non completato"; - } -}; -const EnumFromString = (value: string) => { - switch (value) { - case "Successo": - return PaymentStatusEnum.success; - case "In lavorazione": - return PaymentStatusEnum.processing; - case "Errore": - return PaymentStatusEnum.failed; - default: - return null; - } -}; - -export const PaymentsTable = ({ - data, - isAdmin, - isUserView = false, -}: PaymentsTableProps) => { - const tabledata = data.map((row) => { - return { - actions: "Azioni", - amount: row.amount_cent, - id: row.id, - paid_at: row.paid_at, - raw_status: row.paymentstatus, - status: StatusMapping(row.paymentstatus), - intent_id: row.intent_id, - }; - }); - - const columns_titles = { - actions: "Azioni", - amount: "Importo", - paid_at: "Pagato il", - status: "Stato", - username: "Utente", - }; - const status_options = [ - { label: "Inutilizzato", value: "Inutilizzato" }, - { label: "Successo", value: "Successo" }, - { label: "In lavorazione", value: "In lavorazione" }, - { label: "Errore", value: "Errore" }, - ]; - const utils = api.useUtils(); - const { mutate: update } = api.servizio.updatePagamento.useMutation({ - onSuccess: async () => { - await utils.servizio.getAllPagamenti.invalidate(); - await utils.servizio.getOrdini.invalidate(); - await utils.servizio.getOrdineById.invalidate({ - ordineId: data[0]?.ordine_id, - }); - if ((!isAdmin || isUserView) && data[0]) { - await utils.servizio.getUserPagamenti.invalidate({ - userId: data[0].userid, - }); - } - }, - }); - const { mutate: remove } = api.servizio.removePagamento.useMutation({ - onSuccess: async () => { - await utils.servizio.getAllPagamenti.invalidate(); - await utils.servizio.getOrdini.invalidate(); - await utils.servizio.getOrdineById.invalidate({ - ordineId: data[0]?.ordine_id, - }); - if ((!isAdmin || isUserView) && data[0]) { - await utils.servizio.getUserPagamenti.invalidate({ - userId: data[0].userid, - }); - } - }, - }); - - type Column = (typeof tabledata)[number]; - - const columns: ColumnDef[] = [ - { - accessorKey: "paid_at", - cell: ({ row }) => { - const date = row.getValue("paid_at"); - return new Date(date as Date).toLocaleDateString(); - }, - enableHiding: false, - header: ({ column }) => ( - - ), - sortingFn: "datetime", - }, - { - accessorKey: "status", - - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)); - }, - header: ({ column }) => ( - - ), - }, - { - accessorKey: "amount", - cell: ({ row }) => { - return {formatCurrency(row.original.amount / 100)}; - }, - header: ({ column }) => ( - - ), - }, - { - cell: ({ row }) => { - const data = row.original; - if (!isAdmin) return null; - return ( - - - - - - Azioni - - - - - Aggiorna status - - - - { - update({ - data: { - paymentstatus: EnumFromString(v), - paid_at: v === "Successo" ? new Date() : null, - }, - pagamentoId: data.id, - }); - }} - value={StatusMapping(data.raw_status)} - > - - Inutilizzato - - - - Successo - - - In lavorazione - - - Errore - - - - - - - - confirm("Sei sicuro di voler eliminare il servizio?") && - remove({ pagamentoId: data.id }) - } - > - - Elimina Pagamento - - - - ); - }, - enableHiding: false, - id: "actions", - }, - ]; - - const searchFiltro: SearchFiltro | undefined = - isAdmin && !isUserView - ? { - columnName: "username", - placeholder: "Cerca utente...", - } - : undefined; - - const pinnedFiltri: PinnedFiltro[] = [ - { - columnName: "status", - options: status_options, - title: "Status", - }, - ]; - - return ( -
    - -
    - ); -}; diff --git a/apps/infoalloggi/src/components/ui/alert-dialog.tsx b/apps/infoalloggi/src/components/ui/alert-dialog.tsx index 98af3ac..8c28f01 100644 --- a/apps/infoalloggi/src/components/ui/alert-dialog.tsx +++ b/apps/infoalloggi/src/components/ui/alert-dialog.tsx @@ -1,7 +1,9 @@ +"use client"; + import { AlertDialog as AlertDialogPrimitive } from "radix-ui"; import type * as React from "react"; -import { buttonVariants } from "~/components/ui/button"; import { cn } from "~/lib/utils"; +import { Button } from "./button"; function AlertDialog({ ...props @@ -32,7 +34,7 @@ function AlertDialogOverlay({ return ( ) { +}: React.ComponentProps & { + size?: "default" | "sm"; +}) { return ( @@ -66,7 +72,10 @@ function AlertDialogHeader({ }: React.ComponentProps<"div">) { return (
    @@ -80,7 +89,7 @@ function AlertDialogFooter({ return (
    ) { + return ( +
    + ); +} + function AlertDialogTitle({ className, ...props }: React.ComponentProps) { return ( @@ -108,7 +136,10 @@ function AlertDialogDescription({ }: React.ComponentProps) { return ( @@ -117,38 +148,51 @@ function AlertDialogDescription({ function AlertDialogAction({ className, + variant = "default", + size = "default", ...props -}: React.ComponentProps) { +}: React.ComponentProps & + Pick, "variant" | "size">) { return ( - + ); } function AlertDialogCancel({ className, + variant = "outline", + size = "default", ...props -}: React.ComponentProps) { +}: React.ComponentProps & + Pick, "variant" | "size">) { return ( - + ); } export { AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, AlertDialogAction, AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogMedia, + AlertDialogOverlay, + AlertDialogPortal, + AlertDialogTitle, + AlertDialogTrigger, }; diff --git a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx index b5bf076..28a726f 100644 --- a/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx +++ b/apps/infoalloggi/src/forms/FormEditAnnuncio.tsx @@ -267,7 +267,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => { + + + + + + + + )} + /> + ( + +
    + Pagato + + + + +
    + + + Imposta se il servizio è pagato + +
    + )} + /> + ( + +
    + Sconto + + + + +
    +
    + )} + /> + + {scontoWch && scontoWch[0] !== 0 ? ( + + Prezzo scontato:{" "} + {formatCurrency( + ((1 - scontoWch[0] / 100) * scontoWch[1]) / 100, + )} + + ) : ( + Prezzo: {formatCurrency(scontoWch[1] / 100)} + )} +
    + + + + { + remove({ ordineId: data.ordine_id }); + }} + title="Elimina ordine" + > + + +
    + + + + +
    +
    + + + + ); +}; diff --git a/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx b/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx index 0c47562..b304054 100644 --- a/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx +++ b/apps/infoalloggi/src/forms/FormEditServizioAdmin.tsx @@ -35,6 +35,7 @@ const Schema = z.object({ skipPayment: z.boolean(), skipControlloDoc: z.boolean(), skipDocMotivazione: z.boolean(), + onboardOk: z.boolean(), }); export type FormValues = z.infer; @@ -64,6 +65,7 @@ export const FormEditServizio = ({ skipPayment: false, skipControlloDoc: false, skipDocMotivazione: false, + onboardOk: false, }; } const { locale, t } = useTranslation(); @@ -193,6 +195,28 @@ export const FormEditServizio = ({
    + ( + +
    + Onboard Fatta + + + + +
    + + Il servizio è interrotto +
    + )} + /> (), + type: z.enum(OrderTypeEnum), created_at: z.date(), + amount_cent: z.number(), + sconto: z.number().min(0).max(100), }); type FormValues = z.infer; z.config(z.locales.it()); const form = useZodForm(schema, { - defaultValues: {}, + defaultValues: { + sconto: 0, + amount_cent: 0, + isActive: false, + }, }); const utils = api.useUtils(); const { mutate: createOrder } = api.servizio.createOrdine.useMutation({ @@ -75,20 +81,17 @@ export const FormNewOrder = ({ }, onSuccess: async () => { toast.success("Ordine creato con successo"); - await utils.servizio.getOrdini.invalidate({ - userId, - }); + await utils.servizio.invalidate(); setOpen(false); form.reset(); }, }); + const { watch } = form; + + const scontoWch = watch(["sconto", "amount_cent"]); function onSubmit(fields: FormValues) { createOrder({ - data: { - ...fields, - servizio_id: servizioId, - userid: userId, - }, + data: { ...fields, servizio_id: servizioId, userid: userId }, }); } @@ -117,6 +120,15 @@ export const FormNewOrder = ({ const selected = zPrezziarioId.safeParse(value.value); if (selected.success) { field.onChange(selected.data); + const selectedPrezzo = prezziario.find( + (p) => p.idprezziario === selected.data, + ); + if (selectedPrezzo) { + form.setValue( + "amount_cent", + selectedPrezzo.prezzo_cent, + ); + } } else { console.error( "Invalid packid selected", @@ -220,7 +232,7 @@ export const FormNewOrder = ({ render={({ field }) => (
    - Attivo + Pagato - Imposta se il servizio è attivo + Imposta se il servizio è pagato )} /> + ( + +
    + Sconto + + + + +
    +
    + )} + /> + + {scontoWch && scontoWch[0] !== 0 ? ( + + Prezzo scontato:{" "} + {formatCurrency( + ((1 - scontoWch[0] / 100) * scontoWch[1]) / 100, + )} + + ) : ( + Prezzo: {formatCurrency(scontoWch[1] / 100)} + )}
    diff --git a/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx b/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx index 20ae33c..c8a4add 100644 --- a/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx +++ b/apps/infoalloggi/src/forms/FormNewServizioAcquisto.tsx @@ -114,14 +114,15 @@ type FormNewServizioValues = z.infer; export const FormNewServizioAcquisto = ({ initialData, userId, - isAdmin = false, + isAdmin, }: { initialData: InitialData; userId: UsersId; - isAdmin?: boolean; + isAdmin: boolean; }) => { const router = useRouter(); const { comuni, nazioni } = useCatasto(); + console.log(isAdmin); const schema = FormNewServizioSchema.superRefine( ( { @@ -226,14 +227,7 @@ export const FormNewServizioAcquisto = ({ path: ["n_adulti"], }); } - if (!permanenza && tipologia === TipologiaPosizioneEnum.Transitorio) { - refinementContext.issues.push({ - code: "custom", - input: "", - message: "Inserisci permanenza", - path: ["permanenza"], - }); - } + if (tipologia === TipologiaPosizioneEnum.Transitorio) { if ( motivazione_transitorio === "" || @@ -254,7 +248,7 @@ export const FormNewServizioAcquisto = ({ path: ["scadenza_motivazione_transitoria"], }); } - if (!permanenza) { + if (permanenza == null) { refinementContext.issues.push({ code: "custom", input: "", @@ -301,7 +295,7 @@ export const FormNewServizioAcquisto = ({ defaultValues: defaultValues, }); const DatePickerLocale = locale === "it" ? it : enUS; - const { mutate } = api.servizio.submitOnboardForPurchase.useMutation({ + const { mutate } = api.servizio.processServizioOnboard.useMutation({ onError: (error) => { toast.error(`Errore invio dati: ${error.message}`, { id: "salvataggio", @@ -312,25 +306,19 @@ export const FormNewServizioAcquisto = ({ id: "salvataggio", }); }, - onSuccess: async (data) => { + onSuccess: async () => { toast.success("Servizio impostato con successo", { id: "salvataggio", }); if (isAdmin) { - if (data.ordineIdForPayment) { - await router.push(`/area-riservata/admin/user-view/ordini/${userId}`); - } else { - await router.push( - `/area-riservata/admin/user-view/servizio/${userId}/${initialData.servizio.servizio_id}`, - ); - } + await router.push( + `/area-riservata/admin/user-view/servizio/${userId}/${initialData.servizio.servizio_id}`, + ); } else { - if (data.ordineIdForPayment) { - await router.push(`/servizio/pagamento/${data.ordineIdForPayment}`); - } else { - await router.push("/area-riservata/dashboard"); - } + await router.push( + `/area-riservata/servizio/${initialData.servizio.servizio_id}`, + ); } }, }); @@ -1245,33 +1233,6 @@ export const FormNewServizioAcquisto = ({
    - {isAdmin && ( - ( - -
    - - Vuoi saltare la creazione dell'ordine e impostare il - servizio come attivo? - - - { - field.onChange(v); - }} - /> - - -
    -
    - )} - /> - )} diff --git a/apps/infoalloggi/src/i18n/stripe.ts b/apps/infoalloggi/src/i18n/stripe.ts index 3f18c64..62837be 100644 --- a/apps/infoalloggi/src/i18n/stripe.ts +++ b/apps/infoalloggi/src/i18n/stripe.ts @@ -1,54 +1,58 @@ -export type PaymentType = - | "manual" - | "acss_debit" - | "affirm" - | "afterpay_clearpay" - | "alipay" - | "alma" - | "amazon_pay" - | "au_becs_debit" - | "bacs_debit" - | "bancontact" - | "billie" - | "blik" - | "boleto" - | "card" - | "card_present" - | "cashapp" - | "customer_balance" - | "eps" - | "fpx" - | "giropay" - | "grabpay" - | "ideal" - | "interac_present" - | "kakao_pay" - | "klarna" - | "konbini" - | "kr_card" - | "link" - | "mobilepay" - | "multibanco" - | "naver_pay" - | "nz_bank_account" - | "oxxo" - | "p24" - | "pay_by_bank" - | "payco" - | "paynow" - | "paypal" - | "pix" - | "promptpay" - | "revolut_pay" - | "samsung_pay" - | "satispay" - | "sepa_debit" - | "sofort" - | "swish" - | "twint" - | "us_bank_account" - | "wechat_pay" - | "zip"; +export const PAYMENT_TYPES = [ + "manual", + "acss_debit", + "affirm", + "afterpay_clearpay", + "alipay", + "alma", + "amazon_pay", + "au_becs_debit", + "bacs_debit", + "bancontact", + "billie", + "blik", + "boleto", + "card", + "card_present", + "cashapp", + "customer_balance", + "eps", + "fpx", + "giropay", + "grabpay", + "ideal", + "interac_present", + "kakao_pay", + "klarna", + "konbini", + "kr_card", + "link", + "mobilepay", + "multibanco", + "naver_pay", + "nz_bank_account", + "oxxo", + "p24", + "pay_by_bank", + "payco", + "paynow", + "paypal", + "pix", + "promptpay", + "revolut_pay", + "samsung_pay", + "satispay", + "sepa_debit", + "sofort", + "swish", + "twint", + "us_bank_account", + "wechat_pay", + "zip", +] as const; + +export type PaymentType = (typeof PAYMENT_TYPES)[number]; + export const PaymentMethodToString = (type: PaymentType) => { switch (type) { case "manual": diff --git a/apps/infoalloggi/src/pages/annuncio/[cod].tsx b/apps/infoalloggi/src/pages/annuncio/[cod].tsx index 593a0d2..3018007 100644 --- a/apps/infoalloggi/src/pages/annuncio/[cod].tsx +++ b/apps/infoalloggi/src/pages/annuncio/[cod].tsx @@ -184,7 +184,7 @@ const AnnuncioView = ({ cod, flag }: Omit) => {
    - +
    ); diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/onboard/[servizioId].tsx b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/onboard/[servizioId].tsx index 836a646..832f7df 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/onboard/[servizioId].tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/onboard/[servizioId].tsx @@ -38,7 +38,7 @@ const OnboardUser: NextPageWithLayout = ({ diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ordini/[userId].tsx b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ordini/[userId].tsx index 283dc55..123f44d 100644 --- a/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ordini/[userId].tsx +++ b/apps/infoalloggi/src/pages/area-riservata/admin/user-view/ordini/[userId].tsx @@ -1,26 +1,24 @@ import type { GetServerSideProps } from "next"; -import Link from "next/link"; -import { OrdineManualeDialog } from "~/components/area-riservata/ordine_manuale"; +import Head from "next/head"; import { AreaRiservataLayoutUserView } from "~/components/Layout"; import { LoadingPage } from "~/components/loading"; import { Status500 } from "~/components/status-page"; import { OrdiniTable } from "~/components/tables/orders-table"; -import { Button } from "~/components/ui/button"; import type { NextPageWithLayout } from "~/pages/_app"; -import type { ServizioServizioId } from "~/schemas/public/Servizio"; +import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { UsersId } from "~/schemas/public/Users"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; -import { zServizioId, zUserId } from "~/server/utils/zod_types"; +import { zUserId } from "~/server/utils/zod_types"; import { api } from "~/utils/api"; type OrdiniUserProps = { userId: UsersId; - servizioId: ServizioServizioId | null; + ordineId: OrdiniOrdineId | null; }; const OrdiniUser: NextPageWithLayout = ({ userId, - servizioId, + ordineId, }: OrdiniUserProps) => { const { data, isLoading } = api.servizio.getOrdini.useQuery({ userId, @@ -29,39 +27,24 @@ const OrdiniUser: NextPageWithLayout = ({ if (isLoading) return ; if (!data) return ; return ( -
    -
    -

    - Ordini{" "} - - {servizioId ? `- Servizio ${servizioId}` : ""} - -

    - {servizioId && ( -
    - - - - -
    - )} -
    - - servizioId ? ordine.servizio_id === servizioId : true, - )} - isAdmin - userId={userId} - /> -
    + <> + + Ordini - Infoalloggi.it + +
    +
    +

    Ordini

    +
    + +
    + ); }; export default OrdiniUser; export const getServerSideProps = (async (context) => { const userId = context.params?.userId; + if (typeof userId !== "string") { return { notFound: true, @@ -75,6 +58,13 @@ export const getServerSideProps = (async (context) => { notFound: true, }; } + const ordineId = context.query?.ordineId; + if (ordineId && typeof ordineId !== "string") { + return { + notFound: true, + }; + } + const access_token = context.req.cookies.access_token; const helpers = await TrpcAuthedFetchingIstance({ access_token }); @@ -83,18 +73,11 @@ export const getServerSideProps = (async (context) => { userId: parsed.data, }); - let servizioId: ServizioServizioId | null = null; - - const parsedServizioId = zServizioId.safeParse(context.query?.servizioId); - if (parsedServizioId.success) { - servizioId = parsedServizioId.data; - } - return { props: { + ordineId: ordineId ? (ordineId as OrdiniOrdineId) : null, userId: parsed.data, trpcState: helpers.trpc.dehydrate(), - servizioId, }, }; } diff --git a/apps/infoalloggi/src/pages/area-riservata/ordini.tsx b/apps/infoalloggi/src/pages/area-riservata/ordini.tsx index 74f7bec..77a66b5 100644 --- a/apps/infoalloggi/src/pages/area-riservata/ordini.tsx +++ b/apps/infoalloggi/src/pages/area-riservata/ordini.tsx @@ -6,18 +6,42 @@ import { Status500 } from "~/components/status-page"; import { OrdiniTable } from "~/components/tables/orders-table"; import type { NextPageWithLayout } from "~/pages/_app"; import { useEnforcedSession } from "~/providers/SessionProvider"; -import type { UsersId } from "~/schemas/public/Users"; +import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { api } from "~/utils/api"; -const Ordini: NextPageWithLayout = () => { +type OrdiniProps = { + ordineId: OrdiniOrdineId | null; +}; +const Ordini: NextPageWithLayout = ({ ordineId }: OrdiniProps) => { const session = useEnforcedSession(); + const { data, isLoading } = api.servizio.getOrdini.useQuery({ + userId: session.user.id, + }); + + if (isLoading) return ; + if (!data) return ; return ( <> Ordini - Infoalloggi.it - +
    +
    +
    +

    Ordini

    + +
    + +
    +
    +
    +
    ); }; @@ -25,12 +49,19 @@ export default Ordini; export const getServerSideProps = (async (context) => { const access_token = context.req.cookies.access_token; + const ordineId = context.query?.ordineId; + if (ordineId && typeof ordineId !== "string") { + return { + notFound: true, + }; + } const helpers = await TrpcAuthedFetchingIstance({ access_token }); if (helpers) { await helpers.trpc.servizio.getOrdini.prefetch({}); return { props: { + ordineId: ordineId ? (ordineId as OrdiniOrdineId) : null, trpcState: helpers.trpc.dehydrate(), }, }; @@ -40,29 +71,6 @@ export const getServerSideProps = (async (context) => { }; }) satisfies GetServerSideProps; -const SessionWrapper = ({ userId }: { userId: UsersId }) => { - const { data, isLoading } = api.servizio.getOrdini.useQuery({ - userId, - }); - - if (isLoading) return ; - if (!data) return ; - - return ( -
    -
    -
    -

    Ordini

    - -
    - -
    -
    -
    -
    - ); -}; - Ordini.getLayout = function getLayout(page) { return {page}; }; diff --git a/apps/infoalloggi/src/pages/area-riservata/admin/scheda-annuncio-stampa/[id].tsx b/apps/infoalloggi/src/pages/area-riservata/scheda-annuncio-stampa/[id].tsx similarity index 100% rename from apps/infoalloggi/src/pages/area-riservata/admin/scheda-annuncio-stampa/[id].tsx rename to apps/infoalloggi/src/pages/area-riservata/scheda-annuncio-stampa/[id].tsx diff --git a/apps/infoalloggi/src/pages/area-riservata/servizio/[servizioId].tsx b/apps/infoalloggi/src/pages/area-riservata/servizio/[servizioId].tsx index 156c488..fb3f26a 100644 --- a/apps/infoalloggi/src/pages/area-riservata/servizio/[servizioId].tsx +++ b/apps/infoalloggi/src/pages/area-riservata/servizio/[servizioId].tsx @@ -1,15 +1,10 @@ -import { ArrowRight } from "lucide-react"; import type { GetServerSideProps } from "next"; -import Link from "next/link"; import { UserDashboardFaq } from "~/components/area-riservata/dashboard"; import { AreaRiservataLayout } from "~/components/Layout"; import { LoadingPage } from "~/components/loading"; import { Servizio } from "~/components/servizio/servizio"; import { Status500 } from "~/components/status-page"; -import { Button } from "~/components/ui/button"; -import { Separator } from "~/components/ui/separator"; 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"; @@ -23,7 +18,6 @@ const ServizioPage: NextPageWithLayout = ({ servizioId, }: ServizioPageProps) => { const { user } = useEnforcedSession(); - const { t } = useTranslation(); const { data, isLoading } = api.servizio.getServizioData.useQuery({ servizioId, }); @@ -36,7 +30,7 @@ const ServizioPage: NextPageWithLayout = ({ } return (
    -
    + {/*

    {t.servizio.dettaglio_servizio} @@ -50,7 +44,7 @@ const ServizioPage: NextPageWithLayout = ({

    -
    +
    */}
    { {item.qas.map((f, i) => ( + i + }`} value={i.toString()} > diff --git a/apps/infoalloggi/src/pages/servizio/bonifico-checkout/[ordineId].tsx b/apps/infoalloggi/src/pages/servizio/bonifico-checkout/[ordineId].tsx new file mode 100644 index 0000000..c0c49fe --- /dev/null +++ b/apps/infoalloggi/src/pages/servizio/bonifico-checkout/[ordineId].tsx @@ -0,0 +1,124 @@ +import { ArrowRight } from "lucide-react"; +import type { GetServerSideProps } from "next"; +import Link from "next/link"; +import { AreaRiservataLayout } from "~/components/Layout"; +import { Button } from "~/components/ui/button"; +import { formatCurrency } from "~/lib/utils"; +import type { NextPageWithLayout } from "~/pages/_app"; +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; +import { zOrdineId } from "~/server/utils/zod_types"; + +type BonificoManualeProps = { + titolo: string; + prezzo: number; + sconto: number; + descrizione: string; +}; + +/** + * /servizio/bonifico-checkout/[ordineId] \ + * Pagina di checkout per il pagamento di un ordine di servizio tramite bonifico + */ +const BonificoManualePage: NextPageWithLayout = ({ + titolo, + prezzo, + sconto, + descrizione, +}: BonificoManualeProps) => { + return ( +
    +

    Pagamento tramite bonifico

    +
    +

    {titolo}

    +

    {descrizione}

    +

    + Importo: {formatCurrency(prezzo / 100)}{" "} + {sconto > 0 && ( + (Sconto: {sconto}%) + )} +

    +
    +

    Istruzioni per il bonifico:

    +
      +
    • + Intestazione: Arcenia Srl +
    • +
    • + IBAN: IT33F0326860160052727620300 BANCA SELLA (Codice + Swift) SELBIT2BXXX per bonifici esteri +
    • +
    • + Causale: {titolo} +
    • +
    +
    +

    + Inoltraci la contabile del bonifico per velocizzare la verifica del + pagamento. +

    + Grazie! +
    + + + +
    + ); +}; + +BonificoManualePage.getLayout = function getLayout(page) { + return {page}; +}; + +export default BonificoManualePage; + +export const getServerSideProps = (async (context) => { + const id = context.params?.ordineId; + if (typeof id !== "string") { + console.error("Error: ordineId is not a string"); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const parsed = zOrdineId.safeParse(id); + if (!parsed.success) { + console.error("Error parsing ordineId", parsed.error); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const access_token = context.req.cookies.access_token; + + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (helper) { + const paymentData = + await helper.trpc.pagamenti.getPagamentoDataForCheckout.fetch({ + ordineId: parsed.data, + }); + + return { + props: { + titolo: paymentData.nome_it, + prezzo: paymentData.amount_cent, + sconto: paymentData.sconto, + descrizione: paymentData.desc_it, + }, + }; + } + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; +}) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx b/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx index f2295e1..336ac0a 100644 --- a/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx +++ b/apps/infoalloggi/src/pages/servizio/conferma-immobile/[...slug].tsx @@ -95,6 +95,7 @@ const ConfirmSection = ({ }, onSuccess: async (data, _variables, context) => { await utils.storage.retrieveUserFileData.invalidate(); + await utils.servizio.invalidate(); toast.success("Confermato", { icon: "👍", @@ -105,7 +106,7 @@ const ConfirmSection = ({ `/area-riservata/admin/user-view/ricerca/${data.userId}`, ); } else { - await router.push("/area-riservata/dashboard"); + await router.push(`/area-riservata/servizio/${servizioId}`); } }, }); diff --git a/apps/infoalloggi/src/pages/servizio/onboard/[servizioId].tsx b/apps/infoalloggi/src/pages/servizio/onboard/[servizioId].tsx index c74d370..18d5f8c 100644 --- a/apps/infoalloggi/src/pages/servizio/onboard/[servizioId].tsx +++ b/apps/infoalloggi/src/pages/servizio/onboard/[servizioId].tsx @@ -15,7 +15,13 @@ import { api } from "~/utils/api"; type OnboardServizioProps = { servizioId: ServizioServizioId; }; - +/** + * /servizio/onboard/[servizioId] \ + * Pagina di onboarding per un nuovo servizio. Mostra un tutorial e un form per completare l'acquisto del servizio, con i dati precompilati se disponibili. + * Accessibile solo se l'onboarding è disponibile per l'utente e il servizio specificato. + * Se l'onboarding non è disponibile, reindirizza alla dashboard dell'area riservata. + * Se non è possibile recuperare i dati necessari, mostra una pagina di errore. + */ const OnboardServizio: NextPageWithLayout = ({ servizioId, }: OnboardServizioProps) => { @@ -38,7 +44,11 @@ const OnboardServizio: NextPageWithLayout = ({
    - +
    ); diff --git a/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx similarity index 74% rename from apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx rename to apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx index f610399..c8bbd91 100644 --- a/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[paymentId].tsx +++ b/apps/infoalloggi/src/pages/servizio/pagamento-checkout/[ordineId].tsx @@ -4,7 +4,7 @@ import { AreaRiservataLayout } from "~/components/Layout"; import stripe from "~/lib/stripe"; import type { NextPageWithLayout } from "~/pages/_app"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; -import { zPagamentoId } from "~/server/utils/zod_types"; +import { zOrdineId } from "~/server/utils/zod_types"; type PagamentoPageProps = { clientSecret: string | null; @@ -13,6 +13,10 @@ type PagamentoPageProps = { descrizione: string; }; +/** + * /servizio/pagamento-checkout/[ordineId] \ + * Pagina di checkout per il pagamento di un ordine di servizio. Riceve l'ordineId, recupera i dati necessari e crea un PaymentIntent su Stripe, quindi passa il clientSecret al componente di checkout. + */ const PagamentoPage: NextPageWithLayout = ({ clientSecret, titolo, @@ -41,9 +45,9 @@ PagamentoPage.getLayout = function getLayout(page) { export default PagamentoPage; export const getServerSideProps = (async (context) => { - const id = context.params?.paymentId; + const id = context.params?.ordineId; if (typeof id !== "string") { - console.error("Error: paymentId is not a string"); + console.error("Error: ordineId is not a string"); return { redirect: { destination: "/500", @@ -52,9 +56,9 @@ export const getServerSideProps = (async (context) => { }; } - const parsed = zPagamentoId.safeParse(id); + const parsed = zOrdineId.safeParse(id); if (!parsed.success) { - console.error("Error parsing paymentId", parsed.error); + console.error("Error parsing ordineId", parsed.error); return { redirect: { destination: "/500", @@ -69,9 +73,17 @@ export const getServerSideProps = (async (context) => { if (helper) { const paymentData = await helper.trpc.pagamenti.getPagamentoDataForCheckout.fetch({ - paymentId: parsed.data, + ordineId: parsed.data, }); + const customer = await stripe.customers.create({ + email: paymentData.email, + name: paymentData.username, + metadata: { + userId: paymentData.userid, + }, + }); + const { client_secret: clientSecret } = await stripe.paymentIntents.create({ amount: paymentData.amount_cent, @@ -79,12 +91,15 @@ export const getServerSideProps = (async (context) => { enabled: true, }, currency: "eur", + customer: customer.id, description: paymentData.nome_it, metadata: { - paymentId: paymentData.id, + packId: paymentData.packid, + ordineId: paymentData.ordine_id, servizioId: paymentData.servizio_id, userId: paymentData.userid, }, + receipt_email: paymentData.email, }); diff --git a/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx b/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx index 9c0154e..5ad8b38 100644 --- a/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx +++ b/apps/infoalloggi/src/pages/servizio/pagamento-status.tsx @@ -9,6 +9,7 @@ import stripe from "~/lib/stripe"; import { cn } from "~/lib/utils"; import { useTranslation } from "~/providers/I18nProvider"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; +import type { ServizioServizioId } from "~/schemas/public/Servizio"; import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; enum AcquistoStatusEnum { @@ -20,6 +21,7 @@ enum AcquistoStatusEnum { } type AcquistoProcessingProps = { + servizioId: ServizioServizioId | null; status: AcquistoStatusEnum; }; @@ -38,6 +40,7 @@ export const getServerSideProps = (async (context) => { if (!paymentIntent) { return { props: { + servizioId: null, status: AcquistoStatusEnum.internal_error, }, }; @@ -54,6 +57,7 @@ export const getServerSideProps = (async (context) => { if (!payment) { return { props: { + servizioId: null, status: AcquistoStatusEnum.not_found, }, }; @@ -75,6 +79,7 @@ export const getServerSideProps = (async (context) => { } return { props: { + servizioId: payment.servizio_id, status: resultStatus, }, }; @@ -89,6 +94,7 @@ export const getServerSideProps = (async (context) => { }) satisfies GetServerSideProps; export default function AcquistoProcessing({ + servizioId, status, }: AcquistoProcessingProps) { const { t } = useTranslation(); @@ -108,7 +114,7 @@ export default function AcquistoProcessing({ title={t.acquisto_elaborazione.success_title} /> - + )} {status === AcquistoStatusEnum.processing && ( diff --git a/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx deleted file mode 100644 index 4674f1c..0000000 --- a/apps/infoalloggi/src/pages/servizio/pagamento/[ordineId].tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { ArrowRight, ExternalLink } from "lucide-react"; -import type { GetServerSideProps } from "next"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { AreaRiservataLayout } from "~/components/Layout"; -import { LoadingPage } from "~/components/loading"; -import { Status500 } from "~/components/status-page"; -import { Button } from "~/components/ui/button"; -import { formatCurrency } from "~/lib/utils"; -import type { NextPageWithLayout } from "~/pages/_app"; -import { useTranslation } from "~/providers/I18nProvider"; -import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; -import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario"; -import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; -import { zOrdineId } from "~/server/utils/zod_types"; -import { api } from "~/utils/api"; - -type ServizioPurchaseProps = { - ordineId: OrdiniOrdineId; -}; - -const ServicePurchasePage: NextPageWithLayout = ({ - ordineId, -}: ServizioPurchaseProps) => { - const router = useRouter(); - const { data, isLoading } = api.servizio.getServizioPaymentData.useQuery({ - ordineId, - }); - - if (isLoading) { - return ; - } - if (!data) { - return ; - } - return ( -
    - - await router.push(`/servizio/pagamento-checkout/${data.payment.id}`) - } - packId={data.ordine.packid} - /> -
    - ); -}; - -const PricePreparation = ({ - packId, - onProcedi, -}: { - packId: PrezziarioIdprezziario; - onProcedi: () => void; -}) => { - const { data: prezziario, isLoading } = - api.prezziario.getPrezzoPerServizio.useQuery({ - idprezziario: packId, - }); - const { t } = useTranslation(); - return ( -
    - {isLoading || !prezziario ? ( - <> -

    {t.acquisto.preparazione}

    - - - ) : ( -
    -
    -

    {prezziario.nome_it}

    -

    - Cod: “{prezziario.idprezziario}” -

    -

    - {formatCurrency(prezziario.prezzo_cent / 100)} -

    -

    {prezziario.desc_it}

    -
    - - {prezziario.testo_condizioni && ( - - - - )} - - -
    - )} -
    - ); -}; - -ServicePurchasePage.getLayout = function getLayout(page) { - return {page}; -}; - -export default ServicePurchasePage; - -export const getServerSideProps = (async (context) => { - const id = context.params?.ordineId; - - if (typeof id !== "string") { - console.error("Error: ordineId is not a string"); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; - } - - const parsed = zOrdineId.safeParse(id); - if (!parsed.success) { - console.error("Error parsing ordineId", parsed.error); - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; - } - - const access_token = context.req.cookies.access_token; - - const helper = await TrpcAuthedFetchingIstance({ access_token }); - if (helper) { - const isAwaitingPayment = - await helper.trpc.servizio.isOrdineAwaitingPayment.fetch({ - ordineId: parsed.data, - }); - if (!isAwaitingPayment) { - return { - redirect: { - destination: "/area-riservata/dashboard", - permanent: false, - }, - }; - } - - await helper.trpc.servizio.getServizioPaymentData.prefetch({ - ordineId: parsed.data, - }); - - return { - props: { - ordineId: parsed.data, - trpcState: helper.trpc.dehydrate(), - }, - }; - } - return { - redirect: { - destination: "/500", - permanent: false, - }, - }; -}) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/pages/servizio/pagamento/[servizioId].tsx b/apps/infoalloggi/src/pages/servizio/pagamento/[servizioId].tsx new file mode 100644 index 0000000..7834dfb --- /dev/null +++ b/apps/infoalloggi/src/pages/servizio/pagamento/[servizioId].tsx @@ -0,0 +1,151 @@ +import { ArrowRight, ExternalLink } from "lucide-react"; +import type { GetServerSideProps } from "next"; +import Link from "next/link"; +import { useRouter } from "next/router"; +import z from "zod"; +import { AreaRiservataLayout } from "~/components/Layout"; +import { Button } from "~/components/ui/button"; +import { formatCurrency } from "~/lib/utils"; +import type { NextPageWithLayout } from "~/pages/_app"; +import { useTranslation } from "~/providers/I18nProvider"; +import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; +import type { PreparedPaymentData } from "~/server/controllers/pagamenti.controller"; +import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; +import { zServizioId } from "~/server/utils/zod_types"; + +type PreviewPurchaseProps = { + stripeDisabled: boolean; + data: PreparedPaymentData; +}; +/** + * /servizio/pagamento/[servizioId]?type= \ + * Pagina di preparazione al pagamento. + */ +const PreviewPurchasePage: NextPageWithLayout = ({ + stripeDisabled, + data, +}: PreviewPurchaseProps) => { + const router = useRouter(); + const { t } = useTranslation(); + + return ( +
    +
    +
    +
    +

    {data.packName}

    +

    Cod: “{data.packid}”

    +

    + {formatCurrency(data.amount_cent / 100)} {data.sconto > 0 && (Sconto: {data.sconto}%)} +

    +

    {data.packDesc}

    +
    + + + + + + +
    +
    +
    + ); +}; + +PreviewPurchasePage.getLayout = function getLayout(page) { + return {page}; +}; + +export default PreviewPurchasePage; + +export const getServerSideProps = (async (context) => { + const id = context.params?.servizioId; + + if (typeof id !== "string") { + console.error("Error: servizioId is not a string"); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const parsedServizioId = zServizioId.safeParse(id); + if (!parsedServizioId.success) { + console.error("Error parsing servizioId", parsedServizioId.error); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const payment_type = context.query?.type; + if (typeof payment_type !== "string") { + console.error("Error: payment type is not a string"); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + const parsedType = z.enum(OrderTypeEnum).safeParse(payment_type); + if (!parsedType.success) { + console.error("Error parsing payment type", parsedType.error); + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; + } + + const access_token = context.req.cookies.access_token; + + const helper = await TrpcAuthedFetchingIstance({ access_token }); + if (helper) { + const data = await helper.trpc.pagamenti.preparePayment.fetch({ + type: parsedType.data, + servizioId: parsedServizioId.data, + }); + + const flag = await helper.trpc.flags.GetFlagValue.fetch({ + id: "STRIPE_DISABLED", + }); + + return { + props: { + stripeDisabled: flag || false, + data, + }, + }; + } + return { + redirect: { + destination: "/500", + permanent: false, + }, + }; +}) satisfies GetServerSideProps; diff --git a/apps/infoalloggi/src/providers/ServizioProvider.tsx b/apps/infoalloggi/src/providers/ServizioProvider.tsx index 2554792..2f4453d 100644 --- a/apps/infoalloggi/src/providers/ServizioProvider.tsx +++ b/apps/infoalloggi/src/providers/ServizioProvider.tsx @@ -45,7 +45,6 @@ export const useServizio = () => { type ServizioAnnuncioProviderProps = { data: ServizioData["annunci"][number]; servizioId: ServizioServizioId; - userId: UsersId; children: React.ReactNode; }; @@ -60,7 +59,6 @@ const ServizioAnnuncioContext = export const ServizioAnnuncioProvider = ({ data, servizioId, - userId, children, }: ServizioAnnuncioProviderProps) => { const utils = api.useUtils(); @@ -70,10 +68,7 @@ export const ServizioAnnuncioProvider = ({ toast.error(error.message); }, onSuccess: async () => { - await utils.servizio.getAllServizioAnnunci.invalidate({ - userId: userId, - }); - await utils.servizio.getServizioData.invalidate({ servizioId }); + await utils.servizio.invalidate(); toast.success("Dati salvati con successo"); }, diff --git a/apps/infoalloggi/src/proxies/auth.ts b/apps/infoalloggi/src/proxies/auth.ts index 7ee021b..d295fdb 100644 --- a/apps/infoalloggi/src/proxies/auth.ts +++ b/apps/infoalloggi/src/proxies/auth.ts @@ -10,7 +10,7 @@ export const authProxy: ProxyFn = async (req: NextRequest) => { const path = req.nextUrl.pathname; const isOverridableRoute = - path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/") || + path.startsWith("/area-riservata/scheda-annuncio-stampa/") || path.startsWith("/servizio/condizioni/") || path.startsWith("/servizio/ricevuta-cortesia/"); // permette a puppeteer di accedere alla pagina di generazione PDF bypassando l'autenticazione normale diff --git a/apps/infoalloggi/src/schemas/public/Ordini.ts b/apps/infoalloggi/src/schemas/public/Ordini.ts index 29f7a0d..51ace68 100644 --- a/apps/infoalloggi/src/schemas/public/Ordini.ts +++ b/apps/infoalloggi/src/schemas/public/Ordini.ts @@ -5,6 +5,7 @@ import type { UsersId } from './Users'; import type { PrezziarioIdprezziario } from './Prezziario'; import type { ServizioServizioId } from './Servizio'; import type { default as OrderTypeEnum } from './OrderTypeEnum'; +import type { default as PaymentStatusEnum } from './PaymentStatusEnum'; import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; /** Identifier type for public.ordini */ @@ -25,6 +26,18 @@ export default interface OrdiniTable { type: ColumnType; isActive: ColumnType; + + amount_cent: ColumnType; + + paymentmethod: ColumnType; + + paid_at: ColumnType; + + intent_id: ColumnType; + + paymentstatus: ColumnType; + + sconto: ColumnType; } export type Ordini = Selectable; diff --git a/apps/infoalloggi/src/schemas/public/Servizio.ts b/apps/infoalloggi/src/schemas/public/Servizio.ts index 24e3115..e6d1dae 100644 --- a/apps/infoalloggi/src/schemas/public/Servizio.ts +++ b/apps/infoalloggi/src/schemas/public/Servizio.ts @@ -70,6 +70,8 @@ export default interface ServizioTable { skipControlloDoc: ColumnType; skipDocMotivazione: ColumnType; + + onboardOk: ColumnType; } export type Servizio = Selectable; diff --git a/apps/infoalloggi/src/server/api/routers/pagamenti.ts b/apps/infoalloggi/src/server/api/routers/pagamenti.ts index fd0f4a4..6465450 100644 --- a/apps/infoalloggi/src/server/api/routers/pagamenti.ts +++ b/apps/infoalloggi/src/server/api/routers/pagamenti.ts @@ -1,9 +1,15 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod/v4"; +import type OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; -import { getPagamentoDataForCheckout } from "~/server/controllers/servizio.controller"; +import { + getPagamentoDataForCheckout, + preparePayment, +} from "~/server/controllers/pagamenti.controller"; + import { db } from "~/server/db"; -import { zPagamentoId } from "~/server/utils/zod_types"; +import { genPdfRicevutaCortesia } from "~/server/services/puppeteer.service"; +import { zOrdineId, zServizioId } from "~/server/utils/zod_types"; // Create a new ratelimiter, that allows 10 requests per 10 seconds /* const ratelimit = new RateLimiterHandler({ @@ -18,24 +24,45 @@ export const pagamentiRouter = createTRPCRouter({ .input(z.object({ pIntentId: z.string() })) .query(async ({ input }) => { try { - return ( - (await db - .selectFrom("payments") - .selectAll("payments") - .where("payments.intent_id", "=", input.pIntentId) - .executeTakeFirst()) || null - ); + const ordine = await db + .selectFrom("ordini") + .selectAll("ordini") + .where("ordini.intent_id", "=", input.pIntentId) + .executeTakeFirst(); + return ordine || null; } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Error retriving payment: ${(error as Error).message}`, + message: `Errore durante il recupero del pagamento: ${(error as Error).message}`, }); } }), + preparePayment: protectedProcedure + .input( + z.object({ + servizioId: zServizioId, + type: z.custom(), + }), + ) + .query(async ({ input }) => { + return await preparePayment(input.servizioId, input.type); + }), getPagamentoDataForCheckout: protectedProcedure - .input(z.object({ paymentId: zPagamentoId })) + .input(z.object({ ordineId: zOrdineId })) .query(async ({ input }) => { - return await getPagamentoDataForCheckout(input.paymentId); + return await getPagamentoDataForCheckout(input.ordineId); + }), + getRicevutaCortesiaPdf: protectedProcedure + .input(z.object({ ordineId: zOrdineId })) + .mutation(async ({ input }) => { + try { + return await genPdfRicevutaCortesia(input.ordineId); + } catch (e) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Errore nella generazione della ricevuta di cortesia: ${(e as Error).message}`, + }); + } }), }); diff --git a/apps/infoalloggi/src/server/api/routers/prezziario.ts b/apps/infoalloggi/src/server/api/routers/prezziario.ts index 763038c..d4af17f 100644 --- a/apps/infoalloggi/src/server/api/routers/prezziario.ts +++ b/apps/infoalloggi/src/server/api/routers/prezziario.ts @@ -84,7 +84,7 @@ export const prezziarioRouter = createTRPCRouter({ getPrezzoPerServizio: publicProcedure .input(z.object({ idprezziario: zPrezziarioId })) .query(async ({ input }) => { - return await getPrezziarioByIdHandler({ id: input.idprezziario }); + return await getPrezziarioByIdHandler(input.idprezziario); }), updatePrezziario: adminProcedure .input( diff --git a/apps/infoalloggi/src/server/api/routers/servizio.ts b/apps/infoalloggi/src/server/api/routers/servizio.ts index d8fdbdf..9997a94 100644 --- a/apps/infoalloggi/src/server/api/routers/servizio.ts +++ b/apps/infoalloggi/src/server/api/routers/servizio.ts @@ -1,6 +1,5 @@ -import { z } from "zod/v4"; +import z from "zod"; import type { NewOrdini, OrdiniUpdate } from "~/schemas/public/Ordini"; -import type { PaymentsUpdate } from "~/schemas/public/Payments"; import type { NewServizio, ServizioUpdate } from "~/schemas/public/Servizio"; import type { ServizioAnnunciUpdate } from "~/schemas/public/ServizioAnnunci"; import type { Users } from "~/schemas/public/Users"; @@ -18,34 +17,27 @@ import { removeOrder, updateOrder, } from "~/server/controllers/ordini.controller"; +import { getPacksPerServizio } from "~/server/controllers/pagamenti.controller"; import { addServizio, addServizioAnnunci, adminUpdateConfermaStatus, + attivaServizio_SaltaPagamentoAcconto, deleteServizio, deleteServizioAnnuncio, - getAllPagamenti, getAllServizioAnnunci, getAnnunciAvailableToAdd, getCompatibileAnnunci, getDataPerAcquisto, getOrdineRicevutaData, - getPacksPerServizio, getServiziByUserId, getServizioAnnuncio, getServizioById, getServizioDataById, - getServizioPagamentoData, - getUserPagamenti, InteractionLogicTipologia, interruzioneServizio, - isOrdineAwaitingPayment, - postAcquistoDataHandler, - removePagamento, + processServizioOnboard, SbloccaContatti, - setupSaldoConsulenzaServizio, - setupSaldoServizio, - updatePagamento, updateServizio, updateServizioAnnuncio, userAccettaConferma, @@ -54,7 +46,6 @@ import { import { zAnnuncioId, zOrdineId, - zPagamentoId, zServizioId, zUserId, } from "~/server/utils/zod_types"; @@ -116,9 +107,7 @@ export const servizioRouter = createTRPCRouter({ .query(async ({ input }) => { return await getDataPerAcquisto(input.servizioId); }), - getAllPagamenti: adminProcedure.query(async () => { - return await getAllPagamenti(); - }), + getAllServizioAnnunci: protectedProcedure .input( z.object({ @@ -185,6 +174,7 @@ export const servizioRouter = createTRPCRouter({ .query(async ({ input }) => { return await getOrdini(input.userId); }), + isOnboardAvailable: protectedProcedure .input( z.object({ @@ -229,24 +219,6 @@ export const servizioRouter = createTRPCRouter({ servizioId: input.servizioId, }); }), - getServizioPaymentData: protectedProcedure - .input( - z.object({ - ordineId: zOrdineId, - }), - ) - .query(async ({ input }) => { - return await getServizioPagamentoData(input.ordineId); - }), - getUserPagamenti: protectedProcedure - .input( - z.object({ - userId: zUserId, - }), - ) - .query(async ({ input }) => { - return await getUserPagamenti(input.userId); - }), getUserServizi: adminProcedure .input( @@ -266,17 +238,8 @@ export const servizioRouter = createTRPCRouter({ .mutation(async ({ input }) => { return await interruzioneServizio(input.servizioId); }), - isOrdineAwaitingPayment: protectedProcedure - .input( - z.object({ - ordineId: zOrdineId, - }), - ) - .query(async ({ input }) => { - return await isOrdineAwaitingPayment(input.ordineId); - }), - submitOnboardForPurchase: protectedProcedure + processServizioOnboard: protectedProcedure .input( z.object({ anagrafica: z.custom(), @@ -289,10 +252,20 @@ export const servizioRouter = createTRPCRouter({ }), ) .mutation(async ({ input }) => { - return await postAcquistoDataHandler({ + return await processServizioOnboard({ ...input, }); }), + attivazioneSenzaPagamento: protectedProcedure + .input( + z.object({ + ordineId: zOrdineId, + }), + ) + .mutation(async ({ input }) => { + return await attivaServizio_SaltaPagamentoAcconto(input.ordineId); + }), + removeAnnuncioServizio: protectedProcedure .input( z.object({ @@ -312,42 +285,7 @@ export const servizioRouter = createTRPCRouter({ .mutation(async ({ input }) => { return await removeOrder(input.ordineId); }), - removePagamento: adminProcedure - .input( - z.object({ - pagamentoId: zPagamentoId, - }), - ) - .mutation(async ({ input }) => { - return await removePagamento(input.pagamentoId); - }), - setupSaldoConsulenzaServizio: protectedProcedure - .input( - z.object({ - annuncioId: zAnnuncioId, - servizioId: zServizioId, - }), - ) - .mutation(async ({ input }) => { - return await setupSaldoConsulenzaServizio({ - annuncioId: input.annuncioId, - servizioId: input.servizioId, - }); - }), - setupSaldoServizio: protectedProcedure - .input( - z.object({ - annuncioId: zAnnuncioId, - servizioId: zServizioId, - }), - ) - .mutation(async ({ input }) => { - return await setupSaldoServizio({ - annuncioId: input.annuncioId, - servizioId: input.servizioId, - }); - }), getPacksSolution: protectedProcedure .input( z.object({ @@ -392,19 +330,7 @@ export const servizioRouter = createTRPCRouter({ ordineId: input.ordineId, }); }), - updatePagamento: adminProcedure - .input( - z.object({ - data: z.custom(), - pagamentoId: zPagamentoId, - }), - ) - .mutation(async ({ input }) => { - return await updatePagamento({ - data: input.data, - paymentId: input.pagamentoId, - }); - }), + updateServizio: protectedProcedure .input( z.object({ diff --git a/apps/infoalloggi/src/server/api/routers/stripe.ts b/apps/infoalloggi/src/server/api/routers/stripe.ts index b5bed9f..7108a24 100644 --- a/apps/infoalloggi/src/server/api/routers/stripe.ts +++ b/apps/infoalloggi/src/server/api/routers/stripe.ts @@ -3,42 +3,30 @@ import { adminProcedure, createTRPCRouter, protectedApiProcedure, - protectedProcedure, } from "~/server/api/trpc"; import { - createIntentHandler, stripeHealthCheck, whIntentCreatedHandler, whIntentFailedHandler, whIntentSucceededHandler, } from "~/server/controllers/stripe.controller"; -import { zPagamentoId } from "~/server/utils/zod_types"; +import { zOrdineId } from "~/server/utils/zod_types"; export const stripeRouter = createTRPCRouter({ health: adminProcedure.query(async () => { return await stripeHealthCheck(); }), - createIntent: protectedProcedure - .input( - z.object({ - paymentId: zPagamentoId, - }), - ) - .mutation(async ({ input }) => { - return await createIntentHandler({ - paymentId: input.paymentId, - }); - }), + whIntentCreated: protectedApiProcedure .input( z.object({ - paymentId: zPagamentoId, + ordineId: zOrdineId, pIntentId: z.string(), }), ) .mutation(async ({ input }) => { return await whIntentCreatedHandler({ - paymentId: input.paymentId, + ordineId: input.ordineId, pIntentId: input.pIntentId, }); }), @@ -53,14 +41,14 @@ export const stripeRouter = createTRPCRouter({ whIntentSucceeded: protectedApiProcedure .input( z.object({ - paymentId: zPagamentoId, + ordineId: zOrdineId, pIntentId: z.string(), pm_id: z.string(), }), ) .mutation(async ({ input }) => { return await whIntentSucceededHandler({ - paymentId: input.paymentId, + ordineId: input.ordineId, pIntentId: input.pIntentId, pm_id: input.pm_id, }); diff --git a/apps/infoalloggi/src/server/controllers/ordini.controller.ts b/apps/infoalloggi/src/server/controllers/ordini.controller.ts index 0277a7d..c585bc7 100644 --- a/apps/infoalloggi/src/server/controllers/ordini.controller.ts +++ b/apps/infoalloggi/src/server/controllers/ordini.controller.ts @@ -1,31 +1,23 @@ import { TRPCError } from "@trpc/server"; -import { jsonArrayFrom } from "kysely/helpers/postgres"; import type { NewOrdini, + Ordini, OrdiniOrdineId, OrdiniUpdate, } from "~/schemas/public/Ordini"; -import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; -import type { UsersId } from "~/schemas/public/Users"; -import { db, type Querier } from "~/server/db"; +import type { Users, UsersId } from "~/schemas/public/Users"; +import { db } from "~/server/db"; -export type OrdiniWPagamenti = Awaited>[number]; -export const getOrdini = async (userId: UsersId | undefined) => { +export type Ordini_w_Utente = Ordini & Pick; +export const getOrdini = async ( + userId: UsersId | undefined, +): Promise => { try { let qry = db .selectFrom("ordini") .selectAll("ordini") .innerJoin("users", "users.id", "ordini.userid") - .select("users.username") - .select((eb) => [ - jsonArrayFrom( - eb - .selectFrom("payments") - .selectAll("payments") - .whereRef("payments.ordine_id", "=", "ordini.ordine_id") - .orderBy("payments.created_at", "desc"), - ).as("pagamenti"), - ]); + .select("users.username"); if (userId) { qry = qry.where("ordini.userid", "=", userId); @@ -46,15 +38,6 @@ export const getOrdineById = async (ordineId: OrdiniOrdineId) => { .selectAll("ordini") .innerJoin("users", "users.id", "ordini.userid") .select("users.username") - .select((eb) => [ - jsonArrayFrom( - eb - .selectFrom("payments") - .selectAll("payments") - .whereRef("payments.ordine_id", "=", "ordini.ordine_id") - .orderBy("payments.created_at", "desc"), - ).as("pagamenti"), - ]) .where("ordini.ordine_id", "=", ordineId) .orderBy("created_at", "desc") .executeTakeFirstOrThrow( @@ -70,38 +53,11 @@ export const getOrdineById = async (ordineId: OrdiniOrdineId) => { export const createOrdine = async (data: NewOrdini) => { try { - return await db.transaction().execute(async (tx) => { - const prezzo = await tx - .selectFrom("prezziario") - .select(["prezzo_cent", "nome_it"]) - .where("prezziario.idprezziario", "=", data.packid) - .executeTakeFirstOrThrow( - () => new Error(`Prezziario not found - packid: ${data.packid}`), - ); - - const order = await tx - .insertInto("ordini") - .values(data) - .returningAll() - .executeTakeFirstOrThrow(() => new Error("Failed to create ordine")); - await tx - .insertInto("payments") - .values({ - ordine_id: order.ordine_id, - servizio_id: order.servizio_id, - userid: order.userid, - amount_cent: prezzo.prezzo_cent, - paymentname: prezzo.nome_it, - paymentmethod: "manual", - paymentstatus: data.isActive - ? PaymentStatusEnum.success - : PaymentStatusEnum.processing, - paid_at: data.isActive ? new Date() : null, - }) - .execute(); - - return order; - }); + return await db + .insertInto("ordini") + .values(data) + .returningAll() + .executeTakeFirstOrThrow(() => new Error("Failed to create ordine")); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -146,29 +102,3 @@ export const updateOrder = async ({ }); } }; - -export const cleanupUnusedOrders = async ({ db }: { db: Querier }) => { - try { - const orders = await db - .selectFrom("ordini") - .select("ordini.ordine_id") - .leftJoin("payments", "payments.ordine_id", "ordini.ordine_id") - .where("payments.paymentstatus", "is", null) - .where("ordini.isActive", "=", false) - .distinct() - .execute(); - - if (orders.length === 0) { - return "ok"; - } - const ordineIds = orders.map((o) => o.ordine_id); - await db.deleteFrom("ordini").where("ordine_id", "in", ordineIds).execute(); - - return "ok"; - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error cleaning up old orders: ${(e as Error).message}`, - }); - } -}; diff --git a/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts b/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts new file mode 100644 index 0000000..7177a37 --- /dev/null +++ b/apps/infoalloggi/src/server/controllers/pagamenti.controller.ts @@ -0,0 +1,345 @@ +import { TRPCError } from "@trpc/server"; +import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; +import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; +import type { + Prezziario, + PrezziarioIdprezziario, +} from "~/schemas/public/Prezziario"; +import type { Servizio, ServizioServizioId } from "~/schemas/public/Servizio"; +import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; +import { db } from "../db"; +import { getPrezziarioByIdHandler } from "../services/prezziario.service"; +import { createOrdine } from "./ordini.controller"; +import { getServizioById } from "./servizio.controller"; + +export const ACCONTO_TRANSITORIO = "ACCONTO_BD" as PrezziarioIdprezziario; +export const ACCONTO_STABILE = "ACCONTO_CA" as PrezziarioIdprezziario; + +export type PreparedPaymentData = { + ordine_id: OrdiniOrdineId; + packid: PrezziarioIdprezziario; + amount_cent: number; + sconto: number; + packName: string; + packDesc: string; + condizioni: string; +}; +export const preparePayment = async ( + servizioId: ServizioServizioId, + type: OrderTypeEnum, +): Promise => { + try { + const servizio = await getServizioById(servizioId); + const inactiveOrders = await db + .selectFrom("ordini") + .selectAll() + .where("servizio_id", "=", servizioId) + .where("isActive", "=", false) + .where("paymentstatus", "is", null) + .execute(); + + switch (type) { + case OrderTypeEnum.Acconto: { + if (!servizio.isOkAcconto) { + const acconto = await AccontoSolver(servizio); + if (!acconto.success) { + throw new Error( + `Acconto non trovato per il servizio: ${acconto.message}`, + ); + } + if (!acconto.pack.testo_condizioni) { + throw new Error("Condizioni non trovate per acconto"); + } + + // servizio sta aspettando attivazione acconto + + const preExistingAcconto = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Acconto, + ); + + if (preExistingAcconto) { + // acconto già creato, procedere con questo + return { + ordine_id: preExistingAcconto.ordine_id, + packid: preExistingAcconto.packid, + amount_cent: preExistingAcconto.amount_cent, + sconto: preExistingAcconto.sconto, + packName: acconto.pack.nome_it, + packDesc: acconto.pack.desc_it, + condizioni: acconto.pack.testo_condizioni, + }; + } + // nessun acconto preesistente, crearne uno nuovo + + const ordine = await createOrdine({ + servizio_id: servizioId, + amount_cent: acconto.pack.prezzo_cent, + packid: acconto.pack.idprezziario, + type: OrderTypeEnum.Acconto, + userid: servizio.user_id, + }); + return { + ordine_id: ordine.ordine_id, + packid: acconto.pack.idprezziario, + amount_cent: acconto.pack.prezzo_cent, + sconto: 0, + packName: acconto.pack.nome_it, + packDesc: acconto.pack.desc_it, + condizioni: acconto.pack.testo_condizioni, + }; + } + + throw new Error( + "Il servizio ha già un acconto attivo, non è possibile preparare un nuovo pagamento", + ); + } + case OrderTypeEnum.Saldo: { + if (!servizio.isOkAcconto) { + throw new Error( + "Non è possibile procedere con il pagamento del saldo prima di aver attivato l'acconto", + ); + } + if (!servizio.isOkSaldo) { + //find saldo pack based on servizio tipologia and permanenza/budget, if tipologia posizione + const saldo = await SaldoSolver(servizio); + if (!saldo.success) { + throw new Error( + `Saldo non trovato per il servizio: ${saldo.message}`, + ); + } + if (!saldo.pack.testo_condizioni) { + throw new Error("Condizioni non trovate per saldo"); + } + const preExistingSaldo = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Saldo, + ); + if (preExistingSaldo) { + // saldo già creato, procedere con questo + return { + ordine_id: preExistingSaldo.ordine_id, + packid: preExistingSaldo.packid, + amount_cent: preExistingSaldo.amount_cent, + sconto: preExistingSaldo.sconto, + packName: saldo.pack.nome_it, + packDesc: saldo.pack.desc_it, + condizioni: saldo.pack.testo_condizioni, + }; + } + // nessun saldo preesistente, crearne uno nuovo + const ordine = await createOrdine({ + servizio_id: servizioId, + amount_cent: saldo.pack.prezzo_cent, + packid: saldo.pack.idprezziario, + type: OrderTypeEnum.Saldo, + userid: servizio.user_id, + }); + return { + ordine_id: ordine.ordine_id, + packid: saldo.pack.idprezziario, + amount_cent: saldo.pack.prezzo_cent, + sconto: 0, + packName: saldo.pack.nome_it, + packDesc: saldo.pack.desc_it, + condizioni: saldo.pack.testo_condizioni, + }; + } + throw new Error( + "Il servizio ha già un saldo attivo, non è possibile preparare un nuovo pagamento", + ); + } + + case OrderTypeEnum.Consulenza: { + if (!servizio.isOkAcconto || !servizio.isOkSaldo) { + throw new Error( + "Non è possibile procedere con il pagamento della consulenza prima di aver attivato acconto e saldo", + ); + } + if (!servizio.isOkConsulenza) { + const existingConsulenza = inactiveOrders.find( + (ordine) => ordine.type === OrderTypeEnum.Consulenza, + ); + if (existingConsulenza) { + // consulenza già creato, procedere con questo + const consulenzaPack = await getPrezziarioByIdHandler( + existingConsulenza.packid, + ); + if (!consulenzaPack) { + throw new Error("Pack consulenza non trovato"); + } + if (!consulenzaPack.testo_condizioni) { + throw new Error("Condizioni non trovate per consulenza"); + } + return { + ordine_id: existingConsulenza.ordine_id, + packid: existingConsulenza.packid, + amount_cent: existingConsulenza.amount_cent, + sconto: existingConsulenza.sconto, + packName: consulenzaPack.nome_it, + packDesc: consulenzaPack.desc_it, + condizioni: consulenzaPack.testo_condizioni, + }; + } + //todo vedere se oltre a emettere un errore si vuole anche passare un messaggio alla pagina di errore + throw new Error( + "Il servizio non ha un pacchetto di consulenza, è necessario inserirlo prima di procedere", + ); + } + + throw new Error( + "Il servizio ha già una consulenza attiva, non è possibile preparare un nuovo pagamento", + ); + } + + default: + throw new Error("Tipo di ordine non valido"); + } + } catch (e) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Errore durante la preparazione del pagamento: ${(e as Error).message}`, + }); + } +}; + +export const getPagamentoDataForCheckout = async (ordineId: OrdiniOrdineId) => { + try { + return await db + .selectFrom("ordini") + .select([ + "ordini.ordine_id", + "ordini.packid", + "ordini.amount_cent", + "ordini.servizio_id", + "ordini.userid", + "ordini.sconto", + ]) + .innerJoin("users", "users.id", "ordini.userid") + .select(["users.username", "users.email"]) + .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") + .select(["prezziario.nome_it", "prezziario.desc_it"]) + .where("ordini.ordine_id", "=", ordineId) + .executeTakeFirstOrThrow( + () => new Error(`Payment not found - ordineId: ${ordineId}`), + ); + } catch (e) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `Payment not found: ${(e as Error).message}`, + }); + } +}; + +const SaldoTransitorioMapping: Record = { + 0: "SALDOBD_1MESE" as PrezziarioIdprezziario, + 1: "SALDOBD_3MESI" as PrezziarioIdprezziario, + 2: "SALDOBD_6MESI" as PrezziarioIdprezziario, + 3: "SALDOBD_9MESI" as PrezziarioIdprezziario, + 4: "SALDOBD_12MESI" as PrezziarioIdprezziario, +}; + +// const ConsulenzaPriceMapping = { +// "4 + 4": "CONS_4+4" as PrezziarioIdprezziario, +// "30 Giorni": "CONS_30GG" as PrezziarioIdprezziario, +// Agevolato: "CONS_3+2" as PrezziarioIdprezziario, +// Commerciale: "CONS_COMM" as PrezziarioIdprezziario, +// Foresteria: "CONS_COMM" as PrezziarioIdprezziario, +// "Transi.Age": "CONS_3+2" as PrezziarioIdprezziario, +// Transitorio: "CONS_TRAN" as PrezziarioIdprezziario, +// }; + +type GetPackError = { success: false; message: string }; +type GetPackResult = { success: true; pack: Prezziario }; + +type ServizioPacks = { + acconto: GetPackResult | GetPackError; + saldo: GetPackResult | GetPackError; +}; + +const AccontoSolver = async ( + servizio: Servizio, +): Promise => { + switch (servizio.tipologia) { + case TipologiaPosizioneEnum.Transitorio: { + const pack = await getPrezziarioByIdHandler(ACCONTO_TRANSITORIO); + + if (!pack) { + return { success: false, message: "Pack non trovato" }; + } + return { + success: true, + pack, + }; + } + + case TipologiaPosizioneEnum.Stabile: { + const pack = await getPrezziarioByIdHandler(ACCONTO_STABILE); + + if (!pack) { + return { success: false, message: "Pack non trovato" }; + } + return { + success: true, + pack, + }; + } + default: + return { success: false, message: "Seleziona tipologia" }; + } +}; + +export const SaldoSolver = async ( + servizio: Servizio, +): Promise => { + switch (servizio.tipologia) { + case TipologiaPosizioneEnum.Transitorio: { + const mpp = + servizio.permanenza && SaldoTransitorioMapping[servizio.permanenza]; + if (!servizio.permanenza || !mpp) { + return { + success: false, + message: "Permanenza non selezionata", + }; + } + const pack = await getPrezziarioByIdHandler(mpp); + if (!pack) { + return { success: false, message: "Pack non trovato" }; + } + + return { success: true, pack }; + } + case TipologiaPosizioneEnum.Stabile: { + const pack = await getPrezziarioByIdHandler( + (servizio.budget > 700 + ? "SALDOCA700+" + : servizio.budget > 500 + ? "SALDOCA500+" + : "SALDOCA500") as PrezziarioIdprezziario, + ); + if (!pack) { + return { success: false, message: "Pack non trovato" }; + } + return { + success: true, + pack, + }; + } + default: + return { success: false, message: "Seleziona tipologia" }; + } +}; + +export const getPacksPerServizio = async ( + servizioId: ServizioServizioId, +): Promise => { + const servizio = await getServizioById(servizioId); + if (!servizio) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Servizio non trovato", + }); + } + return { + acconto: await AccontoSolver(servizio), + saldo: await SaldoSolver(servizio), + }; +}; diff --git a/apps/infoalloggi/src/server/controllers/servizio.controller.ts b/apps/infoalloggi/src/server/controllers/servizio.controller.ts index a59a336..4960577 100644 --- a/apps/infoalloggi/src/server/controllers/servizio.controller.ts +++ b/apps/infoalloggi/src/server/controllers/servizio.controller.ts @@ -1,23 +1,14 @@ import { TRPCError } from "@trpc/server"; import { add } from "date-fns"; -import { sql } from "kysely"; import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres"; import { z } from "zod/v4"; import type { PurchaseData } from "~/components/acquisto_receipt"; import { env } from "~/env"; import type { Annunci, AnnunciId } from "~/schemas/public/Annunci"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; -import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; +import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; -import type { - Payments, - PaymentsId, - PaymentsUpdate, -} from "~/schemas/public/Payments"; -import type { - Prezziario, - PrezziarioIdprezziario, -} from "~/schemas/public/Prezziario"; +import type { Prezziario } from "~/schemas/public/Prezziario"; import type { NewServizio, Servizio, @@ -28,6 +19,7 @@ import type { ServizioAnnunci, ServizioAnnunciUpdate, } from "~/schemas/public/ServizioAnnunci"; +import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import type { Users, UsersId } from "~/schemas/public/Users"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; @@ -37,7 +29,6 @@ import { canSendNotification, nextTimeForNotification, } from "~/server/controllers/event_queue.controller"; -import { cleanupUnusedOrders } from "~/server/controllers/ordini.controller"; import { type MinimalSMSData, SendMessage, @@ -52,18 +43,47 @@ import { } from "~/utils/kysely-helper"; import { GetUserInterests } from "../services/interests.service"; import { getPrezziarioByIdHandler } from "../services/prezziario.service"; -import { genPdfSchedaAnnuncioBase64 } from "../services/puppeteer.service"; +import { genPdfCondizioniBase64 } from "../services/puppeteer.service"; import type { AnnuncioRicerca } from "./annunci.controller"; +import { createOrdine } from "./ordini.controller"; +import { + ACCONTO_STABILE, + ACCONTO_TRANSITORIO, + SaldoSolver, +} from "./pagamenti.controller"; export const addServizio = async (data: NewServizio) => { try { - return await db - .insertInto("servizio") - .values(data) - .returningAll() - .executeTakeFirstOrThrow( - () => new Error("Failed to insert new servizio into database"), + return await db.transaction().execute(async (tx) => { + const servizio = await tx + .insertInto("servizio") + .values(data) + .returningAll() + .executeTakeFirstOrThrow( + () => new Error("Failed to insert new servizio into database"), + ); + + const acconto = await getPrezziarioByIdHandler( + data.tipologia === TipologiaPosizioneEnum.Transitorio + ? ACCONTO_TRANSITORIO + : ACCONTO_STABILE, ); + if (!acconto) { + throw new Error("Acconto not found for new servizio"); + } + await tx + .insertInto("ordini") + .values({ + servizio_id: servizio.servizio_id, + userid: servizio.user_id, + type: OrderTypeEnum.Acconto, + amount_cent: acconto.prezzo_cent, + packid: acconto.idprezziario, + isActive: data.skipPayment || false, + }) + .execute(); + return servizio; + }); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -208,7 +228,7 @@ export const getDataPerAcquisto = async (servizioId: ServizioServizioId) => { } }; -export const postAcquistoDataHandler = async ({ +export const processServizioOnboard = async ({ servizioId, userId, user, @@ -220,9 +240,9 @@ export const postAcquistoDataHandler = async ({ user: Pick; anagrafica: UsersAnagraficaUpdate; servizio: ServizioUpdate; -}): Promise<{ ordineIdForPayment: OrdiniOrdineId | null }> => { +}) => { try { - const servizioData = await db.transaction().execute(async (tx) => { + await db.transaction().execute(async (tx) => { // Update user and anagrafica data await tx .updateTable("users") @@ -261,7 +281,7 @@ export const postAcquistoDataHandler = async ({ // Update servizio data return await tx .updateTable("servizio") - .set(servizio) + .set({ ...servizio, onboardOk: true }) .where("servizio_id", "=", servizioId) .returningAll() .executeTakeFirstOrThrow( @@ -270,88 +290,7 @@ export const postAcquistoDataHandler = async ({ ); }); - 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 { ordineIdForPayment: null }; - } - //cleanup unused payments - - await db - .deleteFrom("payments") - .where("servizio_id", "=", servizioId) - .where("paymentstatus", "is", null) - .execute(); - - if ( - !servizioData || - servizioData.isInterrotto || - servizioData.isOkAcconto || - servizioData.decorrenza !== null - ) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Cannot create payment for this servizioData", - }); - } - const packId = - servizioData.tipologia === TipologiaPosizioneEnum.Transitorio - ? ("ACCONTO_BD" as PrezziarioIdprezziario) - : ("ACCONTO_CA" as PrezziarioIdprezziario); - - const acconto = await db - .selectFrom("prezziario") - .select("idprezziario") - .where("isActive", "=", true) - .where("idprezziario", "=", packId) - .executeTakeFirst(); - - if (!acconto) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Acconto not found", - }); - } - - const existingOrder = await db - .selectFrom("ordini") - .select("ordini.ordine_id") - .where("ordini.servizio_id", "=", servizioData.servizio_id) - .where("ordini.packid", "=", acconto.idprezziario) - .where("ordini.type", "=", OrderTypeEnum.Acconto) - .where("ordini.isActive", "=", false) - .orderBy("ordini.created_at", "desc") - .executeTakeFirst(); - - if (existingOrder) { - await db - .deleteFrom("ordini") - .where("ordini.isActive", "=", false) - .where("ordine_id", "!=", existingOrder.ordine_id) - .execute(); - return { ordineIdForPayment: existingOrder.ordine_id }; - } - - const order = await db - .insertInto("ordini") - .values({ - packid: acconto.idprezziario, - servizio_id: servizioData.servizio_id, - type: OrderTypeEnum.Acconto, - userid: userId, - }) - .returning("ordine_id") - .executeTakeFirstOrThrow(() => new Error("Failed to create ordine")); - - return { ordineIdForPayment: order.ordine_id }; + return true; } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", @@ -360,349 +299,72 @@ export const postAcquistoDataHandler = async ({ } }; -const SaldoTransitorioMapping: Record = { - 0: "SALDOBD_1MESE" as PrezziarioIdprezziario, - 1: "SALDOBD_3MESI" as PrezziarioIdprezziario, - 2: "SALDOBD_6MESI" as PrezziarioIdprezziario, - 3: "SALDOBD_9MESI" as PrezziarioIdprezziario, - 4: "SALDOBD_12MESI" as PrezziarioIdprezziario, -}; - -export const setupSaldoServizio = async ({ - servizioId, - annuncioId, -}: { - servizioId: ServizioServizioId; - annuncioId: AnnunciId; -}) => { +/** + * Attiva il servizio senza pagamento,\ + * usato se un acconto già attivo è presente per il servizio,\ + * invio delle condizioni contrattuali in allegato. + */ +export const attivaServizio_SaltaPagamentoAcconto = async ( + ordineId: OrdiniOrdineId, +) => { try { return await db.transaction().execute(async (tx) => { - // Cleanup previous payments and orders - await cleanupUnusedOrders({ db: tx }); - - const servizio = await tx - .selectFrom("servizio") - .selectAll() - .where("servizio_id", "=", servizioId) + const ordine = await tx + .selectFrom("ordini") + .innerJoin("users", "users.id", "ordini.userid") + .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") + .select([ + "ordini.ordine_id", + "ordini.servizio_id", + "ordini.created_at", + "prezziario.testo_condizioni", + "users.email", + "users.id as userId", + ]) + .where("ordine_id", "=", ordineId) .executeTakeFirstOrThrow( - () => new Error(`Servizio not found - servizioId: ${servizioId}`), + () => new Error(`Ordine not found - ordineId: ${ordineId}`), ); - if ( - !servizio.isOkAcconto || - servizio.isInterrotto || - servizio.decorrenza == null || - servizio.isOkSaldo - ) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Servizio not found", - }); - } - - const annuncio = await tx - .selectFrom("servizio_annunci") - .selectAll() - .where("servizio_id", "=", servizioId) - .where("annunci_id", "=", annuncioId) - .executeTakeFirstOrThrow( - () => - new Error( - `Annuncio not found - servizioId: ${servizioId}, annunciId: ${annuncioId}`, - ), - ); - if ( - annuncio.user_confirmed_at == null || - annuncio.accettato_conferma_at == null - ) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Annuncio not found", - }); - } - let saldo: - | Pick< - Prezziario, - "idprezziario" | "prezzo_cent" | "nome_it" | "nome_en" | "sconto" - > - | undefined; - - const qry = tx - .selectFrom("prezziario") - .select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"]) - .where("isActive", "=", true); - - if (servizio.tipologia === TipologiaPosizioneEnum.Transitorio) { - const mpp = - servizio.permanenza && SaldoTransitorioMapping[servizio.permanenza]; - if (!servizio.permanenza || !mpp) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Permanenza not set for transitorio servizio", - }); - } - saldo = await qry.where("idprezziario", "=", mpp).executeTakeFirst(); - } else if (servizio.tipologia === TipologiaPosizioneEnum.Stabile) { - if (servizio.budget > 700) { - saldo = await qry - .where("idprezziario", "=", "SALDOCA700+" as PrezziarioIdprezziario) - .executeTakeFirst(); - } else if (servizio.budget > 500) { - saldo = await qry - .where("idprezziario", "=", "SALDOCA500+" as PrezziarioIdprezziario) - - .executeTakeFirst(); - } else { - saldo = await qry - .where("idprezziario", "=", "SALDOCA500" as PrezziarioIdprezziario) - .executeTakeFirst(); - } - } - - if (!saldo) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Acconto not found", - }); - } await tx - .deleteFrom("payments") - .where("servizio_id", "=", servizioId) - .where("paymentstatus", "is", null) + .updateTable("servizio") + .set({ + decorrenza: new Date(), + isOkAcconto: true, + }) + .where("servizio_id", "=", ordine.servizio_id) .execute(); - const order = await db - .insertInto("ordini") - .values({ - packid: saldo.idprezziario, - servizio_id: servizio.servizio_id, - type: OrderTypeEnum.Saldo, - userid: servizio.user_id, - }) - .returning("ordine_id") - .executeTakeFirstOrThrow(() => new Error("Failed to create ordine")); + await NewMail({ + template: { + mailType: "servizioAttivato", + }, + mail: { + subject: "Pagamento Confermato - Acconto Infoalloggi.it", + to: ordine.email, + attachments: ordine.testo_condizioni + ? [ + { + filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`, - return order.ordine_id; + content: await genPdfCondizioniBase64( + ordine.testo_condizioni as TestiEStringheStingaId, + ), + + encoding: "base64", + contentType: "application/pdf", + }, + ] + : [], + }, + userId: ordine.userId, + }); + return true; }); } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Error setting up saldo servizio: ${(e as Error).message}`, - }); - } -}; - -const ConsulenzaPriceMapping = { - "4 + 4": "CONS_4+4" as PrezziarioIdprezziario, - "30 Giorni": "CONS_30GG" as PrezziarioIdprezziario, - Agevolato: "CONS_3+2" as PrezziarioIdprezziario, - Commerciale: "CONS_COMM" as PrezziarioIdprezziario, - Foresteria: "CONS_COMM" as PrezziarioIdprezziario, - "Transi.Age": "CONS_3+2" as PrezziarioIdprezziario, - Transitorio: "CONS_TRAN" as PrezziarioIdprezziario, -}; - -type GetPackError = { success: false; message: string }; -type GetPackResult = { success: true; pack: Prezziario }; - -type ServizioPacks = { - acconto: GetPackResult | GetPackError; - saldo: GetPackResult | GetPackError; -}; -export const getPacksPerServizio = async ( - servizioId: ServizioServizioId, -): Promise => { - const servizio = await getServizioById(servizioId); - if (!servizio) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Servizio non trovato", - }); - } - - const getAcconto = async (): Promise => { - switch (servizio.tipologia) { - case TipologiaPosizioneEnum.Transitorio: { - const pack = await getPrezziarioByIdHandler({ - id: "ACCONTO_BD" as PrezziarioIdprezziario, - }); - - if (!pack) { - return { success: false, message: "Pack non trovato" }; - } - return { - success: true, - pack, - }; - } - - case TipologiaPosizioneEnum.Stabile: { - const pack = await getPrezziarioByIdHandler({ - id: "ACCONTO_CA" as PrezziarioIdprezziario, - }); - - if (!pack) { - return { success: false, message: "Pack non trovato" }; - } - return { - success: true, - pack, - }; - } - default: - return { success: false, message: "Seleziona tipologia" }; - } - }; - - const getSaldo = async (): Promise => { - switch (servizio.tipologia) { - case TipologiaPosizioneEnum.Transitorio: { - const mpp = - servizio.permanenza && SaldoTransitorioMapping[servizio.permanenza]; - if (!servizio.permanenza || !mpp) { - return { - success: false, - message: "Permanenza non selezionata", - }; - } - const pack = await getPrezziarioByIdHandler({ id: mpp }); - if (!pack) { - return { success: false, message: "Pack non trovato" }; - } - - return { success: true, pack }; - } - case TipologiaPosizioneEnum.Stabile: { - const pack = await getPrezziarioByIdHandler({ - id: (servizio.budget > 700 - ? "SALDOCA700+" - : servizio.budget > 500 - ? "SALDOCA500+" - : "SALDOCA500") as PrezziarioIdprezziario, - }); - if (!pack) { - return { success: false, message: "Pack non trovato" }; - } - return { - success: true, - pack, - }; - } - default: - return { success: false, message: "Seleziona tipologia" }; - } - }; - - return { - acconto: await getAcconto(), - saldo: await getSaldo(), - }; -}; -type TipoEnum = keyof typeof ConsulenzaPriceMapping; -export const setupSaldoConsulenzaServizio = async ({ - servizioId, - annuncioId, -}: { - servizioId: ServizioServizioId; - annuncioId: AnnunciId; -}) => { - try { - return await db.transaction().execute(async (tx) => { - // Cleanup previous payments and orders - await cleanupUnusedOrders({ db: tx }); - - const servizio = await tx - .selectFrom("servizio") - .selectAll() - .where("servizio_id", "=", servizioId) - .executeTakeFirstOrThrow( - () => new Error(`Servizio not found - servizioId: ${servizioId}`), - ); - - if ( - !servizio.isOkAcconto || - servizio.isInterrotto || - servizio.decorrenza == null || - !servizio.isOkSaldo || - servizio.isOkConsulenza - ) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Non serve saldo per questo servizio", - }); - } - - const annuncio = await tx - .selectFrom("servizio_annunci") - .selectAll() - .where("servizio_id", "=", servizioId) - .where("annunci_id", "=", annuncioId) - .executeTakeFirstOrThrow( - () => - new Error( - `Annuncio not found - servizioId: ${servizioId}, annunciId: ${annuncioId}`, - ), - ); - if ( - annuncio.user_confirmed_at == null || - annuncio.accettato_conferma_at == null || - !annuncio.contratto_tipo - ) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Annuncio not found or contratto not set", - }); - } - - if ( - !Object.keys(ConsulenzaPriceMapping).includes(annuncio.contratto_tipo) - ) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Consulenza type not found", - }); - } - - const pack = await tx - .selectFrom("prezziario") - .select(["idprezziario", "prezzo_cent", "nome_it", "nome_en", "sconto"]) - .where("isActive", "=", true) - .where( - "idprezziario", - "=", - ConsulenzaPriceMapping[annuncio.contratto_tipo as TipoEnum], - ) - .executeTakeFirst(); - - if (!pack) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Pack not found", - }); - } - await tx - .deleteFrom("payments") - .where("servizio_id", "=", servizioId) - .where("paymentstatus", "is", null) - .execute(); - - const order = await db - .insertInto("ordini") - .values({ - packid: pack.idprezziario, - servizio_id: servizio.servizio_id, - type: OrderTypeEnum.Consulenza, - userid: servizio.user_id, - }) - .returning("ordine_id") - .executeTakeFirstOrThrow(() => new Error("Failed to create ordine")); - - return order.ordine_id; - }); - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error setting up consulenza servizio: ${(e as Error).message}`, + message: `Error activating servizio without payment: ${(e as Error).message}`, }); } }; @@ -750,6 +412,7 @@ export type ServizioData = Servizio & { video: string; }[]; })[]; + ordini: (Ordini & Pick)[]; }; export const getAllServizioAnnunci = async ( @@ -852,6 +515,15 @@ export const getAllServizioAnnunci = async ( "servizio.servizio_id", ), ).as("annunci"), + jsonArrayFrom( + eb + .selectFrom("ordini") + .selectAll("ordini") + .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") + .select("prezziario.testo_condizioni") + .whereRef("ordini.servizio_id", "=", "servizio.servizio_id") + .orderBy("ordini.created_at", "desc"), + ).as("ordini"), ]) .orderBy("servizio.created_at", "desc") .execute(); @@ -879,6 +551,11 @@ export const getAllServizioAnnunci = async ( disponibile_da: parseNullableDateString(annuncio.disponibile_da), media_updated_at: parseNullableDateString(annuncio.media_updated_at), })), + ordini: item.ordini.map((ordine) => ({ + ...ordine, + created_at: new Date(ordine.created_at), + paid_at: ordine.paid_at ? new Date(ordine.paid_at) : null, + })), }; }); @@ -990,6 +667,15 @@ export const getServizioDataById = async ( "servizio.servizio_id", ), ).as("annunci"), + jsonArrayFrom( + eb + .selectFrom("ordini") + .selectAll("ordini") + .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") + .select("prezziario.testo_condizioni") + .whereRef("ordini.servizio_id", "=", "servizio.servizio_id") + .orderBy("ordini.created_at", "desc"), + ).as("ordini"), ]) .orderBy("servizio.created_at", "desc") .where("servizio.servizio_id", "=", servizioId) @@ -1020,6 +706,11 @@ export const getServizioDataById = async ( disponibile_da: parseNullableDateString(annuncio.disponibile_da), media_updated_at: parseNullableDateString(annuncio.media_updated_at), })), + ordini: data.ordini.map((ordine) => ({ + ...ordine, + created_at: new Date(ordine.created_at), + paid_at: ordine.paid_at ? new Date(ordine.paid_at) : null, + })), }; return formattedData; } catch (e) { @@ -1252,30 +943,6 @@ export const SbloccaContatti = async ({ } : null; - const EmailInteressato: NewMailProps = { - template: { - mailType: "emailContattiConScheda", - props: { - nominativo: data.annuncio.locatore || "", - telefono: data.annuncio.numero || "", - indirizzo: data.annuncio.indirizzo || "", - codice: data.annuncio.codice, - }, - }, - mail: { - subject: `Contatti Annuncio ${data.annuncio.codice} - InfoAlloggi`, - to: data.user.email, - attachments: [ - { - filename: `scheda annuncio ${data.annuncio.codice}.pdf`, - content: await genPdfSchedaAnnuncioBase64(data.annuncioId), - encoding: "base64", - contentType: "application/pdf", - }, - ], - }, - userId: data.user.id, - }; const numeroProprietario = data.annuncio.numero ? data.annuncio.numero.replace(/\s+/g, "").split(",")[0] || null : null; @@ -1286,8 +953,39 @@ export const SbloccaContatti = async ({ recipient: [numeroProprietario], } : null; - - await NewMail(EmailInteressato); + await addEventToQueue({ + data: { + data: { + template: { + mailType: "emailContattiConScheda", + props: { + nominativo: data.annuncio.locatore || "", + telefono: data.annuncio.numero || "", + indirizzo: data.annuncio.indirizzo || "", + codice: data.annuncio.codice, + }, + }, + mail: { + subject: `Contatti Annuncio ${data.annuncio.codice} - InfoAlloggi`, + to: data.user.email, + attachments: [ + { + filename: `scheda annuncio ${data.annuncio.codice}.pdf`, + generate: { + type: "scheda_annuncio", + annuncioId: data.annuncioId, + }, + encoding: "base64", + contentType: "application/pdf", + }, + ], + }, + userId: data.user.id, + }, + tipo: "email", + }, + lock_expires: new Date(Date.now() + 1000), + }); if (canSendNotification()) { if (EmailProprietario) { @@ -1576,6 +1274,22 @@ export const adminUpdateConfermaStatus = async ({ const servizio = await getServizioById(servizioId); const utente = await getUser({ db, userId: servizio.user_id }); + + const saldo = await SaldoSolver(servizio); + if (saldo.success) { + await createOrdine({ + servizio_id: servizioId, + type: OrderTypeEnum.Saldo, + amount_cent: saldo.pack.prezzo_cent, + packid: saldo.pack.idprezziario, + userid: servizio.user_id, + }); + } else { + console.error( + `Errore nel calcolo del saldo per il servizio ${servizioId}: ${saldo.message}, procedura invio conferma`, + ); + } + //ADMIN CONFERMA LAVORATA EMAIL if (status) { await NewMail({ @@ -1583,7 +1297,7 @@ export const adminUpdateConfermaStatus = async ({ mailType: "generic", props: { link: { - href: `${env.BASE_URL}/area-riservata/dashboard#${servizioId}-${annuncioId}`, + href: `${env.BASE_URL}/area-riservata/servizio/${servizioId}`, label: "Vai al servizio", }, noreply: true, @@ -1657,200 +1371,6 @@ export const userAccettaConferma = async ({ }); } }; -export const isOrdineAwaitingPayment = async (ordineId: OrdiniOrdineId) => { - try { - const ordine = await db - .selectFrom("ordini") - .select("ordine_id") - .where("ordine_id", "=", ordineId) - .where("isActive", "=", false) - .executeTakeFirst(); - - if (!ordine) { - console.error("Ordine not found:", ordineId); - return false; - } - - const payment = await db - .selectFrom("payments") - .select("id") - .where("ordine_id", "=", ordineId) - .where("paymentstatus", "=", PaymentStatusEnum.success) - .executeTakeFirst(); - - if (!payment) { - return true; - } - console.error("Payment already exists for ordine:", ordineId); - return false; - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error checking ordine payment status: ${(e as Error).message}`, - }); - } -}; -export const getServizioPagamentoData = async (ordineId: OrdiniOrdineId) => { - try { - await db - .deleteFrom("payments") - .where("ordine_id", "=", ordineId) - .where("paymentstatus", "is", null) - .execute(); - - const ordine = await db - .selectFrom("ordini") - .selectAll() - .where("ordine_id", "=", ordineId) - .executeTakeFirstOrThrow( - () => new Error(`Ordine not found - ordineId: ${ordineId}`), - ); - - const pack = await db - .selectFrom("prezziario") - .selectAll() - .where("idprezziario", "=", ordine.packid) - .executeTakeFirstOrThrow( - () => new Error(`Prezziario not found - packid: ${ordine.packid}`), - ); - - const payment = await db - .insertInto("payments") - .values({ - amount_cent: pack.prezzo_cent, - ordine_id: ordineId, - paymentname: pack.nome_it, - servizio_id: ordine.servizio_id, - userid: ordine.userid, - }) - .returningAll() - .executeTakeFirstOrThrow( - () => new Error(`Failed to create payment - ordineId: ${ordineId}`), - ); - - const servizio = await db - .selectFrom("servizio") - .selectAll() - .where("servizio.servizio_id", "=", payment.servizio_id) - .executeTakeFirstOrThrow( - () => - new Error(`Servizio not found - servizioId: ${payment.servizio_id}`), - ); - - if (!servizio) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Servizio not found", - }); - } - - return { - ordine, - payment, - servizio, - }; - } catch (e) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Servizio not found: ${(e as Error).message}`, - }); - } -}; - -export const getPagamentoDataForCheckout = async (paymentId: PaymentsId) => { - try { - return await db - .selectFrom("payments") - .selectAll("payments") - .innerJoin("users", "users.id", "payments.userid") - .select("users.email") - .innerJoin("ordini", "ordini.ordine_id", "payments.ordine_id") - .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") - .select(["prezziario.nome_it", "prezziario.desc_it"]) - .where("payments.id", "=", paymentId) - .executeTakeFirstOrThrow( - () => new Error(`Payment not found - paymentId: ${paymentId}`), - ); - } catch (e) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `Payment not found: ${(e as Error).message}`, - }); - } -}; - -type Pagamento = Payments & Pick; -export const getAllPagamenti = async (): Promise => { - try { - return await db - .selectFrom("payments") - .selectAll() - .innerJoin("users", "users.id", "payments.userid") - .select("users.username") - .orderBy("payments.created_at", "desc") - .execute(); - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error fetching user pagamenti: ${(e as Error).message}`, - }); - } -}; - -export const updatePagamento = async ({ - paymentId, - data, -}: { - paymentId: PaymentsId; - data: PaymentsUpdate; -}) => { - try { - return await db - .updateTable("payments") - .set(data) - .where("id", "=", paymentId) - .returningAll() - .executeTakeFirstOrThrow( - () => new Error(`Failed to update pagamento - paymentId: ${paymentId}`), - ); - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error updating pagamento: ${(e as Error).message}`, - }); - } -}; - -export const removePagamento = async (paymentId: PaymentsId) => { - try { - await db.deleteFrom("payments").where("id", "=", paymentId).execute(); - - return true; - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error removing pagamento: ${(e as Error).message}`, - }); - } -}; -export const getUserPagamenti = async ( - userId: UsersId, -): Promise => { - try { - return await db - .selectFrom("payments") - .selectAll() - .select([sql.lit("").as("username")]) - .where("payments.userid", "=", userId) - .orderBy("payments.created_at", "desc") - .execute(); - } catch (e) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error fetching user pagamenti: ${(e as Error).message}`, - }); - } -}; export const SendContattoAnnuncioEmail = async ({ codice, @@ -1993,64 +1513,53 @@ export const getOrdineRicevutaData = async ({ }) => { try { const data = await db - .selectFrom("payments") + .selectFrom("ordini") .select([ - "payments.id", - "payments.userid", - "payments.amount_cent", - "payments.paymentname", - "payments.created_at", - "payments.paymentmethod", + "ordini.ordine_id", + "ordini.userid", + "ordini.amount_cent", + "ordini.sconto", + "ordini.created_at", + "ordini.paymentmethod", ]) - .select((eb) => [ - jsonObjectFrom( - eb - .selectFrom("users") - .whereRef("users.id", "=", "payments.userid") - .select(["users.nome", "users.cognome"]) - - .limit(1), - ).as("user"), - jsonObjectFrom( - eb - .selectFrom("users_anagrafica") - .whereRef("users_anagrafica.userid", "=", "payments.userid") - .select([ - "users_anagrafica.via_residenza", - "users_anagrafica.civico_residenza", - "users_anagrafica.comune_residenza", - "users_anagrafica.provincia_residenza", - "users_anagrafica.cap_residenza", - "users_anagrafica.nazione_residenza", - ]) - .limit(1), - ).as("anagrafica"), - ]) - .where("payments.ordine_id", "=", ordineId) - .where("payments.paymentstatus", "=", PaymentStatusEnum.success) + .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") + .select("prezziario.nome_it") + .where("ordini.ordine_id", "=", ordineId) + .where("ordini.paymentstatus", "=", PaymentStatusEnum.success) .executeTakeFirstOrThrow( () => new Error(`Ordine not found - ordineId: ${ordineId}`), ); - if (!data || !data.user || !data.anagrafica) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Ordine not found", - }); - } + const anagrafica = await db + .selectFrom("users") + .select("users.username") + .innerJoin("users_anagrafica", "users_anagrafica.userid", "users.id") + .select([ + "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("users.id", "=", data.userid) + .executeTakeFirstOrThrow( + () => new Error(`Anagrafica not found - userid: ${data.userid}`), + ); + return { - clienteIndirizzo: `${data.anagrafica.via_residenza} ${data.anagrafica.civico_residenza}\n${data.anagrafica.comune_residenza} (${data.anagrafica.provincia_residenza}) ${data.anagrafica.cap_residenza}\n${data.anagrafica.nazione_residenza}`, - clienteNome: `${data.user.nome} ${data.user.cognome}`, + clienteIndirizzo: `${anagrafica.via_residenza} ${anagrafica.civico_residenza}\n${anagrafica.comune_residenza} (${anagrafica.provincia_residenza}) ${anagrafica.cap_residenza}\n${anagrafica.nazione_residenza}`, + clienteNome: anagrafica.username, date: data.created_at, items: [ { - description: data.paymentname, - discount: 0, // Assuming no discount for simplicity + description: data.nome_it, + discount: data.sconto / 100, // Convert percentage to decimal price: data.amount_cent / 100, // Convert cents to euros }, ], - paymentId: data.id, + ordineId: data.ordine_id, paymentMethod: data.paymentmethod, } as PurchaseData; } catch (e) { diff --git a/apps/infoalloggi/src/server/controllers/stripe.controller.ts b/apps/infoalloggi/src/server/controllers/stripe.controller.ts index 6a37e4d..d8f2aa8 100644 --- a/apps/infoalloggi/src/server/controllers/stripe.controller.ts +++ b/apps/infoalloggi/src/server/controllers/stripe.controller.ts @@ -2,8 +2,8 @@ import { TRPCError } from "@trpc/server"; import { env } from "~/env"; import stripe from "~/lib/stripe"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; +import type { Ordini, OrdiniOrdineId } from "~/schemas/public/Ordini"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; -import type { Payments, PaymentsId } from "~/schemas/public/Payments"; import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import { db } from "~/server/db"; @@ -31,71 +31,22 @@ export const stripeHealthCheck = async () => { } }; -export const createIntentHandler = async ({ - paymentId, -}: { - paymentId: PaymentsId; -}) => { - try { - const payment = await db - .selectFrom("payments") - .selectAll("payments") - .innerJoin("users", "users.id", "payments.userid") - .select("users.email") - .innerJoin("ordini", "ordini.ordine_id", "payments.ordine_id") - .innerJoin("prezziario", "prezziario.idprezziario", "ordini.packid") - .select(["prezziario.nome_it", "prezziario.desc_it"]) - .where("payments.id", "=", paymentId) - .executeTakeFirstOrThrow( - () => new Error(`Payment not found - paymentId: ${paymentId}`), - ); - - const paymentIntent = await stripe.paymentIntents.create({ - amount: payment.amount_cent, - //payment_method_configuration: "pmc_1P4NfjILe4KoQRqXYChFCTFj", - automatic_payment_methods: { - enabled: true, - }, - currency: "eur", - description: payment.nome_it, - metadata: { - paymentId, - servizioId: payment.servizio_id, - userId: payment.userid, - }, - receipt_email: payment.email, - }); - - return { - clientSecret: paymentIntent.client_secret, - descrizione: payment.desc_it, - prezzo: payment.amount_cent, - titolo: payment.nome_it, - }; - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Error creating payment intent: ${(error as Error).message}`, - }); - } -}; - export const whIntentCreatedHandler = async ({ pIntentId, - paymentId, + ordineId, }: { - pIntentId: Payments["intent_id"]; - paymentId: PaymentsId; + pIntentId: Ordini["intent_id"]; + ordineId: OrdiniOrdineId; }) => { try { return await db - .updateTable("payments") + .updateTable("ordini") .set({ intent_id: pIntentId, paymentstatus: PaymentStatusEnum.processing, }) - .where("id", "=", paymentId) - .returning("id") + .where("ordine_id", "=", ordineId) + .returning("ordine_id") .execute(); } catch (error) { throw new TRPCError({ @@ -107,56 +58,41 @@ export const whIntentCreatedHandler = async ({ export const whIntentSucceededHandler = async ({ pIntentId, - paymentId, + ordineId, pm_id, }: { - pIntentId: Payments["intent_id"]; - paymentId: PaymentsId; + pIntentId: Ordini["intent_id"]; + ordineId: OrdiniOrdineId; pm_id: string; }) => { try { const paymentMethod = await stripe.paymentMethods.retrieve(pm_id); await db.transaction().execute(async (trx) => { - const payment = await trx - .updateTable("payments") + const ordine = await trx + .updateTable("ordini") .set({ paid_at: new Date(), paymentmethod: paymentMethod.type, paymentstatus: PaymentStatusEnum.success, + isActive: true, }) .where("intent_id", "=", pIntentId) - .where("payments.id", "=", paymentId) - .returning(["servizio_id", "ordine_id"]) + .where("ordine_id", "=", ordineId) + .returning(["servizio_id", "ordine_id", "userid", "type", "created_at"]) .executeTakeFirstOrThrow(() => { throw new Error( - `Payment not found - intent_id: ${pIntentId}, paymentId: ${paymentId}`, + `Payment not found - intent_id: ${pIntentId}, ordineId: ${ordineId}`, ); }); - const ordine = await trx - .selectFrom("ordini") - .selectAll("ordini") - .where("ordine_id", "=", payment.ordine_id) - .executeTakeFirstOrThrow(() => { - throw new Error(`Ordine not found - ordine_id: ${payment.ordine_id}`); - }); - - await trx - .updateTable("ordini") - .where("ordine_id", "=", payment.ordine_id) - .set({ - isActive: true, - }) - .execute(); - const servizio = await trx .selectFrom("servizio") - .select("servizio.tipologia") - .where("servizio_id", "=", payment.servizio_id) + .select(["servizio.tipologia", "servizio.servizio_id"]) + .where("servizio_id", "=", ordine.servizio_id) .executeTakeFirstOrThrow(() => { throw new Error( - `Servizio not found - servizio_id: ${payment.servizio_id}`, + `Servizio not found - servizio_id: ${ordine.servizio_id}`, ); }); @@ -170,7 +106,7 @@ export const whIntentSucceededHandler = async ({ case OrderTypeEnum.Acconto: await trx .updateTable("servizio") - .where("servizio_id", "=", payment.servizio_id) + .where("servizio_id", "=", servizio.servizio_id) .set({ decorrenza: new Date(), isOkAcconto: true, @@ -223,7 +159,7 @@ export const whIntentSucceededHandler = async ({ case OrderTypeEnum.Saldo: await trx .updateTable("servizio") - .where("servizio_id", "=", payment.servizio_id) + .where("servizio_id", "=", servizio.servizio_id) .set({ isOkConsulenza: servizio.tipologia === TipologiaPosizioneEnum.Transitorio, @@ -263,7 +199,7 @@ export const whIntentSucceededHandler = async ({ case OrderTypeEnum.Consulenza: await trx .updateTable("servizio") - .where("servizio_id", "=", payment.servizio_id) + .where("servizio_id", "=", servizio.servizio_id) .set({ isOkConsulenza: true, }) @@ -336,7 +272,7 @@ export const whIntentSucceededHandler = async ({ // Enhanced error logging console.error("whIntentSucceededHandler error:", { pIntentId, - paymentId, + ordineId, pm_id, error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, @@ -352,10 +288,10 @@ export const whIntentSucceededHandler = async ({ export const whIntentFailedHandler = async ({ pIntentId, }: { - pIntentId: Payments["intent_id"]; + pIntentId: Ordini["intent_id"]; }) => { const userId = await db - .updateTable("payments") + .updateTable("ordini") .set({ paymentstatus: PaymentStatusEnum.failed, }) diff --git a/apps/infoalloggi/src/server/services/prezziario.service.ts b/apps/infoalloggi/src/server/services/prezziario.service.ts index 57d3096..7d0f7af 100644 --- a/apps/infoalloggi/src/server/services/prezziario.service.ts +++ b/apps/infoalloggi/src/server/services/prezziario.service.ts @@ -25,11 +25,7 @@ export const getPrezziario = async () => { } }; -export const getPrezziarioByIdHandler = async ({ - id, -}: { - id: PrezziarioIdprezziario; -}) => { +export const getPrezziarioByIdHandler = async (id: PrezziarioIdprezziario) => { try { return await db .selectFrom("prezziario") diff --git a/apps/infoalloggi/src/server/services/puppeteer.service.ts b/apps/infoalloggi/src/server/services/puppeteer.service.ts index 5977fe8..27a2f09 100644 --- a/apps/infoalloggi/src/server/services/puppeteer.service.ts +++ b/apps/infoalloggi/src/server/services/puppeteer.service.ts @@ -83,7 +83,7 @@ export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => { export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => { const keybd = getKeydbClient(); - const url = `/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`; + const url = `/area-riservata/scheda-annuncio-stampa/${annuncioId}?raw=true`; const options: PDFOptions = { printBackground: true }; if (keybd) { const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`; @@ -141,11 +141,15 @@ const genPdfCondizioni = async (condizioniId: TestiEStringheStingaId) => { return await puppeteerGenPdf({ url }); }; +export const genPdfRicevutaCortesia = async (ordineId: OrdiniOrdineId) => { + const url = `/servizio/ricevuta-cortesia/${ordineId}?raw=true`; + return await puppeteerGenPdf({ url }); +}; + export const genPdfRicevutaCortesiaBase64 = async ( ordineId: OrdiniOrdineId, ) => { - const url = `/servizio/ricevuta-cortesia/${ordineId}?raw=true`; - const pdf = await puppeteerGenPdf({ url }); + const pdf = await genPdfRicevutaCortesia(ordineId); const pdfBase64 = Buffer.from(pdf).toString("base64"); const base64Content = pdfBase64.includes("base64,") ? pdfBase64.split("base64,")[1] diff --git a/apps/infoalloggi/src/server/utils/zod_types.ts b/apps/infoalloggi/src/server/utils/zod_types.ts index 25ff28a..760d5b7 100644 --- a/apps/infoalloggi/src/server/utils/zod_types.ts +++ b/apps/infoalloggi/src/server/utils/zod_types.ts @@ -7,7 +7,6 @@ import type { EtichetteIdEtichetta } from "~/schemas/public/Etichette"; import type { FlagsId } from "~/schemas/public/Flags"; import type { MessagesMessageid } from "~/schemas/public/Messages"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; -import type { PaymentsId } from "~/schemas/public/Payments"; import type { PrezziarioIdprezziario } from "~/schemas/public/Prezziario"; import type { ServizioServizioId } from "~/schemas/public/Servizio"; import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; @@ -77,11 +76,6 @@ export const zServizioId = z.custom( "falied to validate ServizioId", ); -export const zPagamentoId = z.custom( - (val) => typeof val === "string", - "falied to validate PagamentoId", -); - // export const zComuneId = z.custom( // (val) => typeof val === "number", // "falied to validate ComuneId",