diff --git a/app/home/(user)/(dashboard)/life-style/page.tsx b/app/home/(user)/(dashboard)/life-style/page.tsx new file mode 100644 index 0000000..97862ab --- /dev/null +++ b/app/home/(user)/(dashboard)/life-style/page.tsx @@ -0,0 +1,74 @@ +import React from 'react'; + +import { Circle } from 'lucide-react'; + +import { cn } from '@kit/ui/lib/utils'; +import { PageBody } from '@kit/ui/makerkit/page'; +import { Trans } from '@kit/ui/makerkit/trans'; +import { Skeleton } from '@kit/ui/shadcn/skeleton'; + +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; +import { + PageViewAction, + createPageViewLog, +} from '~/lib/services/audit/pageView.service'; + +import { HomeLayoutPageHeader } from '../../_components/home-page-header'; +import { loadLifeStyle } from '../../_lib/server/load-life-style'; +import { loadCurrentUserAccount } from '../../_lib/server/load-user-account'; + +export async function generateMetadata() { + const { t } = await createI18nServerInstance(); + + return { + title: t('common:lifeStyle.title'), + }; +} + +async function LifeStylePage() { + const { account } = await loadCurrentUserAccount(); + if (!account) { + return null; + } + const data = await loadLifeStyle(account); + + await createPageViewLog({ + accountId: account.id, + action: PageViewAction.VIEW_LIFE_STYLE, + }); + + if (!data.lifestyle) { + return ; + } + + return ( + <> + } + description="" + /> + + +
+ {data.lifestyle.map(({ title, description, score }, index) => ( + +
+

{title}

+ +
+

{description}

+
+ ))} +
+
+ + ); +} + +export default withI18n(LifeStylePage); diff --git a/app/home/(user)/(dashboard)/order-analysis/page.tsx b/app/home/(user)/(dashboard)/order-analysis/page.tsx index edd0ad7..5e2bd2f 100644 --- a/app/home/(user)/(dashboard)/order-analysis/page.tsx +++ b/app/home/(user)/(dashboard)/order-analysis/page.tsx @@ -37,8 +37,8 @@ async function OrderAnalysisPage() { return ( <> } - description={} + title={} + description={} /> diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index cdb7bcf..b754b22 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -1,5 +1,3 @@ -import { Suspense } from 'react'; - import { redirect } from 'next/navigation'; import { toTitleCase } from '@/lib/utils'; @@ -12,11 +10,9 @@ import { createUserAnalysesApi } from '@kit/user-analyses/api'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import AIBlocks from '../_components/ai/ai-blocks'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; -import Recommendations from '../_components/recommendations'; -import RecommendationsSkeleton from '../_components/recommendations-skeleton'; -import { isValidOpenAiEnv } from '../_lib/server/is-valid-open-ai-env'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; export const generateMetadata = async () => { @@ -33,7 +29,10 @@ async function UserHomePage() { const { account } = await loadCurrentUserAccount(); const api = createUserAnalysesApi(client); + const userAnalysesApi = createUserAnalysesApi(client); + const bmiThresholds = await api.fetchBmiThresholds(); + const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); if (!account) { redirect('/'); @@ -53,16 +52,13 @@ async function UserHomePage() { /> - {(await isValidOpenAiEnv()) && ( - <> -

- -

- }> - - - - )} + +

+ +

+
+ +
); diff --git a/app/home/(user)/_components/ai/ai-blocks.tsx b/app/home/(user)/_components/ai/ai-blocks.tsx new file mode 100644 index 0000000..a115f53 --- /dev/null +++ b/app/home/(user)/_components/ai/ai-blocks.tsx @@ -0,0 +1,49 @@ +'use server'; + +import React, { Suspense } from 'react'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; + +import { isValidOpenAiEnv } from '../../_lib/server/is-valid-open-ai-env'; +import LifeStyleCard from './life-style-card'; +import OrderAnalysesPackageCard from './order-analyses-package-card'; +import Recommendations from './recommendations'; +import RecommendationsSkeleton from './recommendations-skeleton'; +import { AnalysisResponses } from './types'; + +const AIBlocks = async ({ + account, + analysisResponses, +}: { + account: AccountWithParams; + analysisResponses?: AnalysisResponses; +}) => { + const isOpenAiAvailable = await isValidOpenAiEnv(); + + if (!isOpenAiAvailable) { + return ; + } + + if (analysisResponses?.length === 0) { + return ( + <> + + }> + + + + ); + } + + return ( + }> + + + + ); +}; + +export default AIBlocks; diff --git a/app/home/(user)/_components/ai/life-style-card.tsx b/app/home/(user)/_components/ai/life-style-card.tsx new file mode 100644 index 0000000..5ca0aed --- /dev/null +++ b/app/home/(user)/_components/ai/life-style-card.tsx @@ -0,0 +1,44 @@ +'use server'; + +import React from 'react'; + +import Link from 'next/link'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import { pathsConfig } from '@/packages/shared/src/config'; +import { ChevronRight } from 'lucide-react'; + +import { Trans } from '@kit/ui/makerkit/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import { Card, CardHeader } from '@kit/ui/shadcn/card'; + +import { loadLifeStyle } from '../../_lib/server/load-life-style'; +import { AnalysisResponses } from './types'; + +const LifeStyleCard = async ({ + account, + analysisResponses, +}: { + account: AccountWithParams; + analysisResponses?: AnalysisResponses; +}) => { + const data = await loadLifeStyle(account, analysisResponses); + + return ( + + +
+ +
+ + + +
+ {data.summary} +
+ ); +}; + +export default LifeStyleCard; diff --git a/app/home/(user)/_components/ai/order-analyses-package-card.tsx b/app/home/(user)/_components/ai/order-analyses-package-card.tsx new file mode 100644 index 0000000..a27339e --- /dev/null +++ b/app/home/(user)/_components/ai/order-analyses-package-card.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import Link from 'next/link'; + +import { pathsConfig } from '@/packages/shared/src/config'; +import { ChevronRight, HeartPulse } from 'lucide-react'; + +import { Trans } from '@kit/ui/makerkit/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import { + Card, + CardDescription, + CardFooter, + CardHeader, +} from '@kit/ui/shadcn/card'; + +const OrderAnalysesPackageCard = () => { + return ( + + +
+ +
+
+ + + +
+
+ +
+ +
+ + + +
+
+ ); +}; + +export default OrderAnalysesPackageCard; diff --git a/app/home/(user)/_components/ai/recommendations-skeleton.tsx b/app/home/(user)/_components/ai/recommendations-skeleton.tsx new file mode 100644 index 0000000..2964784 --- /dev/null +++ b/app/home/(user)/_components/ai/recommendations-skeleton.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip'; + +import { Button } from '@kit/ui/shadcn/button'; +import { + Card, + CardDescription, + CardFooter, + CardHeader, +} from '@kit/ui/shadcn/card'; +import { Skeleton } from '@kit/ui/skeleton'; + +const RecommendationsSkeleton = ({ amount = 2 }: { amount?: number }) => { + const emptyData = [ + { + title: '1', + description: '', + subtitle: '', + variant: { id: '' }, + price: 1, + }, + ]; + return Array.from({ length: amount }, (_, index) => { + const { title, description, subtitle } = emptyData[0]!; + + return ( + + + +
+
+
+ + +
+
+ {title} + {description && ( + <> + {' '} + + {description} +
+ } + /> + + )} + + {subtitle && {subtitle}} +
+
+ +
+
+ ); + }); +}; + +export default RecommendationsSkeleton; diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/ai/recommendations.tsx similarity index 67% rename from app/home/(user)/_components/recommendations.tsx rename to app/home/(user)/_components/ai/recommendations.tsx index 71403ef..10b6ef8 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/ai/recommendations.tsx @@ -4,9 +4,9 @@ import React from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import { loadAnalyses } from '../_lib/server/load-analyses'; -import { loadRecommendations } from '../_lib/server/load-recommendations'; -import OrderAnalysesCards from './order-analyses-cards'; +import { loadAnalyses } from '../../_lib/server/load-analyses'; +import { loadRecommendations } from '../../_lib/server/load-recommendations'; +import OrderAnalysesCards from '../order-analyses-cards'; export default async function Recommendations({ account, @@ -15,7 +15,7 @@ export default async function Recommendations({ }) { const { analyses, countryCode } = await loadAnalyses(); - const analysisRecommendations = await loadRecommendations(analyses, account); + const analysisRecommendations = await loadRecommendations(account); const orderAnalyses = analyses.filter((analysis) => analysisRecommendations.includes(analysis.title), ); diff --git a/app/home/(user)/_components/ai/types.ts b/app/home/(user)/_components/ai/types.ts new file mode 100644 index 0000000..2026cce --- /dev/null +++ b/app/home/(user)/_components/ai/types.ts @@ -0,0 +1,18 @@ +import { Database } from '@/packages/supabase/src/database.types'; + +export interface ILifeStyleResponse { + lifestyle: { + title: string; + description: string; + score: 0 | 1 | 2; + }[]; + summary: string | null; +} + +export enum PROMPT_NAME { + LIFE_STYLE = 'Life Style', + ANALYSIS_RECOMMENDATIONS = 'Analysis Recommendations', +} + +export type AnalysisResponses = + Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']; diff --git a/app/home/(user)/_components/order-analyses-cards.tsx b/app/home/(user)/_components/order-analyses-cards.tsx index 88574fa..b6f73e3 100644 --- a/app/home/(user)/_components/order-analyses-cards.tsx +++ b/app/home/(user)/_components/order-analyses-cards.tsx @@ -57,72 +57,64 @@ export default function OrderAnalysesCards({ } }; - return ( -
- {analyses.map(({ title, variant, description, subtitle, price }) => { - const formattedPrice = - typeof price === 'number' - ? formatCurrency({ - currencyCode: 'eur', - locale: language, - value: price, - }) - : null; - return ( - - -
- -
-
- -
-
- -
-
- {title} - {description && ( - <> - - {formattedPrice} - {description} -
- } - /> - - )} - - {subtitle && {subtitle}} -
-
- {formattedPrice} -
- - - ); - })} - - ); + return analyses.map(({ title, variant, description, subtitle, price }) => { + const formattedPrice = + typeof price === 'number' + ? formatCurrency({ + currencyCode: 'eur', + locale: language, + value: price, + }) + : null; + return ( + + +
+ +
+
+ +
+
+ +
+
+ {title} + {description && ( + <> + + {formattedPrice} + {description} +
+ } + /> + + )} + + {subtitle && {subtitle}} + +
+ {formattedPrice} +
+
+
+ ); + }); } diff --git a/app/home/(user)/_components/recommendations-skeleton.tsx b/app/home/(user)/_components/recommendations-skeleton.tsx deleted file mode 100644 index fad1225..0000000 --- a/app/home/(user)/_components/recommendations-skeleton.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; - -import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip'; - -import { Button } from '@kit/ui/shadcn/button'; -import { - Card, - CardDescription, - CardFooter, - CardHeader, -} from '@kit/ui/shadcn/card'; -import { Skeleton } from '@kit/ui/skeleton'; - -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)/_lib/server/ai-actions.ts b/app/home/(user)/_lib/server/ai-actions.ts new file mode 100644 index 0000000..d014619 --- /dev/null +++ b/app/home/(user)/_lib/server/ai-actions.ts @@ -0,0 +1,212 @@ +'use server'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; +import OpenAI from 'openai'; + +import PersonalCode from '~/lib/utils'; + +import { + AnalysisResponses, + ILifeStyleResponse, + PROMPT_NAME, +} from '../../_components/ai/types'; +import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; + +async function getLatestResponseTime(items?: AnalysisResponses) { + if (!items?.length) return null; + + let latest = null; + for (const it of items) { + const d = new Date(it.response_time); + const t = d.getTime(); + if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) { + latest = d; + } + } + return latest; +} + +export async function updateLifeStyle({ + account, + analysisResponses, +}: { + account: AccountWithParams; + analysisResponses?: AnalysisResponses; +}): Promise { + const LIFE_STYLE_PROMPT_ID = process.env.PROMPT_ID_LIFE_STYLE; + if (!LIFE_STYLE_PROMPT_ID || !account?.personal_code) { + return { + lifestyle: [], + summary: null, + }; + } + + const openAIClient = new OpenAI(); + const supabaseClient = getSupabaseServerClient(); + const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); + const weight = account.accountParams?.weight || 'unknown'; + const height = account.accountParams?.height || 'unknown'; + const isSmoker = !!account.accountParams?.isSmoker; + const cholesterol = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'Kolesterool') + ?.response_value.toString() || 'unknown'; + const ldl = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'LDL kolesterool') + ?.response_value.toString() || 'unknown'; + const hdl = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'HDL kolesterool') + ?.response_value.toString() || 'unknown'; + const vitamind = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'Vitamiin D (25-OH)') + ?.response_value.toString() || 'unknown'; + + const latestResponseTime = await getLatestResponseTime(analysisResponses); + const latestISO = latestResponseTime + ? new Date(latestResponseTime).toISOString() + : new Date('2025').toISOString(); + + try { + const response = await openAIClient.responses.create({ + store: false, + prompt: { + id: LIFE_STYLE_PROMPT_ID, + variables: { + gender: gender.value, + age: age.toString(), + weight: weight.toString(), + height: height.toString(), + cholesterol, + ldl, + hdl, + vitamind, + is_smoker: isSmoker.toString(), + }, + }, + }); + + await supabaseClient + .schema('medreport') + .from('ai_responses') + .insert({ + account_id: account.id, + prompt_name: PROMPT_NAME.LIFE_STYLE, + prompt_id: LIFE_STYLE_PROMPT_ID, + input: JSON.stringify({ + gender: gender.value, + age: age.toString(), + weight: weight.toString(), + cholesterol, + ldl, + hdl, + vitamind, + is_smoker: isSmoker.toString(), + }), + latest_data_change: latestISO, + response: response.output_text, + }); + + const json = JSON.parse(response.output_text); + + return json; + } catch (error) { + console.error('Error calling OpenAI: ', error); + return { + lifestyle: [], + summary: null, + }; + } +} + +export async function updateRecommendations({ + analyses, + analysisResponses, + account, +}: { + analyses: OrderAnalysisCard[]; + analysisResponses?: AnalysisResponses; + account: AccountWithParams; +}) { + const RECOMMENDATIONS_PROMPT_IT = + process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS; + + if (!RECOMMENDATIONS_PROMPT_IT || !account?.personal_code) { + console.error('No prompt ID for analysis recommendations'); + return []; + } + + const openAIClient = new OpenAI(); + const supabaseClient = getSupabaseServerClient(); + + const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); + const weight = account.accountParams?.weight || 'unknown'; + + 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, + title, + })); + const latestResponseTime = await getLatestResponseTime(analysisResponses); + const latestISO = latestResponseTime + ? new Date(latestResponseTime).toISOString() + : new Date('2025').toISOString(); + + try { + const response = await openAIClient.responses.create({ + store: false, + prompt: { + id: RECOMMENDATIONS_PROMPT_IT, + variables: { + analyses: JSON.stringify(formattedAnalyses), + results: JSON.stringify(formattedAnalysisResponses), + gender: gender.value, + age: age.toString(), + weight: weight.toString(), + }, + }, + }); + + await supabaseClient + .schema('medreport') + .from('ai_responses') + .insert({ + account_id: account.id, + prompt_name: PROMPT_NAME.ANALYSIS_RECOMMENDATIONS, + prompt_id: RECOMMENDATIONS_PROMPT_IT, + input: JSON.stringify({ + analyses: formattedAnalyses, + results: formattedAnalysisResponses, + gender, + age, + weight, + }), + latest_data_change: latestISO, + response: response.output_text, + }); + + const json = JSON.parse(response.output_text); + + return json.recommended; + } catch (error) { + console.error('Error getting recommendations: ', error); + return []; + } +} diff --git a/app/home/(user)/_lib/server/load-life-style.ts b/app/home/(user)/_lib/server/load-life-style.ts new file mode 100644 index 0000000..95dd06f --- /dev/null +++ b/app/home/(user)/_lib/server/load-life-style.ts @@ -0,0 +1,48 @@ +import { cache } from 'react'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + +import { + AnalysisResponses, + ILifeStyleResponse, + PROMPT_NAME, +} from '../../_components/ai/types'; +import { updateLifeStyle } from './ai-actions'; + +const failedResponse = { + lifestyle: [], + summary: null, +}; + +async function lifeStyleLoader( + account: AccountWithParams, + analysisResponses?: AnalysisResponses, +): Promise { + if (!account?.personal_code) { + return failedResponse; + } + + const supabaseClient = getSupabaseServerClient(); + const { data, error } = await supabaseClient + .schema('medreport') + .from('ai_responses') + .select('response') + .eq('account_id', account.id) + .eq('prompt_name', PROMPT_NAME.LIFE_STYLE) + .order('latest_data_change', { ascending: false, nullsFirst: false }) + .limit(1) + .maybeSingle(); + + if (error) { + console.error('Error fetching AI response from DB: ', error); + return failedResponse; + } + + if (data?.response) { + return JSON.parse(data.response as string); + } else { + return await updateLifeStyle({ account, analysisResponses }); + } +} +export const loadLifeStyle = cache(lifeStyleLoader); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index 8763c87..a1ae213 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -1,140 +1,38 @@ 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 { Database } from '@/packages/supabase/src/database.types'; -import OpenAI from 'openai'; -import PersonalCode from '~/lib/utils'; - -import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; +import { PROMPT_NAME } from '../../_components/ai/types'; export const loadRecommendations = cache(recommendationsLoader); -type AnalysisResponses = - Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']; - -const getLatestResponseTime = (items: AnalysisResponses) => { - if (!items?.length) return null; - - let latest = null; - for (const it of items) { - const d = new Date(it.response_time); - const t = d.getTime(); - if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) { - latest = d; - } - } - return latest; -}; - async function recommendationsLoader( - analyses: OrderAnalysisCard[], account: AccountWithParams | null, ): Promise { - if (!process.env.OPENAI_API_KEY) { - return []; - } if (!account?.personal_code) { return []; } const supabaseClient = getSupabaseServerClient(); - const userAnalysesApi = createUserAnalysesApi(supabaseClient); - const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); - const analysesRecommendationsPromptId = - process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS; - const latestResponseTime = getLatestResponseTime(analysisResponses); - const latestISO = latestResponseTime - ? new Date(latestResponseTime).toISOString() - : new Date('2025').toISOString(); - if (!analysesRecommendationsPromptId) { - console.error('No prompt ID for analysis recommendations'); - return []; - } - - const previouslyRecommended = await supabaseClient + const { data, error } = await supabaseClient .schema('medreport') .from('ai_responses') .select('*') .eq('account_id', account.id) - .eq('prompt_id', analysesRecommendationsPromptId) - .eq('latest_data_change', latestISO); + .eq('prompt_name', PROMPT_NAME.ANALYSIS_RECOMMENDATIONS) + .order('latest_data_change', { ascending: false, nullsFirst: false }) + .limit(1) + .maybeSingle(); - if (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'; - - 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, - title, - })); - - let response; - - try { - response = await openAIClient.responses.create({ - store: false, - prompt: { - id: analysesRecommendationsPromptId, - variables: { - analyses: JSON.stringify(formattedAnalyses), - results: JSON.stringify(formattedAnalysisResponses), - gender: gender.value, - age: age.toString(), - weight: weight.toString(), - }, - }, - }); - } catch (error) { - console.error('Error calling OpenAI: ', error); + if (error) { + console.error('Error fetching AI response from DB: ', error); return []; } - const json = JSON.parse(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); + if (data?.response) { + return JSON.parse(data.response as string).recommended; + } else { + return []; } - - return json.recommended; } diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts index c107cf1..2f45566 100644 --- a/lib/services/audit/pageView.service.ts +++ b/lib/services/audit/pageView.service.ts @@ -7,6 +7,7 @@ export enum PageViewAction { VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS', VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD', VIEW_TTO_SERVICE_BOOKING = 'VIEW_TTO_SERVICE_BOOKING', + VIEW_LIFE_STYLE = 'VIEW_LIFE_STYLE', } export const createPageViewLog = async ({ diff --git a/packages/shared/src/config/paths.config.ts b/packages/shared/src/config/paths.config.ts index b3f4965..383fdf3 100644 --- a/packages/shared/src/config/paths.config.ts +++ b/packages/shared/src/config/paths.config.ts @@ -40,6 +40,7 @@ const PathsSchema = z.object({ completedJobs: z.string().min(1), openJobs: z.string().min(1), analysisDetails: z.string().min(1), + lifeStyle: z.string().min(1), }), }); @@ -83,6 +84,7 @@ const pathsConfig = PathsSchema.parse({ completedJobs: '/doctor/completed-jobs', openJobs: '/doctor/open-jobs', analysisDetails: 'doctor/analysis', + lifeStyle: '/home/life-style', }, } satisfies z.infer); diff --git a/packages/shared/src/config/personal-account-navigation.config.tsx b/packages/shared/src/config/personal-account-navigation.config.tsx index d2763d7..c4bde8f 100644 --- a/packages/shared/src/config/personal-account-navigation.config.tsx +++ b/packages/shared/src/config/personal-account-navigation.config.tsx @@ -1,4 +1,5 @@ import { + Bike, FileLineChart, HeartPulse, LineChart, @@ -53,6 +54,12 @@ const routes = [ Icon: , end: true, }, + { + label: 'common:routes.lifeStyle', + path: pathsConfig.app.lifeStyle, + Icon: , + end: true, + }, ], }, ] satisfies z.infer['routes']; diff --git a/public/locales/et/common.json b/public/locales/et/common.json index 8615a16..1c9f38a 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -84,7 +84,8 @@ "preferences": "Eelistused", "security": "Turvalisus", "admin": "Admin", - "accounts": "Kontod" + "accounts": "Kontod", + "lifeStyle": "Elustiil" }, "roles": { "owner": { @@ -150,5 +151,8 @@ "no": "Ei", "preferNotToAnswer": "Eelistan mitte vastata", "book": "Broneeri", - "change": "Muuda" + "change": "Muuda", + "lifeStyle": { + "title": "Elustiili soovitused" + } } diff --git a/public/locales/et/dashboard.json b/public/locales/et/dashboard.json index 8fce772..acf7f42 100644 --- a/public/locales/et/dashboard.json +++ b/public/locales/et/dashboard.json @@ -21,6 +21,13 @@ "benefits": { "title": "Sinu Medreport konto seis", "validUntil": "Kehtiv kuni {{date}}" + }, + "orderPackage": { + "title": "Telli analüüside pakett", + "description": "Võrdle erinevate pakettide vahel ja vali endale sobiv" + }, + "lifeStyle": { + "title": "Elustiili soovitused" } }, "recommendations": { diff --git a/supabase/migrations/20251023163400_fix_analysis_response_function.sql b/supabase/migrations/20251023163400_fix_analysis_response_function.sql new file mode 100644 index 0000000..d614c3b --- /dev/null +++ b/supabase/migrations/20251023163400_fix_analysis_response_function.sql @@ -0,0 +1,42 @@ +drop function if exists medreport.get_latest_analysis_response_elements_for_current_user(uuid); + +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 = auth.uid() + AND ar.order_status IN ('COMPLETED', 'ON_HOLD') + ) + 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;