From f7f0e5e48f4cd3325fd2724b8b343cd00c24dff3 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Wed, 17 Sep 2025 11:51:39 +0300 Subject: [PATCH 01/10] add openai package --- app/home/(user)/(dashboard)/page.tsx | 4 + .../(user)/_components/recommendations.tsx | 26 ++ package.json | 1 + pnpm-lock.yaml | 314 ++++++++++++++---- 4 files changed, 279 insertions(+), 66 deletions(-) create mode 100644 app/home/(user)/_components/recommendations.tsx diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index ca9fc22..dbda9e9 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -12,6 +12,7 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; +import Recommendations from '../_components/recommendations'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; export const generateMetadata = async () => { @@ -24,6 +25,7 @@ export const generateMetadata = async () => { }; async function UserHomePage() { + console.log('process.env', process.env.OPENAI_API_KEY); const client = getSupabaseServerClient(); const { account } = await loadCurrentUserAccount(); @@ -48,6 +50,8 @@ async function UserHomePage() { /> + + ); diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx new file mode 100644 index 0000000..80bc725 --- /dev/null +++ b/app/home/(user)/_components/recommendations.tsx @@ -0,0 +1,26 @@ +'use client'; + +import React, { useEffect } from 'react'; + +import OpenAI from 'openai'; + +export default function Recommendations() { + // const client = new OpenAI(); + + const getRecommendations = async () => { + // const response = await client.responses.create({ + // model: 'gpt-5', + // input: 'Write a short bedtime story about a unicorn.', + // }); + // console.log(response.output_text); + }; + + // useEffect(() => { + // console.log('process.env', process.env); + // if (process.env.OPENAI_API_KEY) { + // getRecommendations(); + // } + // }, []); + + return
Recommendations
; +} diff --git a/package.json b/package.json index 33ec639..05e16ef 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "next": "15.3.2", "next-sitemap": "^4.2.3", "next-themes": "0.4.6", + "openai": "^5.20.3", "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.58.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 77d0440..9391c41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,10 +85,10 @@ importers: version: 2.10.1(react@19.1.0) '@medusajs/js-sdk': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui': specifier: latest - version: 4.0.21(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2) + version: 4.0.22(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2) '@nosecone/next': specifier: 1.0.0-beta.7 version: 1.0.0-beta.7(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) @@ -146,6 +146,9 @@ importers: next-themes: specifier: 0.4.6 version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + openai: + specifier: ^5.20.3 + version: 5.20.3(ws@8.18.2)(zod@4.1.5) react: specifier: 19.1.0 version: 19.1.0 @@ -185,10 +188,10 @@ importers: version: link:tooling/typescript '@medusajs/types': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui-preset': specifier: latest - version: 2.10.1(tailwindcss@4.1.7) + version: 2.10.2(tailwindcss@4.1.7) '@next/bundle-analyzer': specifier: 15.3.2 version: 15.3.2 @@ -478,10 +481,10 @@ importers: dependencies: '@keystatic/core': specifier: 0.5.47 - version: 0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@keystatic/next': specifier: ^5.0.4 - version: 5.0.4(@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 5.0.4(@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@markdoc/markdoc': specifier: ^0.5.1 version: 0.5.4(@types/react@19.1.4)(react@19.1.0) @@ -853,10 +856,10 @@ importers: version: 2.2.7(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@medusajs/js-sdk': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui': specifier: latest - version: 4.0.21(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2) + version: 4.0.22(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2) '@radix-ui/react-accordion': specifier: ^1.2.1 version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) @@ -902,10 +905,10 @@ importers: version: 7.28.3 '@medusajs/types': specifier: latest - version: 2.10.1(awilix@8.0.1) + version: 2.10.2(awilix@8.0.1) '@medusajs/ui-preset': specifier: latest - version: 2.10.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) + version: 2.10.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) '@types/lodash': specifier: ^4.14.195 version: 4.17.20 @@ -1272,7 +1275,7 @@ importers: dependencies: '@sentry/nextjs': specifier: ^9.19.0 - version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3) + version: 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3) import-in-the-middle: specifier: 1.13.2 version: 1.13.2 @@ -1908,6 +1911,28 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@7.0.2': + resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} + peerDependencies: + '@dnd-kit/core': ^6.0.7 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@edge-csrf/nextjs@2.5.3-cloudflare-rc1': resolution: {integrity: sha512-sH8HKl2s/zFkIgXcVzODljHsBhKW7LN2gXeUNEQTSP31Chy40ryjR7iTf0MA/DtPhOpXUkd6IBUMMF820sK+rA==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -2564,12 +2589,17 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - '@medusajs/js-sdk@2.10.1': - resolution: {integrity: sha512-KOXSU56cFu58w85PxgnqJsxaYNmGVCh4a70pROc4709u/Z6eGx5RdbdOdksk5vwEDWqr4OlngFB8aYVzLpXX4w==} + '@medusajs/icons@2.10.2': + resolution: {integrity: sha512-ZZFEWTGdQGvsRPs5ANV9GlFUFyba852cqWtRu7aO3QlPhk+ECbWqJYkhL3h/HZ1twN5nEG51QAncUkjm5TVBRw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + '@medusajs/js-sdk@2.10.2': + resolution: {integrity: sha512-P6A9E5LQkG/g9YiHlvUbH/k777XTfnY47neI1EmA56B66XBVIwzTqUglluREVZ+akjlI0eOxpwk2PDFPVaK1ZQ==} engines: {node: '>=20'} - '@medusajs/types@2.10.1': - resolution: {integrity: sha512-V4pRtwdCZQRnaXTgtTOD2EFOWaIz4Z59EMueGxHyepV/lST16hMNxzve3pf0KvqkWHSg8yGLMuXGwQDbfSYsIw==} + '@medusajs/types@2.10.2': + resolution: {integrity: sha512-yonCLLnO2FFDB+HCo6nAGz+I7u+7ux+HDJCT8KUPlZJ5/6AYZcgdsuNWMwaa441yjEXDDdZmGZtbSM37lBFoxA==} engines: {node: '>=20'} peerDependencies: awilix: ^8.0.1 @@ -2581,13 +2611,13 @@ packages: vite: optional: true - '@medusajs/ui-preset@2.10.1': - resolution: {integrity: sha512-vm6Zz5qLf63Y18yQE9M+v+uVT2rSExGU8EeaN9wOh4Wbc3nMJ0aJBLag4utIlYBY5MIfMU5EHPNqwfTGvNi6SA==} + '@medusajs/ui-preset@2.10.2': + resolution: {integrity: sha512-t3VmloSbeZOjfyYL+iQTU94wutX0/B+uv9wz8L1MK6CErHK4T4ox246QDsNAR2QrNE0EUEEy+SX0qiW+xHFsMQ==} peerDependencies: tailwindcss: '>=3.0.0' - '@medusajs/ui@4.0.21': - resolution: {integrity: sha512-C2XnIoksSDOUZKaTeRn8jMzTo0kUVZLyggIL5tSLL/oqZOkOYmnnio4wQoZ1FXKf0h8OE6awWLpVncFsQojnXw==} + '@medusajs/ui@4.0.22': + resolution: {integrity: sha512-p3Nl6OTyxe583VVmvGAYbizFFPzr9GsZzbO54Ku9jZqf+MYNqdDjKFP28K5QHkpLpbvkGU05BQZ5d5H49vGxiQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc @@ -3451,6 +3481,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dialog@1.1.5': resolution: {integrity: sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==} peerDependencies: @@ -3495,6 +3538,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.4': resolution: {integrity: sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==} peerDependencies: @@ -8754,6 +8810,18 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + openai@5.20.3: + resolution: {integrity: sha512-8V0KgAcPFppDIP8uMBOkhRrhDBuxNQYQxb9IovP4NN4VyaYGISAzYexyYYuAwVul2HB75Wpib0xDboYJqRMNow==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -10760,6 +10828,56 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@dnd-kit/accessibility@3.1.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + + '@dnd-kit/accessibility@3.1.1(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/utilities': 3.2.2(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + tslib: 2.8.1 + + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + react: 19.1.0 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.1.0)': + dependencies: + react: 19.1.0 + tslib: 2.8.1 + '@edge-csrf/nextjs@2.5.3-cloudflare-rc1(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))': dependencies: next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -11457,7 +11575,7 @@ snapshots: '@juggle/resize-observer@3.4.0': {} - '@keystar/ui@0.7.19(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@keystar/ui@0.7.19(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@emotion/css': 11.13.5 @@ -11550,18 +11668,18 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) optionalDependencies: - next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color - '@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@braintree/sanitize-url': 6.0.4 '@emotion/weak-memoize': 0.3.1 '@floating-ui/react': 0.24.8(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@internationalized/string': 3.2.7 - '@keystar/ui': 0.7.19(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@keystar/ui': 0.7.19(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@markdoc/markdoc': 0.4.0(@types/react@19.1.4)(react@19.1.0) '@react-aria/focus': 3.20.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@react-aria/i18n': 3.12.10(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -11632,13 +11750,13 @@ snapshots: - next - supports-color - '@keystatic/next@5.0.4(@keystatic/core@0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@keystatic/next@5.0.4(@keystatic/core@0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@keystatic/core': 0.5.47(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@keystatic/core': 0.5.47(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@types/react': 19.1.4 chokidar: 3.6.0 - next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) server-only: 0.0.1 @@ -11679,17 +11797,21 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@medusajs/icons@2.10.1(react@19.0.0-rc-66855b96-20241106)': - dependencies: - react: 19.0.0-rc-66855b96-20241106 - '@medusajs/icons@2.10.1(react@19.1.0)': dependencies: react: 19.1.0 - '@medusajs/js-sdk@2.10.1(awilix@8.0.1)': + '@medusajs/icons@2.10.2(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@medusajs/types': 2.10.1(awilix@8.0.1) + react: 19.0.0-rc-66855b96-20241106 + + '@medusajs/icons@2.10.2(react@19.1.0)': + dependencies: + react: 19.1.0 + + '@medusajs/js-sdk@2.10.2(awilix@8.0.1)': + dependencies: + '@medusajs/types': 2.10.2(awilix@8.0.1) fetch-event-stream: 0.1.5 qs: 6.14.0 transitivePeerDependencies: @@ -11697,26 +11819,31 @@ snapshots: - ioredis - vite - '@medusajs/types@2.10.1(awilix@8.0.1)': + '@medusajs/types@2.10.2(awilix@8.0.1)': dependencies: awilix: 8.0.1 bignumber.js: 9.3.0 - '@medusajs/ui-preset@2.10.1(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2)))': + '@medusajs/ui-preset@2.10.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2)))': dependencies: '@tailwindcss/forms': 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2)) tailwindcss-animate: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.9.2))) - '@medusajs/ui-preset@2.10.1(tailwindcss@4.1.7)': + '@medusajs/ui-preset@2.10.2(tailwindcss@4.1.7)': dependencies: '@tailwindcss/forms': 0.5.10(tailwindcss@4.1.7) tailwindcss: 4.1.7 tailwindcss-animate: 1.0.7(tailwindcss@4.1.7) - '@medusajs/ui@4.0.21(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2)': + '@medusajs/ui@4.0.22(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.9.2)': dependencies: - '@medusajs/icons': 2.10.1(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/core': 6.3.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.0.0-rc-66855b96-20241106) + '@dnd-kit/utilities': 3.2.2(react@19.0.0-rc-66855b96-20241106) + '@medusajs/icons': 2.10.2(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@tanstack/react-table': 8.20.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) clsx: 1.2.1 copy-to-clipboard: 3.3.3 @@ -11736,9 +11863,14 @@ snapshots: - '@types/react-dom' - typescript - '@medusajs/ui@4.0.21(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)': + '@medusajs/ui@4.0.22(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.9.2)': dependencies: - '@medusajs/icons': 2.10.1(react@19.1.0) + '@dnd-kit/core': 6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) + '@dnd-kit/utilities': 3.2.2(react@19.1.0) + '@medusajs/icons': 2.10.2(react@19.1.0) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-table': 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) clsx: 1.2.1 copy-to-clipboard: 3.3.3 @@ -12826,6 +12958,50 @@ snapshots: '@types/react': 19.1.4 '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dialog@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + aria-hidden: 1.2.6 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-dialog@1.1.4(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.1.4)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.4)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dialog@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -12907,6 +13083,32 @@ snapshots: '@types/react': 19.1.4 '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.24)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.4)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@19.1.4)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.4 + '@types/react-dom': 19.1.5(@types/react@19.1.4) + '@radix-ui/react-dismissable-layer@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -17232,7 +17434,7 @@ snapshots: '@sentry/core@9.46.0': {} - '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)': + '@sentry/nextjs@9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(next@15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)(webpack@5.101.3)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/semantic-conventions': 1.34.0 @@ -17245,7 +17447,7 @@ snapshots: '@sentry/vercel-edge': 9.46.0(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)) '@sentry/webpack-plugin': 3.5.0(webpack@5.101.3) chalk: 3.0.0 - next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next: 15.3.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) resolve: 1.22.8 rollup: 4.35.0 stacktrace-parser: 0.1.11 @@ -21269,31 +21471,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): - dependencies: - '@next/env': 15.5.2 - '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001723 - postcss: 8.4.31 - react: 19.1.0 - react-dom: 19.1.0(react@19.1.0) - styled-jsx: 5.1.6(@babel/core@7.28.3)(babel-plugin-macros@3.1.0)(react@19.1.0) - optionalDependencies: - '@next/swc-darwin-arm64': 15.5.2 - '@next/swc-darwin-x64': 15.5.2 - '@next/swc-linux-arm64-gnu': 15.5.2 - '@next/swc-linux-arm64-musl': 15.5.2 - '@next/swc-linux-x64-gnu': 15.5.2 - '@next/swc-linux-x64-musl': 15.5.2 - '@next/swc-win32-arm64-msvc': 15.5.2 - '@next/swc-win32-x64-msvc': 15.5.2 - '@opentelemetry/api': 1.9.0 - babel-plugin-react-compiler: 19.1.0-rc.2 - sharp: 0.34.3 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -21387,6 +21564,11 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@5.20.3(ws@8.18.2)(zod@4.1.5): + optionalDependencies: + ws: 8.18.2 + zod: 4.1.5 + opener@1.5.2: {} optionator@0.9.4: From b96ef47b2dcbd47e0317c013f187e74f1a8a8758 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Wed, 17 Sep 2025 11:52:35 +0300 Subject: [PATCH 02/10] Merge branch 'develop' into MED-157 From 1b634f9736d7a06599849415336272dba809b29d Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Wed, 17 Sep 2025 17:35:04 +0300 Subject: [PATCH 03/10] add analysis recommendation --- .../analysis-results/test/test-responses.ts | 1552 ++++++++--------- app/home/(user)/(dashboard)/page.tsx | 21 +- .../(user)/_components/recommendations.tsx | 38 +- .../_lib/server/load-recommendations.test.ts | 133 ++ .../_lib/server/load-recommendations.ts | 153 ++ app/home/[account]/page.tsx | 2 +- 6 files changed, 1100 insertions(+), 799 deletions(-) create mode 100644 app/home/(user)/_lib/server/load-recommendations.test.ts create mode 100644 app/home/(user)/_lib/server/load-recommendations.ts diff --git a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts index e4da467..1d4768d 100644 --- a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts +++ b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts @@ -1,840 +1,840 @@ -import { AnalysisResultDetailsMapped } from "@/packages/features/accounts/src/types/analysis-results"; +import { AnalysisResultDetailsMapped } from '@/packages/features/accounts/src/types/analysis-results'; -type AnalysisTestResponse = Omit; +export type AnalysisTestResponse = Omit< + AnalysisResultDetailsMapped, + 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements' +>; const empty1: AnalysisTestResponse = { - "id": 1, - "orderedAnalysisElements": [], + id: 1, + orderedAnalysisElements: [], }; const big1: AnalysisTestResponse = { - "id": 2, - "orderedAnalysisElements": [ + id: 2, + orderedAnalysisElements: [ { - "analysisIdOriginal": "1744-2", - "isWaitingForResults": false, - "analysisName": "ALAT", - "results": { - "nestedElements": [], - "unit": "U/l", - "normLower": null, - "normUpper": 45, - "normStatus": 2, - "responseTime": "2024-02-29T10:42:25+00:00", - "responseValue": 84, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "1744-2" - } - }, - { - "analysisIdOriginal": "1920-8", - "isWaitingForResults": false, - "analysisName": "ASAT", - "results": { - "nestedElements": [], - "unit": "U/l", - "normLower": 15, - "normUpper": 45, - "normStatus": 0, - "responseTime": "2024-02-29T10:20:55+00:00", - "responseValue": 45, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "1920-8" - } - }, - { - "analysisIdOriginal": "1988-5", - "isWaitingForResults": false, - "analysisName": "CRP", - "results": { - "nestedElements": [], - "unit": "mg/l", - "normLower": null, - "normUpper": 5, - "normStatus": 0, - "responseTime": "2024-02-29T10:18:49+00:00", - "responseValue": 0.79, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "1988-5" - } - }, - { - "analysisIdOriginal": "57747-8", - "isWaitingForResults": false, - "analysisName": "Erütrotsüüdid", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": 5, - "normStatus": 0, - "responseTime": "2024-02-29T10:13:01+00:00", - "responseValue": null, - "responseValueIsNegative": true, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "57747-8" - } - }, - { - "analysisIdOriginal": "2276-4", - "isWaitingForResults": false, - "analysisName": "Ferritiin", - "results": { - "nestedElements": [], - "unit": "µg/l", - "normLower": 28, - "normUpper": 370, - "normStatus": 0, - "responseTime": "2024-02-29T10:46:54+00:00", - "responseValue": 204.1, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "2276-4" - } - }, - { - "analysisIdOriginal": "14771-0", - "isWaitingForResults": false, - "analysisName": "Glükoos", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": 4.1, - "normUpper": 6, - "normStatus": 0, - "responseTime": "2024-02-29T10:06:24+00:00", - "responseValue": 5.4, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "14771-0" - } - }, - { - "analysisIdOriginal": "59156-0", - "isWaitingForResults": false, - "analysisName": "Glükoos", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": 2, - "normStatus": 0, - "responseTime": "2024-02-29T10:13:01+00:00", - "responseValue": null, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": false, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "59156-0" - } - }, - { - "analysisIdOriginal": "13955-0", - "isWaitingForResults": false, - "analysisName": "HCV Ab", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": null, - "normStatus": 0, - "responseTime": "2024-02-29T13:44:48+00:00", - "responseValue": null, - "responseValueIsNegative": true, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "13955-0" - } - }, - { - "analysisIdOriginal": "14646-4", - "isWaitingForResults": false, - "analysisName": "HDL kolesterool", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": 1, - "normUpper": null, - "normStatus": 1, - "responseTime": "2024-02-29T10:20:55+00:00", - "responseValue": 0.8, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "14646-4" - } - }, - { - "analysisIdOriginal": "2000-8", - "isWaitingForResults": false, - "analysisName": "Kaltsium", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": 2.1, - "normUpper": 2.55, - "normStatus": 0, - "responseTime": "2024-02-29T10:12:10+00:00", - "responseValue": 2.49, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "2000-8" - } - }, - { - "analysisIdOriginal": "59158-6", - "isWaitingForResults": false, - "analysisName": "Ketokehad", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": 0.5, - "normStatus": 0, - "responseTime": "2024-02-29T10:13:01+00:00", - "responseValue": null, - "responseValueIsNegative": true, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "59158-6" - } - }, - { - "analysisIdOriginal": "14647-2", - "isWaitingForResults": false, - "analysisName": "Kolesterool", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": null, - "normUpper": 5, - "normStatus": 1, - "responseTime": "2024-02-29T10:20:34+00:00", - "responseValue": 5.7, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "14647-2" - } - }, - { - "analysisIdOriginal": "14682-9", - "isWaitingForResults": false, - "analysisName": "Kreatiniin", - "results": { - "nestedElements": [], - "unit": "µmol/l", - "normLower": 64, - "normUpper": 111, - "normStatus": 0, - "responseTime": "2024-02-29T10:19:00+00:00", - "responseValue": 89, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "14682-9" - } - }, - { - "analysisIdOriginal": "22748-8", - "isWaitingForResults": false, - "analysisName": "LDL kolesterool", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": null, - "normUpper": 3, - "normStatus": 1, - "responseTime": "2024-02-29T10:21:15+00:00", - "responseValue": 4.3, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "22748-8" - } - }, - { - "analysisIdOriginal": "58805-3", - "isWaitingForResults": false, - "analysisName": "Leukotsüüdid", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": 10, - "normStatus": 0, - "responseTime": "2024-02-29T10:13:01+00:00", - "responseValue": null, - "responseValueIsNegative": true, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "58805-3" - } - }, - { - "analysisIdOriginal": "2601-3", - "isWaitingForResults": false, - "analysisName": "Magneesium", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": 0.66, - "normUpper": 1.07, - "normStatus": 0, - "responseTime": "2024-02-29T10:17:26+00:00", - "responseValue": 0.82, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "2601-3" - } - }, - { - "analysisIdOriginal": "70204-3", - "isWaitingForResults": false, - "analysisName": "Mitte-HDL kolesterool", - "results": { - "nestedElements": [], - "labComment": "Mitte-paastu veri <3,9 mmol/L", - "unit": "mmol/l", - "normLower": null, - "normUpper": 3.8, - "normStatus": 1, - "responseTime": "2024-02-29T10:20:55+00:00", - "responseValue": 4.9, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "70204-3" - } - }, - { - "analysisIdOriginal": "14798-3", - "isWaitingForResults": false, - "analysisName": "Raud", - "results": { - "nestedElements": [], - "unit": "µmol/l", - "normLower": 11.6, - "normUpper": 31.3, - "normStatus": 0, - "responseTime": "2024-02-29T10:21:16+00:00", - "responseValue": 16.5, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "14798-3" - } - }, - { - "analysisIdOriginal": "14927-8", - "isWaitingForResults": false, - "analysisName": "Triglütseriidid", - "results": { - "nestedElements": [], - "labComment": "Mitte-paastu veri <2,0 mmol/L", - "unit": "mmol/l", - "normLower": null, - "normUpper": 1.7, - "normStatus": 1, - "responseTime": "2024-02-29T10:21:16+00:00", - "responseValue": 1.89, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "14927-8" - } - }, - { - "analysisIdOriginal": "3016-3", - "isWaitingForResults": false, - "analysisName": "TSH", - "results": { - "nestedElements": [], - "unit": "mIU/l", - "normLower": 0.4, - "normUpper": 4, - "normStatus": 0, - "responseTime": "2024-02-29T10:49:02+00:00", - "responseValue": 1.27, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "3016-3" - } - }, - { - "analysisIdOriginal": "22664-7", - "isWaitingForResults": false, - "analysisName": "Uurea", - "results": { - "nestedElements": [], - "unit": "mmol/l", - "normLower": 3.2, - "normUpper": 7.4, - "normStatus": 0, - "responseTime": "2024-02-29T10:19:11+00:00", - "responseValue": 6.4, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "22664-7" - } - }, - { - "analysisIdOriginal": "50561-0", - "isWaitingForResults": false, - "analysisName": "Valk", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": 0.25, - "normStatus": 0, - "responseTime": "2024-02-29T10:13:01+00:00", - "responseValue": null, - "responseValueIsNegative": true, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "50561-0" - } - }, - { - "analysisIdOriginal": "60493-4", - "isWaitingForResults": false, - "analysisName": "Vitamiin D (25-OH)", - "results": { - "nestedElements": [], - "labComment": "Väärtus >75 nmol/l on D-vitamiini tervislik tase", - "unit": "nmol/l", - "normLower": 75, - "normUpper": null, - "normStatus": 0, - "responseTime": "2024-02-29T10:49:22+00:00", - "responseValue": 105.5, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "60493-4" - } - }, - { - "analysisIdOriginal": "60025-4", - "isWaitingForResults": false, - "analysisName": "Urobilinogeen", - "results": { - "nestedElements": [], - "unit": null, - "normLower": null, - "normUpper": 17, - "normStatus": 0, - "responseTime": "2024-02-29T10:13:01+00:00", - "responseValue": null, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": true, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "60025-4" + analysisIdOriginal: '1744-2', + isWaitingForResults: false, + analysisName: 'ALAT', + results: { + nestedElements: [], + unit: 'U/l', + normLower: null, + normUpper: 45, + normStatus: 2, + responseTime: '2024-02-29T10:42:25+00:00', + responseValue: 84, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '1744-2', }, - } + }, + { + analysisIdOriginal: '1920-8', + isWaitingForResults: false, + analysisName: 'ASAT', + results: { + nestedElements: [], + unit: 'U/l', + normLower: 15, + normUpper: 45, + normStatus: 0, + responseTime: '2024-02-29T10:20:55+00:00', + responseValue: 45, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '1920-8', + }, + }, + { + analysisIdOriginal: '1988-5', + isWaitingForResults: false, + analysisName: 'CRP', + results: { + nestedElements: [], + unit: 'mg/l', + normLower: null, + normUpper: 5, + normStatus: 0, + responseTime: '2024-02-29T10:18:49+00:00', + responseValue: 0.79, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '1988-5', + }, + }, + { + analysisIdOriginal: '57747-8', + isWaitingForResults: false, + analysisName: 'Erütrotsüüdid', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: 5, + normStatus: 0, + responseTime: '2024-02-29T10:13:01+00:00', + responseValue: null, + responseValueIsNegative: true, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '57747-8', + }, + }, + { + analysisIdOriginal: '2276-4', + isWaitingForResults: false, + analysisName: 'Ferritiin', + results: { + nestedElements: [], + unit: 'µg/l', + normLower: 28, + normUpper: 370, + normStatus: 0, + responseTime: '2024-02-29T10:46:54+00:00', + responseValue: 204.1, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '2276-4', + }, + }, + { + analysisIdOriginal: '14771-0', + isWaitingForResults: false, + analysisName: 'Glükoos', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: 4.1, + normUpper: 6, + normStatus: 0, + responseTime: '2024-02-29T10:06:24+00:00', + responseValue: 5.4, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '14771-0', + }, + }, + { + analysisIdOriginal: '59156-0', + isWaitingForResults: false, + analysisName: 'Glükoos', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: 2, + normStatus: 0, + responseTime: '2024-02-29T10:13:01+00:00', + responseValue: null, + responseValueIsNegative: null, + responseValueIsWithinNorm: false, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '59156-0', + }, + }, + { + analysisIdOriginal: '13955-0', + isWaitingForResults: false, + analysisName: 'HCV Ab', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: null, + normStatus: 0, + responseTime: '2024-02-29T13:44:48+00:00', + responseValue: null, + responseValueIsNegative: true, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '13955-0', + }, + }, + { + analysisIdOriginal: '14646-4', + isWaitingForResults: false, + analysisName: 'HDL kolesterool', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: 1, + normUpper: null, + normStatus: 1, + responseTime: '2024-02-29T10:20:55+00:00', + responseValue: 0.8, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '14646-4', + }, + }, + { + analysisIdOriginal: '2000-8', + isWaitingForResults: false, + analysisName: 'Kaltsium', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: 2.1, + normUpper: 2.55, + normStatus: 0, + responseTime: '2024-02-29T10:12:10+00:00', + responseValue: 2.49, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '2000-8', + }, + }, + { + analysisIdOriginal: '59158-6', + isWaitingForResults: false, + analysisName: 'Ketokehad', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: 0.5, + normStatus: 0, + responseTime: '2024-02-29T10:13:01+00:00', + responseValue: null, + responseValueIsNegative: true, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '59158-6', + }, + }, + { + analysisIdOriginal: '14647-2', + isWaitingForResults: false, + analysisName: 'Kolesterool', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: null, + normUpper: 5, + normStatus: 1, + responseTime: '2024-02-29T10:20:34+00:00', + responseValue: 5.7, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '14647-2', + }, + }, + { + analysisIdOriginal: '14682-9', + isWaitingForResults: false, + analysisName: 'Kreatiniin', + results: { + nestedElements: [], + unit: 'µmol/l', + normLower: 64, + normUpper: 111, + normStatus: 0, + responseTime: '2024-02-29T10:19:00+00:00', + responseValue: 89, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '14682-9', + }, + }, + { + analysisIdOriginal: '22748-8', + isWaitingForResults: false, + analysisName: 'LDL kolesterool', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: null, + normUpper: 3, + normStatus: 1, + responseTime: '2024-02-29T10:21:15+00:00', + responseValue: 4.3, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '22748-8', + }, + }, + { + analysisIdOriginal: '58805-3', + isWaitingForResults: false, + analysisName: 'Leukotsüüdid', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: 10, + normStatus: 0, + responseTime: '2024-02-29T10:13:01+00:00', + responseValue: null, + responseValueIsNegative: true, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '58805-3', + }, + }, + { + analysisIdOriginal: '2601-3', + isWaitingForResults: false, + analysisName: 'Magneesium', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: 0.66, + normUpper: 1.07, + normStatus: 0, + responseTime: '2024-02-29T10:17:26+00:00', + responseValue: 0.82, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '2601-3', + }, + }, + { + analysisIdOriginal: '70204-3', + isWaitingForResults: false, + analysisName: 'Mitte-HDL kolesterool', + results: { + nestedElements: [], + labComment: 'Mitte-paastu veri <3,9 mmol/L', + unit: 'mmol/l', + normLower: null, + normUpper: 3.8, + normStatus: 1, + responseTime: '2024-02-29T10:20:55+00:00', + responseValue: 4.9, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '70204-3', + }, + }, + { + analysisIdOriginal: '14798-3', + isWaitingForResults: false, + analysisName: 'Raud', + results: { + nestedElements: [], + unit: 'µmol/l', + normLower: 11.6, + normUpper: 31.3, + normStatus: 0, + responseTime: '2024-02-29T10:21:16+00:00', + responseValue: 16.5, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '14798-3', + }, + }, + { + analysisIdOriginal: '14927-8', + isWaitingForResults: false, + analysisName: 'Triglütseriidid', + results: { + nestedElements: [], + labComment: 'Mitte-paastu veri <2,0 mmol/L', + unit: 'mmol/l', + normLower: null, + normUpper: 1.7, + normStatus: 1, + responseTime: '2024-02-29T10:21:16+00:00', + responseValue: 1.89, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '14927-8', + }, + }, + { + analysisIdOriginal: '3016-3', + isWaitingForResults: false, + analysisName: 'TSH', + results: { + nestedElements: [], + unit: 'mIU/l', + normLower: 0.4, + normUpper: 4, + normStatus: 0, + responseTime: '2024-02-29T10:49:02+00:00', + responseValue: 1.27, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '3016-3', + }, + }, + { + analysisIdOriginal: '22664-7', + isWaitingForResults: false, + analysisName: 'Uurea', + results: { + nestedElements: [], + unit: 'mmol/l', + normLower: 3.2, + normUpper: 7.4, + normStatus: 0, + responseTime: '2024-02-29T10:19:11+00:00', + responseValue: 6.4, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '22664-7', + }, + }, + { + analysisIdOriginal: '50561-0', + isWaitingForResults: false, + analysisName: 'Valk', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: 0.25, + normStatus: 0, + responseTime: '2024-02-29T10:13:01+00:00', + responseValue: null, + responseValueIsNegative: true, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '50561-0', + }, + }, + { + analysisIdOriginal: '60493-4', + isWaitingForResults: false, + analysisName: 'Vitamiin D (25-OH)', + results: { + nestedElements: [], + labComment: 'Väärtus >75 nmol/l on D-vitamiini tervislik tase', + unit: 'nmol/l', + normLower: 75, + normUpper: null, + normStatus: 0, + responseTime: '2024-02-29T10:49:22+00:00', + responseValue: 105.5, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '60493-4', + }, + }, + { + analysisIdOriginal: '60025-4', + isWaitingForResults: false, + analysisName: 'Urobilinogeen', + results: { + nestedElements: [], + unit: null, + normLower: null, + normUpper: 17, + normStatus: 0, + responseTime: '2024-02-29T10:13:01+00:00', + responseValue: null, + responseValueIsNegative: null, + responseValueIsWithinNorm: true, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '60025-4', + }, + }, ], }; const big2: AnalysisTestResponse = { - "id": 3, - "orderedAnalysisElements": [ + id: 3, + orderedAnalysisElements: [ { - "analysisIdOriginal": "1988-5", - "isWaitingForResults": false, - "analysisName": "CRP", - "results": { - "nestedElements": [], - "unit": "mg/L", - "normLower": null, - "normUpper": 5, - "normStatus": 0, - "responseTime": "2025-09-12T14:02:04+00:00", - "responseValue": 1, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "status": "4", - "analysisElementOriginalId": "1988-5" - } + analysisIdOriginal: '1988-5', + isWaitingForResults: false, + analysisName: 'CRP', + results: { + nestedElements: [], + unit: 'mg/L', + normLower: null, + normUpper: 5, + normStatus: 0, + responseTime: '2025-09-12T14:02:04+00:00', + responseValue: 1, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + normLowerIncluded: false, + normUpperIncluded: false, + status: '4', + analysisElementOriginalId: '1988-5', + }, }, { - "analysisIdOriginal": "57021-8", - "isWaitingForResults": false, - "analysisName": "Hemogramm", - "results": { - "nestedElements": [ + analysisIdOriginal: '57021-8', + isWaitingForResults: false, + analysisName: 'Hemogramm', + results: { + nestedElements: [ { - "status": 4, - "unit": "g/L", - "normLower": 134, - "normUpper": 170, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 150, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "718-7" + status: 4, + unit: 'g/L', + normLower: 134, + normUpper: 170, + normStatus: 0, + responseTime: '2025-09-12 14:02:03', + responseValue: 150, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '718-7', }, { - "status": 4, - "unit": "%", - "normLower": 40, - "normUpper": 49, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 45, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "4544-3" + status: 4, + unit: '%', + normLower: 40, + normUpper: 49, + normStatus: 0, + responseTime: '2025-09-12 14:02:03', + responseValue: 45, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '4544-3', }, { - "status": 4, - "unit": "E9/L", - "normLower": 4.1, - "normUpper": 9.7, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "6690-2" + status: 4, + unit: 'E9/L', + normLower: 4.1, + normUpper: 9.7, + normStatus: 0, + responseTime: '2025-09-12 14:02:03', + responseValue: 5, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '6690-2', }, { - "status": 4, - "unit": "E12/L", - "normLower": 4.5, - "normUpper": 5.7, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "789-8" + status: 4, + unit: 'E12/L', + normLower: 4.5, + normUpper: 5.7, + normStatus: 0, + responseTime: '2025-09-12 14:02:03', + responseValue: 5, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '789-8', }, { - "status": 4, - "unit": "fL", - "normLower": 82, - "normUpper": 95, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 85, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "787-2" + status: 4, + unit: 'fL', + normLower: 82, + normUpper: 95, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 85, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '787-2', }, { - "status": 4, - "unit": "pg", - "normLower": 28, - "normUpper": 33, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 30, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "785-6" + status: 4, + unit: 'pg', + normLower: 28, + normUpper: 33, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 30, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '785-6', }, { - "status": 4, - "unit": "g/L", - "normLower": 322, - "normUpper": 356, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 355, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "786-4" + status: 4, + unit: 'g/L', + normLower: 322, + normUpper: 356, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 355, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '786-4', }, { - "status": 4, - "unit": "%", - "normLower": 12, - "normUpper": 15, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 15, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "788-0" + status: 4, + unit: '%', + normLower: 12, + normUpper: 15, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 15, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '788-0', }, { - "status": 4, - "unit": "E9/L", - "normLower": 157, - "normUpper": 372, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 255, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "777-3" + status: 4, + unit: 'E9/L', + normLower: 157, + normUpper: 372, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 255, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '777-3', }, { - "status": 4, - "unit": "%", - "normLower": 0.18, - "normUpper": 0.38, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.2, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "51637-7" + status: 4, + unit: '%', + normLower: 0.18, + normUpper: 0.38, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0.2, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '51637-7', }, { - "status": 4, - "unit": "fL", - "normLower": 9.2, - "normUpper": 12.3, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 10, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "32623-1" + status: 4, + unit: 'fL', + normLower: 9.2, + normUpper: 12.3, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 10, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '32623-1', }, { - "status": 4, - "unit": "fL", - "normLower": 10.1, - "normUpper": 16.2, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 15, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "32207-3" + status: 4, + unit: 'fL', + normLower: 10.1, + normUpper: 16.2, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 15, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '32207-3', }, { - "status": 4, - "unit": "E9/L", - "normLower": 0.01, - "normUpper": 0.08, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.05, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "704-7" + status: 4, + unit: 'E9/L', + normLower: 0.01, + normUpper: 0.08, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0.05, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '704-7', }, { - "status": 4, - "unit": "E9/L", - "normLower": 0.02, - "normUpper": 0.4, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.05, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "711-2" + status: 4, + unit: 'E9/L', + normLower: 0.02, + normUpper: 0.4, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0.05, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '711-2', }, { - "status": 4, - "unit": "E9/L", - "normLower": 1.9, - "normUpper": 6.7, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "751-8" + status: 4, + unit: 'E9/L', + normLower: 1.9, + normUpper: 6.7, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 5, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '751-8', }, { - "status": 4, - "unit": "E9/L", - "normLower": 0.24, - "normUpper": 0.8, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0.5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "742-7" + status: 4, + unit: 'E9/L', + normLower: 0.24, + normUpper: 0.8, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0.5, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '742-7', }, { - "status": 4, - "unit": "E9/L", - "normLower": 1.3, - "normUpper": 3.1, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 1.5, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "731-0" + status: 4, + unit: 'E9/L', + normLower: 1.3, + normUpper: 3.1, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 1.5, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '731-0', }, { - "status": 4, - "unit": "E9/L", - "normLower": 0, - "normUpper": 0.03, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:03", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "51584-1" + status: 4, + unit: 'E9/L', + normLower: 0, + normUpper: 0.03, + normStatus: 0, + responseTime: '2025-09-12 14:02:03', + responseValue: 0, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '51584-1', }, { - "status": 4, - "unit": "%", - "normLower": 0, - "normUpper": 0.5, - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "38518-7" + status: 4, + unit: '%', + normLower: 0, + normUpper: 0.5, + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '38518-7', }, { - "status": 4, - "unit": "E9/L", - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "771-6" + status: 4, + unit: 'E9/L', + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '771-6', }, { - "status": 4, - "unit": "/100WBC", - "normStatus": 0, - "responseTime": "2025-09-12 14:02:04", - "responseValue": 0, - "normLowerIncluded": false, - "normUpperIncluded": false, - "analysisElementOriginalId": "58413-6" - } + status: 4, + unit: '/100WBC', + normStatus: 0, + responseTime: '2025-09-12 14:02:04', + responseValue: 0, + normLowerIncluded: false, + normUpperIncluded: false, + analysisElementOriginalId: '58413-6', + }, ], - "unit": null, - "normLower": null, - "normUpper": null, - "normStatus": null, - "responseTime": null, - "responseValue": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "responseValueIsNegative": false, - "responseValueIsWithinNorm": false, - "status": "4", - "analysisElementOriginalId": "57021-8" - } + unit: null, + normLower: null, + normUpper: null, + normStatus: null, + responseTime: null, + responseValue: null, + normLowerIncluded: false, + normUpperIncluded: false, + responseValueIsNegative: false, + responseValueIsWithinNorm: false, + status: '4', + analysisElementOriginalId: '57021-8', + }, }, { - "analysisIdOriginal": "43583-4", - "isWaitingForResults": false, - "analysisName": "Lipoproteiin a", - "results": { - "nestedElements": [], - "labComment": "Kliendi soovil analüüs tühistatud.", - "unit": null, - "normLower": null, - "normUpper": null, - "normStatus": null, - "responseTime": null, - "responseValue": null, - "normLowerIncluded": false, - "normUpperIncluded": false, - "responseValueIsNegative": false, - "responseValueIsWithinNorm": false, - "status": "5", - "analysisElementOriginalId": "43583-4" - } + analysisIdOriginal: '43583-4', + isWaitingForResults: false, + analysisName: 'Lipoproteiin a', + results: { + nestedElements: [], + labComment: 'Kliendi soovil analüüs tühistatud.', + unit: null, + normLower: null, + normUpper: null, + normStatus: null, + responseTime: null, + responseValue: null, + normLowerIncluded: false, + normUpperIncluded: false, + responseValueIsNegative: false, + responseValueIsWithinNorm: false, + status: '5', + analysisElementOriginalId: '43583-4', + }, }, { - "analysisIdOriginal": "60493-4", - "isWaitingForResults": false, - "analysisName": "Vitamiin D (25-OH)", - "results": { - "nestedElements": [], - "labComment": "Väärtus vahemikus 30-49.9 nmol/L on D-vitamiini ebapiisav tase.", - "unit": "nmol/L", - "normLower": 75, - "normUpper": null, - "normStatus": 1, - "responseTime": "2025-09-12T14:02:04+00:00", - "responseValue": 30, - "normLowerIncluded": false, - "normUpperIncluded": false, - "responseValueIsNegative": null, - "responseValueIsWithinNorm": null, - "status": "4", - "analysisElementOriginalId": "60493-4" - } - } + analysisIdOriginal: '60493-4', + isWaitingForResults: false, + analysisName: 'Vitamiin D (25-OH)', + results: { + nestedElements: [], + labComment: + 'Väärtus vahemikus 30-49.9 nmol/L on D-vitamiini ebapiisav tase.', + unit: 'nmol/L', + normLower: 75, + normUpper: null, + normStatus: 1, + responseTime: '2025-09-12T14:02:04+00:00', + responseValue: 30, + normLowerIncluded: false, + normUpperIncluded: false, + responseValueIsNegative: null, + responseValueIsWithinNorm: null, + status: '4', + analysisElementOriginalId: '60493-4', + }, + }, ], }; -export const analysisResponses: AnalysisTestResponse[] = [ - empty1, - big1, - big2, -]; +export const analysisResponses: AnalysisTestResponse[] = [empty1, big1, big2]; diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index 65d3a67..f3e603c 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -1,7 +1,7 @@ import { redirect } from 'next/navigation'; import { toTitleCase } from '@/lib/utils'; -import { createUserAnalysesApi } from '@kit/user-analyses/api'; +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { PageBody, PageHeader } from '@kit/ui/page'; @@ -13,7 +13,10 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; import Recommendations from '../_components/recommendations'; +import { loadAnalyses } from '../_lib/server/load-analyses'; +import { loadRecommendations } from '../_lib/server/load-recommendations'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; +import { analysisResponses } from './analysis-results/test/test-responses'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -31,6 +34,15 @@ async function UserHomePage() { const { account } = await loadCurrentUserAccount(); const api = createUserAnalysesApi(client); const bmiThresholds = await api.fetchBmiThresholds(); + const { analyses, countryCode } = await loadAnalyses(); + + const analysisRecommendations = await loadRecommendations( + analysisResponses, + analyses, + account, + ); + + console.log('analysisRecommendations', analysisRecommendations); if (!account) { redirect('/'); @@ -51,7 +63,12 @@ async function UserHomePage() { - + + analysisRecommendations.includes(analysis.title), + )} + countryCode={countryCode} + /> ); diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index 80bc725..d9eace1 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -1,26 +1,24 @@ 'use client'; -import React, { useEffect } from 'react'; +import React from 'react'; -import OpenAI from 'openai'; +import OrderAnalysesCards, { OrderAnalysisCard } from './order-analyses-cards'; -export default function Recommendations() { - // const client = new OpenAI(); +export default function Recommendations({ + recommended, + countryCode, +}: { + recommended: OrderAnalysisCard[]; + countryCode: string; +}) { + if (recommended.length < 1) { + return null; + } - const getRecommendations = async () => { - // const response = await client.responses.create({ - // model: 'gpt-5', - // input: 'Write a short bedtime story about a unicorn.', - // }); - // console.log(response.output_text); - }; - - // useEffect(() => { - // console.log('process.env', process.env); - // if (process.env.OPENAI_API_KEY) { - // getRecommendations(); - // } - // }, []); - - return
Recommendations
; + return ( +
+

Medreport soovitab teile

+ +
+ ); } diff --git a/app/home/(user)/_lib/server/load-recommendations.test.ts b/app/home/(user)/_lib/server/load-recommendations.test.ts new file mode 100644 index 0000000..b492eae --- /dev/null +++ b/app/home/(user)/_lib/server/load-recommendations.test.ts @@ -0,0 +1,133 @@ +import type { Mock } from 'jest-mock'; + +// ---- Mocks you can tweak per test ---- +const createResponseMock = jest.fn(); +const getLatestResponseTimeMock = jest.fn(); +const getLatestUniqueAnalysResponsesMock = jest.fn(); +const parsePersonalCodeMock = jest.fn(() => ({ gender: { value: 'male' } })); + +// Mock OpenAI SDK +jest.mock('openai', () => { + return { + __esModule: true, + default: class OpenAI { + responses = { create: createResponseMock }; + }, + }; +}); + +// Mock next/cache (global cache map so it persists between calls) +const globalCache = new Map(); +jest.mock('next/cache', () => { + return { + __esModule: true, + unstable_cache: + (fn: (...args: any[]) => Promise, keyParts: any[], _opts?: any) => + async (...args: any[]) => { + const key = JSON.stringify(keyParts); + if (globalCache.has(key)) return globalCache.get(key); + const val = await fn(...args); + globalCache.set(key, val); + return val; + }, + }; +}); + +// Mock your analysis helpers + personal code parser +jest.mock('../src/analysis-utils', () => ({ + __esModule: true, + getLatestUniqueAnalysResponses: getLatestUniqueAnalysResponsesMock, + getLatestResponseTime: getLatestResponseTimeMock, +})); +jest.mock('../src/personal-code', () => ({ + __esModule: true, + PersonalCode: { parsePersonalCode: parsePersonalCodeMock }, +})); + +describe('loadRecommendations', () => { + beforeEach(() => { + createResponseMock.mockReset(); + getLatestResponseTimeMock.mockReset(); + getLatestUniqueAnalysResponsesMock.mockReset(); + globalCache.clear(); + }); + + it('should call OpenAI once when latest date stays the same (cache hit on 2nd call)', async () => { + const date1 = new Date('2025-09-16T12:00:00Z'); + getLatestResponseTimeMock.mockReturnValue(date1); + getLatestUniqueAnalysResponsesMock.mockImplementation((arr: any[]) => arr); + + createResponseMock.mockResolvedValue({ + output_text: JSON.stringify({ recommended: ['A', 'B'] }), + }); + + const { loadRecommendations } = await import('./load-recommendations'); + + const analysisResponses = [ + { name: 'x', value: 1, responseTime: date1 }, + ] as any[]; + const analyses = [{ title: 't', description: 'd' }] as any[]; + const account = { id: 'u1', personal_code: '12345678901' } as any; + + // Act: 1st call (MISS) + const out1 = await loadRecommendations( + analysisResponses, + analyses, + account, + ); + + // Act: 2nd call with same date (HIT) + const out2 = await loadRecommendations( + analysisResponses, + analyses, + account, + ); + + // Assert: only one API call, result reused + expect(createResponseMock).toHaveBeenCalledTimes(1); + expect(out1).toEqual(['A', 'B']); + expect(out2).toEqual(['A', 'B']); + }); + + it('should call OpenAI again when latest date changes (new cache key)', async () => { + const date1 = new Date('2025-09-16T12:00:00Z'); + const date2 = new Date('2025-09-17T00:00:00Z'); + + getLatestResponseTimeMock.mockReturnValueOnce(date1); + getLatestResponseTimeMock.mockReturnValueOnce(date2); + + getLatestUniqueAnalysResponsesMock.mockImplementation((arr: any[]) => arr); + + createResponseMock + .mockResolvedValueOnce({ + output_text: JSON.stringify({ recommended: ['A', 'B'] }), + }) + .mockResolvedValueOnce({ + output_text: JSON.stringify({ recommended: ['C'] }), + }); + + const { loadRecommendations } = await import('./load-recommendations'); + + const analysisResponses = [ + { name: 'x', value: 1, responseTime: date1 }, + ] as any[]; + const analyses = [{ title: 't', description: 'd' }] as any[]; + const account = { id: 'u1', personal_code: '12345678901' } as any; + + const out1 = await loadRecommendations( + analysisResponses, + analyses, + account, + ); + + const out2 = await loadRecommendations( + analysisResponses, + analyses, + account, + ); + + expect(createResponseMock).toHaveBeenCalledTimes(2); + expect(out1).toEqual(['A', 'B']); + expect(out2).toEqual(['C']); + }); +}); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts new file mode 100644 index 0000000..ed8e6f1 --- /dev/null +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -0,0 +1,153 @@ +import { cache } from 'react'; + +import { unstable_cache } from 'next/cache'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import crypto from 'crypto'; +import OpenAI from 'openai'; + +import PersonalCode from '~/lib/utils'; + +import { AnalysisTestResponse } from '../../(dashboard)/analysis-results/test/test-responses'; +import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; + +export const loadRecommendations = cache(recommendationsLoader); + +type FormattedAnalysisResponse = { + value: string; + name: string; + responseTime: string; +}; + +function canonical(v: unknown): string { + return JSON.stringify(v, (_k, val) => { + if (val && typeof val === 'object' && !Array.isArray(val)) { + // sort object keys for stable JSON + return Object.keys(val) + .sort() + .reduce((o: any, k) => ((o[k] = (val as any)[k]), o), {}); + } + return val; + }); +} +const sha256 = (s: string) => + crypto.createHash('sha256').update(s).digest('hex'); + +const getLatestResponseTime = (items: FormattedAnalysisResponse[]) => { + if (!items?.length) return null; + + let latest = null; + for (const it of items) { + const d = new Date(it.responseTime); + const t = d.getTime(); + if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) { + latest = d; + } + } + return latest; +}; + +const getLatestUniqueAnalysResponses = ( + analysisResponses: AnalysisTestResponse[], +): { name: string; value: string; responseTime: string }[] => { + const analysisElements = analysisResponses + .map(({ orderedAnalysisElements }) => orderedAnalysisElements) + .flat(); + + console.log('analysisElements', analysisElements.length); + + const map = new Map(); + for (const it of analysisElements) { + const prev = map.get(it.analysisName); + if (it.results.responseTime) { + if ( + !prev || + new Date(it.results.responseTime) > new Date(prev.responseTime) + ) { + map.set(it.analysisName, { + name: it.analysisName, + value: it.results.responseValue, + responseTime: it.results.responseTime, + }); + } + } + } + return [...map.values()]; +}; + +async function recommendationsLoader( + analysisResponses: AnalysisTestResponse[], + analyses: OrderAnalysisCard[], + account: AccountWithParams | null, +): Promise { + if (!account?.personal_code) { + return []; + } + + const client = new OpenAI(); + const { gender } = PersonalCode.parsePersonalCode(account.personal_code); + console.log('analysisResponses', analysisResponses); + console.log('analyises', analyses); + const latestUniqueAnalysResponses = + getLatestUniqueAnalysResponses(analysisResponses); + const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); + const latestISO = latestResponseTime + ? new Date(latestResponseTime).toISOString() + : 'none'; + console.log('latestResponseTime', latestResponseTime); + const formattedAnalysisResponses = latestUniqueAnalysResponses.map( + ({ name, value }) => ({ name, value }), + ); + const formattedAnalyses = analyses.map(({ description, title }) => ({ + description, + title, + })); + + console.log('formattedAnalyses', JSON.stringify(formattedAnalyses)); + console.log( + 'latestUniqueAnalysResponses', + JSON.stringify(latestUniqueAnalysResponses), + ); + const response = await client.responses.create({ + model: 'gpt-5', + store: false, + prompt: { + id: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', + variables: { + analyses: JSON.stringify(formattedAnalyses), + results: JSON.stringify(formattedAnalysisResponses), + gender: gender.value, + }, + }, + }); + + const responseJson = JSON.parse(response.output_text); + console.log('responseJson: ', responseJson); + + const keyPayload = { + model: 'gpt-5', // swap to a model your project can access + promptId: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', + latestISO, + }; + + const key = 'recs:' + sha256(canonical(keyPayload)); + + const run = unstable_cache(async () => { + const response = await client.responses.create({ + model: keyPayload.model, + store: false, + prompt: { + id: keyPayload.promptId, + variables: { + analyses: JSON.stringify(formattedAnalyses), + results: JSON.stringify(latestUniqueAnalysResponses), + gender: gender.value, + }, + }, + }); + const json = JSON.parse(response.output_text); + return json.recommended; + }, ['recommendations', key]); + + return await run(); +} diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx index 8279c92..12d9715 100644 --- a/app/home/[account]/page.tsx +++ b/app/home/[account]/page.tsx @@ -4,8 +4,8 @@ import { use } from 'react'; import { CompanyGuard } from '@/packages/features/team-accounts/src/components'; import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api'; +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; -import { createUserAnalysesApi } from '@kit/user-analyses/api'; import { PageBody } from '@kit/ui/page'; From addd3a5dae4e088ce9e3606fc20c2dc57a8f495e Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 19 Sep 2025 16:54:21 +0300 Subject: [PATCH 04/10] send analyses to ai --- app/home/(user)/(dashboard)/page.tsx | 26 +--- .../(user)/_components/recommendations.tsx | 34 +++-- .../_lib/server/load-recommendations.test.ts | 133 ------------------ .../_lib/server/load-recommendations.ts | 103 +++++--------- 4 files changed, 66 insertions(+), 230 deletions(-) delete mode 100644 app/home/(user)/_lib/server/load-recommendations.test.ts diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index f3e603c..c2a279e 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -1,3 +1,5 @@ +import { Suspense } from 'react'; + import { redirect } from 'next/navigation'; import { toTitleCase } from '@/lib/utils'; @@ -13,10 +15,8 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; import Recommendations from '../_components/recommendations'; -import { loadAnalyses } from '../_lib/server/load-analyses'; -import { loadRecommendations } from '../_lib/server/load-recommendations'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; -import { analysisResponses } from './analysis-results/test/test-responses'; +import Loading from '../loading'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -28,21 +28,11 @@ export const generateMetadata = async () => { }; async function UserHomePage() { - console.log('process.env', process.env.OPENAI_API_KEY); const client = getSupabaseServerClient(); const { account } = await loadCurrentUserAccount(); const api = createUserAnalysesApi(client); const bmiThresholds = await api.fetchBmiThresholds(); - const { analyses, countryCode } = await loadAnalyses(); - - const analysisRecommendations = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - console.log('analysisRecommendations', analysisRecommendations); if (!account) { redirect('/'); @@ -62,13 +52,9 @@ async function UserHomePage() { /> - - - analysisRecommendations.includes(analysis.title), - )} - countryCode={countryCode} - /> + + + ); diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index d9eace1..dc45427 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -1,24 +1,40 @@ -'use client'; +'use server'; -import React from 'react'; +import React, { useState } from 'react'; +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; + +import { analysisResponses } from '../(dashboard)/analysis-results/test/test-responses'; +import { loadAnalyses } from '../_lib/server/load-analyses'; +import { loadRecommendations } from '../_lib/server/load-recommendations'; import OrderAnalysesCards, { OrderAnalysisCard } from './order-analyses-cards'; -export default function Recommendations({ - recommended, - countryCode, +export default async function Recommendations({ + account, }: { - recommended: OrderAnalysisCard[]; - countryCode: string; + account: AccountWithParams; }) { - if (recommended.length < 1) { + const { analyses, countryCode } = await loadAnalyses(); + const [isLoadingTimeSlots, setIsLoadingTimeSlots] = useState(false); + const analysisResults = analysisResponses; + console.log('selectedDate', isLoadingTimeSlots); + const analysisRecommendations = await loadRecommendations( + analysisResults, + analyses, + account, + ); + const orderAnalyses = analyses.filter((analysis) => + analysisRecommendations.includes(analysis.title), + ); + console.log('analysisRecommendations', analysisRecommendations); + if (orderAnalyses.length < 1) { return null; } return (

Medreport soovitab teile

- +
); } diff --git a/app/home/(user)/_lib/server/load-recommendations.test.ts b/app/home/(user)/_lib/server/load-recommendations.test.ts deleted file mode 100644 index b492eae..0000000 --- a/app/home/(user)/_lib/server/load-recommendations.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Mock } from 'jest-mock'; - -// ---- Mocks you can tweak per test ---- -const createResponseMock = jest.fn(); -const getLatestResponseTimeMock = jest.fn(); -const getLatestUniqueAnalysResponsesMock = jest.fn(); -const parsePersonalCodeMock = jest.fn(() => ({ gender: { value: 'male' } })); - -// Mock OpenAI SDK -jest.mock('openai', () => { - return { - __esModule: true, - default: class OpenAI { - responses = { create: createResponseMock }; - }, - }; -}); - -// Mock next/cache (global cache map so it persists between calls) -const globalCache = new Map(); -jest.mock('next/cache', () => { - return { - __esModule: true, - unstable_cache: - (fn: (...args: any[]) => Promise, keyParts: any[], _opts?: any) => - async (...args: any[]) => { - const key = JSON.stringify(keyParts); - if (globalCache.has(key)) return globalCache.get(key); - const val = await fn(...args); - globalCache.set(key, val); - return val; - }, - }; -}); - -// Mock your analysis helpers + personal code parser -jest.mock('../src/analysis-utils', () => ({ - __esModule: true, - getLatestUniqueAnalysResponses: getLatestUniqueAnalysResponsesMock, - getLatestResponseTime: getLatestResponseTimeMock, -})); -jest.mock('../src/personal-code', () => ({ - __esModule: true, - PersonalCode: { parsePersonalCode: parsePersonalCodeMock }, -})); - -describe('loadRecommendations', () => { - beforeEach(() => { - createResponseMock.mockReset(); - getLatestResponseTimeMock.mockReset(); - getLatestUniqueAnalysResponsesMock.mockReset(); - globalCache.clear(); - }); - - it('should call OpenAI once when latest date stays the same (cache hit on 2nd call)', async () => { - const date1 = new Date('2025-09-16T12:00:00Z'); - getLatestResponseTimeMock.mockReturnValue(date1); - getLatestUniqueAnalysResponsesMock.mockImplementation((arr: any[]) => arr); - - createResponseMock.mockResolvedValue({ - output_text: JSON.stringify({ recommended: ['A', 'B'] }), - }); - - const { loadRecommendations } = await import('./load-recommendations'); - - const analysisResponses = [ - { name: 'x', value: 1, responseTime: date1 }, - ] as any[]; - const analyses = [{ title: 't', description: 'd' }] as any[]; - const account = { id: 'u1', personal_code: '12345678901' } as any; - - // Act: 1st call (MISS) - const out1 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - // Act: 2nd call with same date (HIT) - const out2 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - // Assert: only one API call, result reused - expect(createResponseMock).toHaveBeenCalledTimes(1); - expect(out1).toEqual(['A', 'B']); - expect(out2).toEqual(['A', 'B']); - }); - - it('should call OpenAI again when latest date changes (new cache key)', async () => { - const date1 = new Date('2025-09-16T12:00:00Z'); - const date2 = new Date('2025-09-17T00:00:00Z'); - - getLatestResponseTimeMock.mockReturnValueOnce(date1); - getLatestResponseTimeMock.mockReturnValueOnce(date2); - - getLatestUniqueAnalysResponsesMock.mockImplementation((arr: any[]) => arr); - - createResponseMock - .mockResolvedValueOnce({ - output_text: JSON.stringify({ recommended: ['A', 'B'] }), - }) - .mockResolvedValueOnce({ - output_text: JSON.stringify({ recommended: ['C'] }), - }); - - const { loadRecommendations } = await import('./load-recommendations'); - - const analysisResponses = [ - { name: 'x', value: 1, responseTime: date1 }, - ] as any[]; - const analyses = [{ title: 't', description: 'd' }] as any[]; - const account = { id: 'u1', personal_code: '12345678901' } as any; - - const out1 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - const out2 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - expect(createResponseMock).toHaveBeenCalledTimes(2); - expect(out1).toEqual(['A', 'B']); - expect(out2).toEqual(['C']); - }); -}); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index ed8e6f1..a843c47 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -1,9 +1,6 @@ import { cache } from 'react'; -import { unstable_cache } from 'next/cache'; - import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import crypto from 'crypto'; import OpenAI from 'openai'; import PersonalCode from '~/lib/utils'; @@ -19,20 +16,6 @@ type FormattedAnalysisResponse = { responseTime: string; }; -function canonical(v: unknown): string { - return JSON.stringify(v, (_k, val) => { - if (val && typeof val === 'object' && !Array.isArray(val)) { - // sort object keys for stable JSON - return Object.keys(val) - .sort() - .reduce((o: any, k) => ((o[k] = (val as any)[k]), o), {}); - } - return val; - }); -} -const sha256 = (s: string) => - crypto.createHash('sha256').update(s).digest('hex'); - const getLatestResponseTime = (items: FormattedAnalysisResponse[]) => { if (!items?.length) return null; @@ -56,23 +39,29 @@ const getLatestUniqueAnalysResponses = ( console.log('analysisElements', analysisElements.length); - const map = new Map(); - for (const it of analysisElements) { - const prev = map.get(it.analysisName); - if (it.results.responseTime) { - if ( - !prev || - new Date(it.results.responseTime) > new Date(prev.responseTime) - ) { - map.set(it.analysisName, { - name: it.analysisName, - value: it.results.responseValue, - responseTime: it.results.responseTime, - }); - } + const byName = analysisElements.reduce< + Record + >((acc, it) => { + const responseTime = it?.results?.responseTime; + const responseValue = it?.results?.responseValue; + if (!responseTime || !responseValue) return acc; + + const key = it.analysisName; + const cur = acc[key]; + const t = Date.parse(responseTime); + const prevT = cur ? Date.parse(cur.responseTime) : -Infinity; + + if (!cur || t > prevT) { + acc[key] = { + name: key, + value: responseValue.toString(), + responseTime, + }; } - } - return [...map.values()]; + return acc; + }, {}); + + return Object.values(byName); }; async function recommendationsLoader( @@ -80,6 +69,9 @@ async function recommendationsLoader( analyses: OrderAnalysisCard[], account: AccountWithParams | null, ): Promise { + if (!process.env.OPENAI_API_KEY) { + return []; + } if (!account?.personal_code) { return []; } @@ -90,11 +82,11 @@ async function recommendationsLoader( console.log('analyises', analyses); const latestUniqueAnalysResponses = getLatestUniqueAnalysResponses(analysisResponses); - const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); - const latestISO = latestResponseTime - ? new Date(latestResponseTime).toISOString() - : 'none'; - console.log('latestResponseTime', latestResponseTime); + // const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); + // const latestISO = latestResponseTime + // ? new Date(latestResponseTime).toISOString() + // : 'none'; + // console.log('latestResponseTime', latestResponseTime); const formattedAnalysisResponses = latestUniqueAnalysResponses.map( ({ name, value }) => ({ name, value }), ); @@ -108,8 +100,9 @@ async function recommendationsLoader( 'latestUniqueAnalysResponses', JSON.stringify(latestUniqueAnalysResponses), ); + const response = await client.responses.create({ - model: 'gpt-5', + model: 'gpt-5-mini', store: false, prompt: { id: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', @@ -121,33 +114,7 @@ async function recommendationsLoader( }, }); - const responseJson = JSON.parse(response.output_text); - console.log('responseJson: ', responseJson); - - const keyPayload = { - model: 'gpt-5', // swap to a model your project can access - promptId: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', - latestISO, - }; - - const key = 'recs:' + sha256(canonical(keyPayload)); - - const run = unstable_cache(async () => { - const response = await client.responses.create({ - model: keyPayload.model, - store: false, - prompt: { - id: keyPayload.promptId, - variables: { - analyses: JSON.stringify(formattedAnalyses), - results: JSON.stringify(latestUniqueAnalysResponses), - gender: gender.value, - }, - }, - }); - const json = JSON.parse(response.output_text); - return json.recommended; - }, ['recommendations', key]); - - return await run(); + const json = JSON.parse(response.output_text); + console.log('response.output_text', response.output_text); + return json.recommended; } From a52a9bec06148222135c2d49a6742efd4092a204 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Sat, 20 Sep 2025 18:12:15 +0300 Subject: [PATCH 05/10] add api --- .../(user)/_components/recommendations.tsx | 6 +-- .../_lib/server/load-recommendations.ts | 45 ++++++++++++------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index dc45427..02fa895 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -1,6 +1,6 @@ 'use server'; -import React, { useState } from 'react'; +import React from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; @@ -15,14 +15,14 @@ export default async function Recommendations({ account: AccountWithParams; }) { const { analyses, countryCode } = await loadAnalyses(); - const [isLoadingTimeSlots, setIsLoadingTimeSlots] = useState(false); const analysisResults = analysisResponses; - console.log('selectedDate', isLoadingTimeSlots); + const analysisRecommendations = await loadRecommendations( analysisResults, analyses, account, ); + const orderAnalyses = analyses.filter((analysis) => analysisRecommendations.includes(analysis.title), ); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index a843c47..0744697 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -1,6 +1,8 @@ import { cache } from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; +import { createServerClient } from '@supabase/ssr'; import OpenAI from 'openai'; import PersonalCode from '~/lib/utils'; @@ -75,18 +77,30 @@ async function recommendationsLoader( if (!account?.personal_code) { return []; } + const supabaseClient = getSupabaseServerClient(); - const client = new OpenAI(); - const { gender } = PersonalCode.parsePersonalCode(account.personal_code); - console.log('analysisResponses', analysisResponses); - console.log('analyises', analyses); + const analysesRecommendationsPromptId = + 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd'; const latestUniqueAnalysResponses = getLatestUniqueAnalysResponses(analysisResponses); - // const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); - // const latestISO = latestResponseTime - // ? new Date(latestResponseTime).toISOString() - // : 'none'; - // console.log('latestResponseTime', latestResponseTime); + const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); + const latestISO = latestResponseTime + ? new Date(latestResponseTime).toISOString() + : 'none'; + + const previouslyRecommended = await supabaseClient + .schema('medreport') + .from('ai_responses') + .select('*') + .eq('account_id', account.id) + .eq('prompt_id', analysesRecommendationsPromptId) + .eq('latest_data_change', latestISO); + + const openAIClient = new OpenAI(); + const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); + const weight = account.accountParams?.weight || 'unknown'; + console.log('analysisResponses', analysisResponses); + console.log('analyises', analyses); const formattedAnalysisResponses = latestUniqueAnalysResponses.map( ({ name, value }) => ({ name, value }), ); @@ -95,23 +109,20 @@ async function recommendationsLoader( title, })); - console.log('formattedAnalyses', JSON.stringify(formattedAnalyses)); - console.log( - 'latestUniqueAnalysResponses', - JSON.stringify(latestUniqueAnalysResponses), - ); - - const response = await client.responses.create({ + const response = await openAIClient.responses.create({ model: 'gpt-5-mini', store: false, prompt: { - id: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', + id: analysesRecommendationsPromptId, variables: { analyses: JSON.stringify(formattedAnalyses), results: JSON.stringify(formattedAnalysisResponses), gender: gender.value, + age: age.toString(), + weight: weight.toString(), }, }, + max_output_tokens: 100, }); const json = JSON.parse(response.output_text); From 643e67e2a1bea5846f13130a6328999f56dc3178 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Sat, 20 Sep 2025 18:42:18 +0300 Subject: [PATCH 06/10] update type --- .../_lib/server/load-recommendations.ts | 24 ++- packages/supabase/src/database.types.ts | 168 ++++++++++++++---- 2 files changed, 151 insertions(+), 41 deletions(-) diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index 0744697..7c222b2 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -96,6 +96,10 @@ async function recommendationsLoader( .eq('prompt_id', analysesRecommendationsPromptId) .eq('latest_data_change', latestISO); + if (previouslyRecommended.data?.[0]?.response) { + return previouslyRecommended.data[0].response; + } + const openAIClient = new OpenAI(); const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); const weight = account.accountParams?.weight || 'unknown'; @@ -110,7 +114,6 @@ async function recommendationsLoader( })); const response = await openAIClient.responses.create({ - model: 'gpt-5-mini', store: false, prompt: { id: analysesRecommendationsPromptId, @@ -126,6 +129,23 @@ async function recommendationsLoader( }); const json = JSON.parse(response.output_text); - console.log('response.output_text', response.output_text); + await supabaseClient + .schema('medreport') + .from('ai_responses') + .insert({ + account_id: account.id, + prompt_name: 'Analysis Recommendations', + prompt_id: analysesRecommendationsPromptId, + input: JSON.stringify({ + analyses: formattedAnalyses, + results: formattedAnalysisResponses, + gender, + age, + weight, + }), + latest_data_change: latestISO, + response: JSON.stringify(response.output_text), + }); + return json.recommended; } diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 6907fac..e620ad9 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -198,6 +198,7 @@ export type Database = { action: string changed_by: string created_at: string + extra_data: Json | null id: number } Insert: { @@ -205,6 +206,7 @@ export type Database = { action: string changed_by: string created_at?: string + extra_data?: Json | null id?: number } Update: { @@ -212,6 +214,7 @@ export type Database = { action?: string changed_by?: string created_at?: string + extra_data?: Json | null id?: number } Relationships: [] @@ -517,6 +520,61 @@ export type Database = { }, ] } + ai_responses: { + Row: { + account_id: string + created_at: string + id: string + input: Json + latest_data_change: string + prompt_id: string + prompt_name: string + response: Json + } + Insert: { + account_id: string + created_at?: string + id?: string + input: Json + latest_data_change: string + prompt_id: string + prompt_name: string + response: Json + } + Update: { + account_id?: string + created_at?: string + id?: string + input?: Json + latest_data_change?: string + prompt_id?: string + prompt_name?: string + response?: Json + } + Relationships: [ + { + foreignKeyName: "ai_responses_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "accounts" + referencedColumns: ["id"] + }, + { + foreignKeyName: "ai_responses_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_account_workspace" + referencedColumns: ["id"] + }, + { + foreignKeyName: "ai_responses_account_id_fkey" + columns: ["account_id"] + isOneToOne: false + referencedRelation: "user_accounts" + referencedColumns: ["id"] + }, + ] + } analyses: { Row: { analysis_id_oid: string @@ -685,11 +743,11 @@ export type Database = { norm_upper: number | null norm_upper_included: boolean | null original_response_element: Json - response_time: string + response_time: string | null response_value: number | null - response_value_is_negative?: boolean | null - response_value_is_within_norm?: boolean | null - status: string + response_value_is_negative: boolean | null + response_value_is_within_norm: boolean | null + status: string | null unit: string | null updated_at: string | null } @@ -706,11 +764,11 @@ export type Database = { norm_upper?: number | null norm_upper_included?: boolean | null original_response_element: Json - response_time: string - response_value: number | null + response_time?: string | null + response_value?: number | null response_value_is_negative?: boolean | null response_value_is_within_norm?: boolean | null - status: string + status?: string | null unit?: string | null updated_at?: string | null } @@ -727,11 +785,11 @@ export type Database = { norm_upper?: number | null norm_upper_included?: boolean | null original_response_element?: Json - response_time?: string + response_time?: string | null response_value?: number | null response_value_is_negative?: boolean | null response_value_is_within_norm?: boolean | null - status: string + status?: string | null unit?: string | null updated_at?: string | null } @@ -1159,7 +1217,7 @@ export type Database = { doctor_user_id: string | null id: number status: Database["medreport"]["Enums"]["analysis_feedback_status"] - updated_at: string + updated_at: string | null updated_by: string | null user_id: string value: string | null @@ -1171,7 +1229,7 @@ export type Database = { doctor_user_id?: string | null id?: number status?: Database["medreport"]["Enums"]["analysis_feedback_status"] - updated_at?: string + updated_at?: string | null updated_by?: string | null user_id: string value?: string | null @@ -1183,7 +1241,7 @@ export type Database = { doctor_user_id?: string | null id?: number status?: Database["medreport"]["Enums"]["analysis_feedback_status"] - updated_at?: string + updated_at?: string | null updated_by?: string | null user_id?: string value?: string | null @@ -1268,27 +1326,42 @@ export type Database = { } medipost_actions: { Row: { - created_at: string - id: number action: string - xml: string + created_at: string | null has_analysis_results: boolean - medipost_external_order_id: string - medipost_private_message_id: string - medusa_order_id: string - response_xml: string has_error: boolean + id: string + medipost_external_order_id: string | null + medipost_private_message_id: string | null + medusa_order_id: string | null + response_xml: string | null + xml: string | null } Insert: { action: string - xml: string - has_analysis_results: boolean - medipost_external_order_id: string - medipost_private_message_id: string - medusa_order_id: string - response_xml: string - has_error: boolean + created_at?: string | null + has_analysis_results?: boolean + has_error?: boolean + id?: string + medipost_external_order_id?: string | null + medipost_private_message_id?: string | null + medusa_order_id?: string | null + response_xml?: string | null + xml?: string | null } + Update: { + action?: string + created_at?: string | null + has_analysis_results?: boolean + has_error?: boolean + id?: string + medipost_external_order_id?: string | null + medipost_private_message_id?: string | null + medusa_order_id?: string | null + response_xml?: string | null + xml?: string | null + } + Relationships: [] } medreport_product_groups: { Row: { @@ -1957,6 +2030,13 @@ export type Database = { personal_code: string }[] } + get_latest_medipost_dispatch_state_for_order: { + Args: { medusa_order_id: string } + Returns: { + action_date: string + has_success: boolean + }[] + } get_medipost_dispatch_tries: { Args: { p_medusa_order_id: string } Returns: number @@ -2049,9 +2129,9 @@ export type Database = { Args: { account_id: string; user_id: string } Returns: boolean } - medipost_retry_dispatch: { - Args: { order_id: string } - Returns: Json + order_has_medipost_dispatch_error: { + Args: { medusa_order_id: string } + Returns: boolean } revoke_nonce: { Args: { p_id: string; p_reason?: string } @@ -2078,16 +2158,26 @@ export type Database = { Returns: undefined } update_account: { - Args: { - p_city: string - p_has_consent_personal_data: boolean - p_last_name: string - p_name: string - p_personal_code: string - p_phone: string - p_uid: string - p_email: string - } + Args: + | { + p_city: string + p_email: string + p_has_consent_personal_data: boolean + p_last_name: string + p_name: string + p_personal_code: string + p_phone: string + p_uid: string + } + | { + p_city: string + p_has_consent_personal_data: boolean + p_last_name: string + p_name: string + p_personal_code: string + p_phone: string + p_uid: string + } Returns: undefined } update_analysis_order_status: { From 3dd82902fe390ed77a84d21801445163f60f3c6b Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 23 Sep 2025 10:50:20 +0300 Subject: [PATCH 07/10] log recommendations --- app/home/(user)/(dashboard)/page.tsx | 4 ++-- .../(user)/_lib/server/load-recommendations.ts | 5 ++--- .../20250920184500_update_ai_responses.sql | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 supabase/migrations/20250920184500_update_ai_responses.sql diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index c2a279e..4ada1be 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -7,6 +7,7 @@ import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/ser import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { PageBody, PageHeader } from '@kit/ui/page'; +import { Skeleton } from '@kit/ui/skeleton'; import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; @@ -16,7 +17,6 @@ import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; import Recommendations from '../_components/recommendations'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; -import Loading from '../loading'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -52,7 +52,7 @@ async function UserHomePage() { /> - + }> diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index 7c222b2..573ec34 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -125,11 +125,10 @@ async function recommendationsLoader( weight: weight.toString(), }, }, - max_output_tokens: 100, }); const json = JSON.parse(response.output_text); - await supabaseClient + const updateAiResponse = await supabaseClient .schema('medreport') .from('ai_responses') .insert({ @@ -144,7 +143,7 @@ async function recommendationsLoader( weight, }), latest_data_change: latestISO, - response: JSON.stringify(response.output_text), + response: response.output_text, }); return json.recommended; diff --git a/supabase/migrations/20250920184500_update_ai_responses.sql b/supabase/migrations/20250920184500_update_ai_responses.sql new file mode 100644 index 0000000..d43d30b --- /dev/null +++ b/supabase/migrations/20250920184500_update_ai_responses.sql @@ -0,0 +1,17 @@ +ALTER TABLE medreport.ai_responses ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "ai_responses_select" ON medreport.ai_responses FOR SELECT TO authenticated USING (true); +CREATE POLICY "ai_responses_insert" ON medreport.ai_responses FOR INSERT TO authenticated WITH CHECK (true); + +grant select, insert, update, delete on table medreport.ai_responses to authenticated; + +ALTER TABLE medreport.ai_responses +ALTER COLUMN prompt_id TYPE text +USING prompt_name::text; + +ALTER TABLE medreport.ai_responses +ALTER COLUMN prompt_name TYPE text +USING prompt_name::text; + +ALTER TABLE medreport.ai_responses +ADD CONSTRAINT ai_responses_id_pkey PRIMARY KEY (id); From 4962ba8ec20fb486182fd1e055f5b141976ffdae Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 23 Sep 2025 14:27:41 +0300 Subject: [PATCH 08/10] add real data to ai --- .../analysis-results/test/test-responses.ts | 2 +- .../(user)/_components/recommendations.tsx | 19 ++- .../_lib/server/load-recommendations.ts | 122 +++++++----------- app/home/[account]/page.tsx | 1 - .../features/user-analyses/src/server/api.ts | 25 ++++ packages/supabase/src/database.types.ts | 19 ++- public/locales/en/dashboard.json | 3 + public/locales/et/dashboard.json | 3 + public/locales/ru/dashboard.json | 3 + .../20250920184500_update_ai_responses.sql | 42 ++++++ 10 files changed, 152 insertions(+), 87 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts index 52ea5b8..bbf2346 100644 --- a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts +++ b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts @@ -1,6 +1,6 @@ import type { AnalysisResultDetailsMapped } from '@/packages/features/user-analyses/src/types/analysis-results'; -export type AnalysisTestResponse = Omit< +type AnalysisTestResponse = Omit< AnalysisResultDetailsMapped, 'order' | 'orderedAnalysisElementIds' | 'summary' | 'elements' >; diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index 02fa895..86efd1e 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -4,10 +4,11 @@ import React from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import { analysisResponses } from '../(dashboard)/analysis-results/test/test-responses'; +import { Trans } from '@kit/ui/makerkit/trans'; + import { loadAnalyses } from '../_lib/server/load-analyses'; import { loadRecommendations } from '../_lib/server/load-recommendations'; -import OrderAnalysesCards, { OrderAnalysisCard } from './order-analyses-cards'; +import OrderAnalysesCards from './order-analyses-cards'; export default async function Recommendations({ account, @@ -15,25 +16,21 @@ export default async function Recommendations({ account: AccountWithParams; }) { const { analyses, countryCode } = await loadAnalyses(); - const analysisResults = analysisResponses; - - const analysisRecommendations = await loadRecommendations( - analysisResults, - analyses, - account, - ); + const analysisRecommendations = await loadRecommendations(analyses, account); const orderAnalyses = analyses.filter((analysis) => analysisRecommendations.includes(analysis.title), ); - console.log('analysisRecommendations', analysisRecommendations); + if (orderAnalyses.length < 1) { return null; } return (
-

Medreport soovitab teile

+

+ +

); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index 573ec34..76e1426 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -1,29 +1,26 @@ import { cache } from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; -import { createServerClient } from '@supabase/ssr'; +import { Database } from '@/packages/supabase/src/database.types'; import OpenAI from 'openai'; import PersonalCode from '~/lib/utils'; -import { AnalysisTestResponse } from '../../(dashboard)/analysis-results/test/test-responses'; import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; export const loadRecommendations = cache(recommendationsLoader); -type FormattedAnalysisResponse = { - value: string; - name: string; - responseTime: string; -}; +type AnalysisResponses = + Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']; -const getLatestResponseTime = (items: FormattedAnalysisResponse[]) => { +const getLatestResponseTime = (items: AnalysisResponses) => { if (!items?.length) return null; let latest = null; for (const it of items) { - const d = new Date(it.responseTime); + const d = new Date(it.response_time); const t = d.getTime(); if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) { latest = d; @@ -32,45 +29,10 @@ const getLatestResponseTime = (items: FormattedAnalysisResponse[]) => { return latest; }; -const getLatestUniqueAnalysResponses = ( - analysisResponses: AnalysisTestResponse[], -): { name: string; value: string; responseTime: string }[] => { - const analysisElements = analysisResponses - .map(({ orderedAnalysisElements }) => orderedAnalysisElements) - .flat(); - - console.log('analysisElements', analysisElements.length); - - const byName = analysisElements.reduce< - Record - >((acc, it) => { - const responseTime = it?.results?.responseTime; - const responseValue = it?.results?.responseValue; - if (!responseTime || !responseValue) return acc; - - const key = it.analysisName; - const cur = acc[key]; - const t = Date.parse(responseTime); - const prevT = cur ? Date.parse(cur.responseTime) : -Infinity; - - if (!cur || t > prevT) { - acc[key] = { - name: key, - value: responseValue.toString(), - responseTime, - }; - } - return acc; - }, {}); - - return Object.values(byName); -}; - async function recommendationsLoader( - analysisResponses: AnalysisTestResponse[], analyses: OrderAnalysisCard[], account: AccountWithParams | null, -): Promise { +): Promise { if (!process.env.OPENAI_API_KEY) { return []; } @@ -78,15 +40,14 @@ async function recommendationsLoader( return []; } const supabaseClient = getSupabaseServerClient(); - + const userAnalysesApi = createUserAnalysesApi(supabaseClient); + const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); const analysesRecommendationsPromptId = 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd'; - const latestUniqueAnalysResponses = - getLatestUniqueAnalysResponses(analysisResponses); - const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); + const latestResponseTime = getLatestResponseTime(analysisResponses); const latestISO = latestResponseTime ? new Date(latestResponseTime).toISOString() - : 'none'; + : new Date('2025').toISOString(); const previouslyRecommended = await supabaseClient .schema('medreport') @@ -97,16 +58,28 @@ async function recommendationsLoader( .eq('latest_data_change', latestISO); if (previouslyRecommended.data?.[0]?.response) { - return previouslyRecommended.data[0].response; + return JSON.parse(previouslyRecommended.data[0].response as string) + .recommended; } const openAIClient = new OpenAI(); const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); const weight = account.accountParams?.weight || 'unknown'; - console.log('analysisResponses', analysisResponses); - console.log('analyises', analyses); - const formattedAnalysisResponses = latestUniqueAnalysResponses.map( - ({ name, value }) => ({ name, value }), + + const formattedAnalysisResponses = analysisResponses.map( + ({ + analysis_name_lab, + response_value, + norm_upper, + norm_lower, + norm_status, + }) => ({ + name: analysis_name_lab, + value: response_value, + normUpper: norm_upper, + normLower: norm_lower, + normStatus: norm_status, + }), ); const formattedAnalyses = analyses.map(({ description, title }) => ({ description, @@ -128,23 +101,28 @@ async function recommendationsLoader( }); const json = JSON.parse(response.output_text); - const updateAiResponse = await supabaseClient - .schema('medreport') - .from('ai_responses') - .insert({ - account_id: account.id, - prompt_name: 'Analysis Recommendations', - prompt_id: analysesRecommendationsPromptId, - input: JSON.stringify({ - analyses: formattedAnalyses, - results: formattedAnalysisResponses, - gender, - age, - weight, - }), - latest_data_change: latestISO, - response: response.output_text, - }); + + try { + await supabaseClient + .schema('medreport') + .from('ai_responses') + .insert({ + account_id: account.id, + prompt_name: 'Analysis Recommendations', + prompt_id: analysesRecommendationsPromptId, + input: JSON.stringify({ + analyses: formattedAnalyses, + results: formattedAnalysisResponses, + gender, + age, + weight, + }), + latest_data_change: latestISO, + response: response.output_text, + }); + } catch (error) { + console.error('Error saving AI response: ', error); + } return json.recommended; } diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx index de1c9f9..7283798 100644 --- a/app/home/[account]/page.tsx +++ b/app/home/[account]/page.tsx @@ -4,7 +4,6 @@ import { use } from 'react'; import { CompanyGuard } from '@/packages/features/team-accounts/src/components'; import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api'; -import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { PageBody } from '@kit/ui/page'; diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts index 5693e32..0532886 100644 --- a/packages/features/user-analyses/src/server/api.ts +++ b/packages/features/user-analyses/src/server/api.ts @@ -425,6 +425,31 @@ class UserAnalysesApi { } return data; } + + async getAllUserAnalysisResponses(): Promise< + Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns'] + > { + const { + data: { user }, + } = await this.client.auth.getUser(); + + if (!user) { + return []; + } + + const { data, error } = await this.client + .schema('medreport') + .rpc('get_latest_analysis_response_elements_for_current_user', { + p_user_id: user.id, + }); + + if (error) { + console.error('Error fetching user analysis responses: ', error); + throw error; + } + + return data; + } } export function createUserAnalysesApi(client: SupabaseClient) { diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 5c40f6b..30db3f3 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -764,8 +764,8 @@ export type Database = { norm_upper?: number | null norm_upper_included?: boolean | null original_response_element: Json - response_time: string | null - response_value: number | null + response_time?: string | null + response_value?: number | null response_value_is_negative?: boolean | null response_value_is_within_norm?: boolean | null status?: string | null @@ -1335,6 +1335,7 @@ export type Database = { medipost_private_message_id: string | null medusa_order_id: string | null response_xml: string | null + updated_at: string | null xml: string | null } Insert: { @@ -1347,6 +1348,7 @@ export type Database = { medipost_private_message_id?: string | null medusa_order_id?: string | null response_xml?: string | null + updated_at?: string | null xml?: string | null } Update: { @@ -1359,6 +1361,7 @@ export type Database = { medipost_private_message_id?: string | null medusa_order_id?: string | null response_xml?: string | null + updated_at?: string | null xml?: string | null } Relationships: [] @@ -2030,6 +2033,18 @@ export type Database = { personal_code: string }[] } + get_latest_analysis_response_elements_for_current_user: { + Args: { p_user_id: string } + Returns: { + analysis_name: string + analysis_name_lab: string + norm_lower: number + norm_status: number + norm_upper: number + response_time: string + response_value: number + }[] + } get_latest_medipost_dispatch_state_for_order: { Args: { medusa_order_id: string } Returns: { diff --git a/public/locales/en/dashboard.json b/public/locales/en/dashboard.json index 5598aba..c92f438 100644 --- a/public/locales/en/dashboard.json +++ b/public/locales/en/dashboard.json @@ -18,5 +18,8 @@ "title": "Order analysis", "description": "Select an analysis to get started" } + }, + "recommendations": { + "title": "Medreport recommends" } } diff --git a/public/locales/et/dashboard.json b/public/locales/et/dashboard.json index 916038d..d18c230 100644 --- a/public/locales/et/dashboard.json +++ b/public/locales/et/dashboard.json @@ -18,5 +18,8 @@ "title": "Telli analüüs", "description": "Telli endale sobiv analüüs" } + }, + "recommendations": { + "title": "Medreport soovitab teile" } } diff --git a/public/locales/ru/dashboard.json b/public/locales/ru/dashboard.json index 1e5685b..9c0a7d6 100644 --- a/public/locales/ru/dashboard.json +++ b/public/locales/ru/dashboard.json @@ -18,5 +18,8 @@ "title": "Заказать анализ", "description": "Закажите подходящий для вас анализ" } + }, + "recommendations": { + "title": "Medreport recommends" } } diff --git a/supabase/migrations/20250920184500_update_ai_responses.sql b/supabase/migrations/20250920184500_update_ai_responses.sql index d43d30b..ce0d5b8 100644 --- a/supabase/migrations/20250920184500_update_ai_responses.sql +++ b/supabase/migrations/20250920184500_update_ai_responses.sql @@ -15,3 +15,45 @@ USING prompt_name::text; ALTER TABLE medreport.ai_responses ADD CONSTRAINT ai_responses_id_pkey PRIMARY KEY (id); + +create or replace function medreport.get_latest_analysis_response_elements_for_current_user(p_user_id uuid) +returns table ( + analysis_name medreport.analysis_response_elements.analysis_name%type, + response_time medreport.analysis_response_elements.response_time%type, + norm_upper medreport.analysis_response_elements.norm_upper%type, + norm_lower medreport.analysis_response_elements.norm_lower%type, + norm_status medreport.analysis_response_elements.norm_status%type, + response_value medreport.analysis_response_elements.response_value%type, + analysis_name_lab medreport.analysis_elements.analysis_name_lab%type +) +language sql +as $$ + WITH ranked AS ( + SELECT + are.analysis_name, + are.response_time, + are.norm_upper, + are.norm_lower, + are.norm_status, + are.response_value, + ae.analysis_name_lab, + ROW_NUMBER() OVER ( + PARTITION BY are.analysis_name + ORDER BY are.response_time DESC, are.id DESC + ) AS rn + FROM medreport.analysis_responses ar + JOIN medreport.analysis_response_elements are + ON are.analysis_response_id = ar.id + JOIN medreport.analysis_elements ae + ON are.analysis_element_original_id = ae.analysis_id_original + WHERE ar.user_id = '9ec20b5a-a939-4e5d-9148-6733e36047f3' -- 👈 your user id + AND ar.order_status = 'COMPLETED' + ) + SELECT analysis_name, response_time, norm_upper, norm_lower, norm_status, response_value, analysis_name_lab + FROM ranked + WHERE rn = 1 + ORDER BY analysis_name; +$$; + +grant execute on function medreport.get_latest_analysis_response_elements_for_current_user(uuid) to authenticated, service_role; + From 4afc498cd774a2d75eec189130d43d4d01db86ba Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 23 Sep 2025 14:33:48 +0300 Subject: [PATCH 09/10] feedback fix --- app/home/(user)/_components/recommendations.tsx | 2 +- .../20250920184500_update_ai_responses.sql | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index 86efd1e..01d0b96 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -22,7 +22,7 @@ export default async function Recommendations({ analysisRecommendations.includes(analysis.title), ); - if (orderAnalyses.length < 1) { + if (orderAnalyses.length === 0) { return null; } diff --git a/supabase/migrations/20250920184500_update_ai_responses.sql b/supabase/migrations/20250920184500_update_ai_responses.sql index ce0d5b8..0752227 100644 --- a/supabase/migrations/20250920184500_update_ai_responses.sql +++ b/supabase/migrations/20250920184500_update_ai_responses.sql @@ -1,7 +1,17 @@ ALTER TABLE medreport.ai_responses ENABLE ROW LEVEL SECURITY; -CREATE POLICY "ai_responses_select" ON medreport.ai_responses FOR SELECT TO authenticated USING (true); -CREATE POLICY "ai_responses_insert" ON medreport.ai_responses FOR INSERT TO authenticated WITH CHECK (true); +create policy "ai_responses_select" +on medreport.ai_responses +for select +to authenticated +using (account_id = auth.uid()); + +create policy "ai_responses_insert" +on medreport.ai_responses +for insert +to authenticated +with check (account_id = auth.uid()); + grant select, insert, update, delete on table medreport.ai_responses to authenticated; @@ -56,4 +66,3 @@ as $$ $$; grant execute on function medreport.get_latest_analysis_response_elements_for_current_user(uuid) to authenticated, service_role; - From df850cf1b28ade32de07d77f9b3d0630fc840bf3 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 23 Sep 2025 15:13:48 +0300 Subject: [PATCH 10/10] update skeleton --- app/home/(user)/(dashboard)/page.tsx | 6 +- .../_components/recommendations-skeleton.tsx | 77 +++++++++++++++++++ .../(user)/_components/recommendations.tsx | 9 +-- packages/ui/src/shadcn/skeleton.tsx | 14 +++- 4 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 app/home/(user)/_components/recommendations-skeleton.tsx diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index d697614..9917845 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -16,6 +16,7 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; import Recommendations from '../_components/recommendations'; +import RecommendationsSkeleton from '../_components/recommendations-skeleton'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; export const generateMetadata = async () => { @@ -52,7 +53,10 @@ async function UserHomePage() { /> - }> +

+ +

+ }>
diff --git a/app/home/(user)/_components/recommendations-skeleton.tsx b/app/home/(user)/_components/recommendations-skeleton.tsx new file mode 100644 index 0000000..74cb352 --- /dev/null +++ b/app/home/(user)/_components/recommendations-skeleton.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip'; +import { HeartPulse } from 'lucide-react'; + +import { Button } from '@kit/ui/shadcn/button'; +import { + Card, + CardDescription, + CardFooter, + CardHeader, +} from '@kit/ui/shadcn/card'; +import { Skeleton } from '@kit/ui/skeleton'; + +import OrderAnalysesCards from './order-analyses-cards'; + +const RecommendationsSkeleton = () => { + const emptyData = [ + { + title: '1', + description: '', + subtitle: '', + variant: { id: '' }, + price: 1, + }, + { + title: '2', + description: '', + subtitle: '', + variant: { id: '' }, + price: 1, + }, + ]; + return ( +
+ {emptyData.map(({ title, description, subtitle }) => ( + + + +
+
+
+ + +
+
+ {title} + {description && ( + <> + {' '} + + {description} +
+ } + /> + + )} + + {subtitle && {subtitle}} +
+
+ +
+
+ ))} +
+ ); +}; + +export default RecommendationsSkeleton; diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index 01d0b96..71403ef 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -4,8 +4,6 @@ import React from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import { Trans } from '@kit/ui/makerkit/trans'; - import { loadAnalyses } from '../_lib/server/load-analyses'; import { loadRecommendations } from '../_lib/server/load-recommendations'; import OrderAnalysesCards from './order-analyses-cards'; @@ -27,11 +25,6 @@ export default async function Recommendations({ } return ( -
-

- -

- -
+ ); } diff --git a/packages/ui/src/shadcn/skeleton.tsx b/packages/ui/src/shadcn/skeleton.tsx index 9f09b6c..5b0ac1e 100644 --- a/packages/ui/src/shadcn/skeleton.tsx +++ b/packages/ui/src/shadcn/skeleton.tsx @@ -2,13 +2,23 @@ import { cn } from '../lib/utils'; function Skeleton({ className, + children, ...props }: React.HTMLAttributes) { return (
+ > +
+ {children ?? } +
+ +
+
); }