From d8f314cb00f4ba87ea33bd25c862ac14f52495ff Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Thu, 2 Oct 2025 18:50:16 +0300 Subject: [PATCH 01/36] MED-186: added upsert to balance if increased MED-185: add wallet balance. to new employee --- .env.example | 1 + README.md | 4 +- .../_components/update-account-form.tsx | 120 ++++++------ .../_components/home-menu-navigation.tsx | 16 +- .../20250930175100_update_tto_tables.sql | 4 - .../20251002170200_upsert_account_balance.sql | 172 ++++++++++++++++++ 6 files changed, 244 insertions(+), 73 deletions(-) delete mode 100644 supabase/migrations/20250930175100_update_tto_tables.sql create mode 100644 supabase/migrations/20251002170200_upsert_account_balance.sql diff --git a/.env.example b/.env.example index 091949f..afbc0aa 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,7 @@ EMAIL_PORT= # or 465 for SSL EMAIL_TLS= # or false for SSL (see provider documentation) NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= +MEDUSA_SECRET_API_KEY== NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo diff --git a/README.md b/README.md index 1125158..ecbbedc 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,13 @@ To access admin pages follow these steps: - Register new user - Go to Profile and add Multi-Factor Authentication - Authenticate with mfa (at current time profile page prompts it again) -- update your role. look at `supabase/sql/super-admin.sql` +- update your `account.application_role` to `super_admin`. - Sign out and Sign in ## Company User - With admin account go to `http://localhost:3000/admin/accounts` -- For Create Company Account to work you need to have rows in `medreport.roles` table. For that you can sql in `supabase/sql/super-admin.sql` +- For Create Company Account to work you need to have rows in `medreport.roles` table. ## Start email server diff --git a/app/auth/update-account/_components/update-account-form.tsx b/app/auth/update-account/_components/update-account-form.tsx index 473325a..9477004 100644 --- a/app/auth/update-account/_components/update-account-form.tsx +++ b/app/auth/update-account/_components/update-account-form.tsx @@ -181,80 +181,74 @@ export function UpdateAccountForm({ )} /> - {!isEmailUser && ( - <> + <> + ( + + + + + + + + + + )} + /> + +
( - + - + - + + field.onChange( + e.target.value === '' ? null : Number(e.target.value), + ) + } + /> )} /> -
- ( - - - - - - - field.onChange( - e.target.value === '' - ? null - : Number(e.target.value), - ) - } - /> - - - - )} - /> - - ( - - - - - - - field.onChange( - e.target.value === '' - ? null - : Number(e.target.value), - ) - } - /> - - - - )} - /> -
- - )} + ( + + + + + + + field.onChange( + e.target.value === '' ? null : Number(e.target.value), + ) + } + /> + + + + )} + /> +
+ */}
- {/* TODO: add wallet functionality - € {Number(0).toFixed(2).replace('.', ',')} + + {formatCurrency({ + value: balanceSummary?.totalBalance || 0, + locale: language, + currencyCode: 'EUR', + })} + - */} + {hasCartItems && ( + + + + ); +}; + +export default UpdateEmployeeBenefitDialog; diff --git a/packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts b/packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts new file mode 100644 index 0000000..c4ea5a0 --- /dev/null +++ b/packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts @@ -0,0 +1,6 @@ +import { z } from 'zod'; + +export const UpdateEmployeeBenefitSchema = z.object({ + accountId: z.string().uuid(), + userId: z.string().uuid(), +}); diff --git a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts index 92c0b5f..ef4b2fd 100644 --- a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts @@ -2,6 +2,7 @@ import { revalidatePath } from 'next/cache'; +import { AccountBalanceService } from '@kit/accounts/services/account-balance.service'; import { enhanceAction } from '@kit/next/actions'; import { createOtpApi } from '@kit/otp'; import { getLogger } from '@kit/shared/logger'; @@ -10,6 +11,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { RemoveMemberSchema } from '../../schema/remove-member.schema'; import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema'; +import { UpdateEmployeeBenefitSchema } from '../../schema/update-employee-benefit.schema'; import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema'; import { createAccountMembersService } from '../services/account-members.service'; @@ -144,3 +146,66 @@ export const transferOwnershipAction = enhanceAction( schema: TransferOwnershipConfirmationSchema, }, ); + +export const updateEmployeeBenefitAction = enhanceAction( + async ({ accountId, userId }) => { + const client = getSupabaseServerClient(); + const logger = await getLogger(); + const accountBalanceService = new AccountBalanceService(); + + const ctx = { + name: 'teams.updateEmployeeBenefit', + userId, + accountId, + }; + + const { data, error } = await client + .schema('medreport') + .from('accounts_memberships') + .select('id,is_eligible_for_benefits') + .eq('user_id', userId) + .eq('account_id', accountId) + .eq('type', 'benefit') + .single(); + + logger.info( + ctx, + 'Changing employee benefit to ', + !data?.is_eligible_for_benefits, + ); + + if (error) { + logger.error(ctx, 'Error on receiving balance entry', error); + } + + if (data) { + const { error } = await client + .schema('medreport') + .from('accounts_memberships') + .update({ is_eligible_for_benefits: !data.is_eligible_for_benefits }) + .eq('id', data.id); + + if (error) { + logger.error(ctx, `Error on updating balance entry`, error); + } + + const { data: scheduleData, error: scheduleError } = await client + .schema('medreport') + .from('benefit_distribution_schedule') + .select('id') + .eq('company_id', accountId) + .single(); + + if (scheduleError) { + logger.error(ctx, `Error on getting company benefit schedule`, error); + } + + if (scheduleData?.id) { + await accountBalanceService.upsertHealthBenefitsBySchedule( + scheduleData.id, + ); + } + } + }, + { schema: UpdateEmployeeBenefitSchema }, +); diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 1fc4d6d..4ab0950 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -557,6 +557,7 @@ export type Database = { created_by: string | null has_seen_confirmation: boolean id: string + is_eligible_for_benefits: boolean updated_at: string updated_by: string | null user_id: string @@ -568,6 +569,7 @@ export type Database = { created_by?: string | null has_seen_confirmation?: boolean id?: string + is_eligible_for_benefits?: boolean updated_at?: string updated_by?: string | null user_id: string @@ -579,6 +581,7 @@ export type Database = { created_by?: string | null has_seen_confirmation?: boolean id?: string + is_eligible_for_benefits?: boolean updated_at?: string updated_by?: string | null user_id?: string @@ -2303,6 +2306,7 @@ export type Database = { created_at: string email: string id: string + is_eligible_for_benefits: boolean name: string personal_code: string picture_url: string diff --git a/public/locales/et/teams.json b/public/locales/et/teams.json index 48b0281..5c1b966 100644 --- a/public/locales/et/teams.json +++ b/public/locales/et/teams.json @@ -96,6 +96,13 @@ "updateRoleLoadingMessage": "Rolli uuendatakse...", "updateRoleSuccessMessage": "Roll edukalt uuendatud", "updatingRoleErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti.", + "updateBenefit": "Uuenda toetust", + "updateBenefitHeading": "Uuenda toetus staatust", + "removeBenefitDescription": "Eemalda töötaja toetusest", + "allowBenefitDescription": "Luba töötajale toetus", + "removeBenefitSubmitLabel": "Eemalda toetus", + "allowBenefitSubmitLabel": "Luba toetus", + "updateBenefiErrorMessage": "Vabandus, tekkis viga. Palun proovi uuesti.", "updateMemberRoleModalHeading": "Uuenda töötaja rolli", "updateMemberRoleModalDescription": "Muuda valitud töötaja rolli. Roll määrab töötaja õigused.", "roleMustBeDifferent": "Roll peab erinema praegusest", diff --git a/supabase/migrations/20251002191000_add_new_type.sql b/supabase/migrations/20251002191000_add_new_type.sql index 7467dcd..c111974 100644 --- a/supabase/migrations/20251002191000_add_new_type.sql +++ b/supabase/migrations/20251002191000_add_new_type.sql @@ -1,2 +1,56 @@ insert into medreport.role_permissions (role, permission) values -('owner', 'benefit.manage'); \ No newline at end of file +('owner', 'benefit.manage'); + +ALTER TABLE medreport.accounts_memberships + ADD COLUMN is_eligible_for_benefits boolean NOT NULL DEFAULT true; + +DROP FUNCTION IF EXISTS medreport.get_account_members(text); + +CREATE OR REPLACE FUNCTION medreport.get_account_members(account_slug text) + RETURNS TABLE( + id uuid, + user_id uuid, + account_id uuid, + role character varying, + role_hierarchy_level integer, + primary_owner_user_id uuid, + name text, + email character varying, + personal_code text, + picture_url character varying, + created_at timestamp with time zone, + updated_at timestamp with time zone, + is_eligible_for_benefits boolean +) + LANGUAGE plpgsql + SET search_path TO '' +AS $function$begin + return QUERY + select + acc.id, + am.user_id, + am.account_id, + am.account_role, + r.hierarchy_level, + a.primary_owner_user_id, + TRIM(CONCAT(acc.name, ' ', acc.last_name)) as name, + acc.email, + acc.personal_code, + acc.picture_url, + am.created_at, + am.updated_at, + am.is_eligible_for_benefits + from + medreport.accounts_memberships am + join medreport.accounts a on a.id = am.account_id + join medreport.accounts acc on acc.id = am.user_id + join medreport.roles r on r.name = am.account_role + where + a.slug = account_slug; + +end;$function$ +; + +grant + execute on function medreport.get_account_members (text) to authenticated, + service_role; From e7b7be756244d801a15329c12843c42828c62b3d Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 3 Oct 2025 16:58:53 +0300 Subject: [PATCH 10/36] fix logs --- .../server/actions/team-members-server-actions.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts index ef4b2fd..2fa4fd7 100644 --- a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts @@ -165,17 +165,15 @@ export const updateEmployeeBenefitAction = enhanceAction( .select('id,is_eligible_for_benefits') .eq('user_id', userId) .eq('account_id', accountId) - .eq('type', 'benefit') .single(); logger.info( - ctx, - 'Changing employee benefit to ', - !data?.is_eligible_for_benefits, + { ...ctx, isEligible: !data?.is_eligible_for_benefits, id: data?.id }, + 'Changing employee benefit', ); if (error) { - logger.error(ctx, 'Error on receiving balance entry', error); + logger.error({ error }, 'Error on receiving balance entry'); } if (data) { @@ -186,7 +184,7 @@ export const updateEmployeeBenefitAction = enhanceAction( .eq('id', data.id); if (error) { - logger.error(ctx, `Error on updating balance entry`, error); + logger.error({ error }, `Error on updating balance entry`); } const { data: scheduleData, error: scheduleError } = await client @@ -197,7 +195,7 @@ export const updateEmployeeBenefitAction = enhanceAction( .single(); if (scheduleError) { - logger.error(ctx, `Error on getting company benefit schedule`, error); + logger.error({ error }, 'Error on getting company benefit schedule'); } if (scheduleData?.id) { From 3f17b82fdba02135e5534a8a1c65a09fbabcd8a9 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Sat, 4 Oct 2025 17:36:36 +0300 Subject: [PATCH 11/36] fix mfa login after keycloak --- app/api/after-mfa/route.ts | 28 +++++++++++++++++++ app/auth/callback/route.ts | 5 ++++ app/auth/verify/page.tsx | 7 +---- .../multi-factor-challenge-container.tsx | 9 ++++-- .../supabase/src/auth-callback.service.ts | 23 +++++++++++---- 5 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 app/api/after-mfa/route.ts diff --git a/app/api/after-mfa/route.ts b/app/api/after-mfa/route.ts new file mode 100644 index 0000000..ad80011 --- /dev/null +++ b/app/api/after-mfa/route.ts @@ -0,0 +1,28 @@ +import { enhanceRouteHandler } from '@/packages/next/src/routes'; +import { createAuthCallbackService } from '@/packages/supabase/src/auth-callback.service'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + +export const POST = () => + enhanceRouteHandler( + async () => { + try { + const supabaseClient = getSupabaseServerClient(); + const { + data: { user }, + } = await supabaseClient.auth.getUser(); + const service = createAuthCallbackService(supabaseClient); + + if (user && service.isKeycloakUser(user)) { + await service.setupMedusaUserForKeycloak(user); + } + + return new Response(null, { status: 200 }); + } catch (err) { + console.error('Error on verifying:', { err }); + return new Response(null, { status: 500 }); + } + }, + { + auth: false, + }, + ); diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index a16e023..d36c96a 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -47,6 +47,11 @@ export async function GET(request: NextRequest) { const service = createAuthCallbackService(getSupabaseServerClient()); const oauthResult = await service.exchangeCodeForSession(authCode); + + if (oauthResult.requiresMultiFactorAuthentication) { + redirect(pathsConfig.auth.verifyMfa); + } + if (!('isSuccess' in oauthResult)) { return redirectOnError(oauthResult.searchParams); } diff --git a/app/auth/verify/page.tsx b/app/auth/verify/page.tsx index 677c020..b9a584d 100644 --- a/app/auth/verify/page.tsx +++ b/app/auth/verify/page.tsx @@ -44,12 +44,7 @@ async function VerifyPage(props: Props) { !!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home; return ( - + ); } diff --git a/packages/features/auth/src/components/multi-factor-challenge-container.tsx b/packages/features/auth/src/components/multi-factor-challenge-container.tsx index a9a7fdd..5ff28d4 100644 --- a/packages/features/auth/src/components/multi-factor-challenge-container.tsx +++ b/packages/features/auth/src/components/multi-factor-challenge-container.tsx @@ -46,8 +46,13 @@ export function MultiFactorChallengeContainer({ const router = useRouter(); const verifyMFAChallenge = useVerifyMFAChallenge({ - onSuccess: () => { - router.replace(paths.redirectPath); + onSuccess: async () => { + try { + await fetch('/api/after-mfa', { method: 'POST' }); + router.replace(paths.redirectPath); + } catch (err) { + // ignore + } }, }); diff --git a/packages/supabase/src/auth-callback.service.ts b/packages/supabase/src/auth-callback.service.ts index f27cbc1..7bf1ff4 100644 --- a/packages/supabase/src/auth-callback.service.ts +++ b/packages/supabase/src/auth-callback.service.ts @@ -1,7 +1,5 @@ import 'server-only'; -import getBaseWebpackConfig from 'next/dist/build/webpack-config'; - import { AuthError, type EmailOtpType, @@ -9,6 +7,8 @@ import { User, } from '@supabase/supabase-js'; +import { checkRequiresMultiFactorAuthentication } from './check-requires-mfa'; + /** * @name createAuthCallbackService * @description Creates an instance of the AuthCallbackService @@ -137,10 +137,12 @@ class AuthCallbackService { | { isSuccess: boolean; user: User; + requiresMultiFactorAuthentication: boolean; } | ErrorURLParameters > { let user: User; + let requiresMultiFactorAuthentication: boolean; try { const { data, error } = await this.client.auth.exchangeCodeForSession(authCode); @@ -153,8 +155,14 @@ class AuthCallbackService { }); } - // Handle Keycloak users - set up Medusa integration - if (data?.user && this.isKeycloakUser(data.user)) { + requiresMultiFactorAuthentication = + await checkRequiresMultiFactorAuthentication(this.client); + + if ( + !requiresMultiFactorAuthentication && + data?.user && + this.isKeycloakUser(data.user) + ) { await this.setupMedusaUserForKeycloak(data.user); } @@ -179,20 +187,21 @@ class AuthCallbackService { return { isSuccess: true, user, + requiresMultiFactorAuthentication, }; } /** * Check if user is from Keycloak provider */ - private isKeycloakUser(user: any): boolean { + isKeycloakUser(user: any): boolean { return ( user?.app_metadata?.provider === 'keycloak' || user?.app_metadata?.providers?.includes('keycloak') ); } - private async setupMedusaUserForKeycloak(user: any): Promise { + async setupMedusaUserForKeycloak(user: any): Promise { if (!user.email) { console.warn('Keycloak user has no email, skipping Medusa setup'); return; @@ -285,6 +294,7 @@ interface ErrorURLParameters { error: string; code?: string; searchParams: string; + requiresMultiFactorAuthentication: boolean; } export function getErrorURLParameters({ @@ -313,6 +323,7 @@ export function getErrorURLParameters({ error: errorMessage, code: code ?? '', searchParams: searchParams.toString(), + requiresMultiFactorAuthentication: false, }; } From ec99b6ac96bc03c475b49f16a9cb9ca242983cf8 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Mon, 6 Oct 2025 07:41:30 +0300 Subject: [PATCH 12/36] minor fixes --- app/doctor/_components/analysis-view.tsx | 3 +-- public/locales/et/dashboard.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/doctor/_components/analysis-view.tsx b/app/doctor/_components/analysis-view.tsx index cfb8ecb..6996d11 100644 --- a/app/doctor/_components/analysis-view.tsx +++ b/app/doctor/_components/analysis-view.tsx @@ -69,8 +69,7 @@ export default function AnalysisView({ const isCurrentDoctorJob = !!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id; const isReadOnly = - !isInProgress || - (!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id); + !!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id; const form = useForm({ resolver: zodResolver(doctorAnalysisFeedbackFormSchema), diff --git a/public/locales/et/dashboard.json b/public/locales/et/dashboard.json index 1b518f0..e9433ce 100644 --- a/public/locales/et/dashboard.json +++ b/public/locales/et/dashboard.json @@ -24,7 +24,7 @@ } }, "recommendations": { - "title": "Medreport soovitab teile", + "title": "Medreport soovitab", "validUntil": "Kehtiv kuni {{date}}" } } From 98dcb881ac14f8d657adcdae3fee637e4ba2812e Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Mon, 6 Oct 2025 08:33:18 +0300 Subject: [PATCH 13/36] fix webhook --- app/api/after-mfa/route.ts | 43 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/app/api/after-mfa/route.ts b/app/api/after-mfa/route.ts index ad80011..1bbe946 100644 --- a/app/api/after-mfa/route.ts +++ b/app/api/after-mfa/route.ts @@ -2,27 +2,26 @@ import { enhanceRouteHandler } from '@/packages/next/src/routes'; import { createAuthCallbackService } from '@/packages/supabase/src/auth-callback.service'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; -export const POST = () => - enhanceRouteHandler( - async () => { - try { - const supabaseClient = getSupabaseServerClient(); - const { - data: { user }, - } = await supabaseClient.auth.getUser(); - const service = createAuthCallbackService(supabaseClient); +export const POST = enhanceRouteHandler( + async () => { + try { + const supabaseClient = getSupabaseServerClient(); + const { + data: { user }, + } = await supabaseClient.auth.getUser(); + const service = createAuthCallbackService(supabaseClient); - if (user && service.isKeycloakUser(user)) { - await service.setupMedusaUserForKeycloak(user); - } - - return new Response(null, { status: 200 }); - } catch (err) { - console.error('Error on verifying:', { err }); - return new Response(null, { status: 500 }); + if (user && service.isKeycloakUser(user)) { + await service.setupMedusaUserForKeycloak(user); } - }, - { - auth: false, - }, - ); + + return new Response(null, { status: 200 }); + } catch (err) { + console.error('Error on verifying:', { err }); + return new Response(null, { status: 500 }); + } + }, + { + auth: false, + }, +); From b3bea06d168fa0ecf5e98090d80b52445feb7a31 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Mon, 6 Oct 2025 11:07:39 +0300 Subject: [PATCH 14/36] update tables if not eligible --- app/home/[account]/billing/page.tsx | 7 +- .../members/account-members-table.tsx | 14 +-- public/locales/et/teams.json | 12 +-- .../20251002191000_add_new_type.sql | 94 +++++++++++++++++++ 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/app/home/[account]/billing/page.tsx b/app/home/[account]/billing/page.tsx index e3ca767..9a1aeff 100644 --- a/app/home/[account]/billing/page.tsx +++ b/app/home/[account]/billing/page.tsx @@ -29,10 +29,13 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) { const account = await api.getTeamAccount(accountSlug); const { members } = await api.getMembers(accountSlug); + const eligibleMembersCount = members.filter( + ({ is_eligible_for_benefits }) => !!is_eligible_for_benefits, + ).length; const [expensesOverview, companyParams] = await Promise.all([ loadTeamAccountBenefitExpensesOverview({ companyId: account.id, - employeeCount: members.length, + employeeCount: eligibleMembersCount, }), api.getTeamAccountParams(account.id), ]); @@ -42,7 +45,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) { diff --git a/packages/features/team-accounts/src/components/members/account-members-table.tsx b/packages/features/team-accounts/src/components/members/account-members-table.tsx index 8c851ec..3eddd0a 100644 --- a/packages/features/team-accounts/src/components/members/account-members-table.tsx +++ b/packages/features/team-accounts/src/components/members/account-members-table.tsx @@ -242,7 +242,6 @@ function useGetColumns( @@ -256,13 +255,11 @@ function useGetColumns( function ActionsDropdown({ permissions, member, - currentUserId, currentTeamAccountId, currentRoleHierarchy, }: { permissions: Permissions; member: Members[0]; - currentUserId: string; currentTeamAccountId: string; currentRoleHierarchy: number; }) { @@ -271,13 +268,8 @@ function ActionsDropdown({ const [isUpdatingRole, setIsUpdatingRole] = useState(false); const [isUpdatingBenefit, setIsUpdatingBenefit] = useState(false); - const isCurrentUser = member.user_id === currentUserId; const isPrimaryOwner = member.primary_owner_user_id === member.user_id; - if (isCurrentUser || isPrimaryOwner) { - return null; - } - const memberRoleHierarchy = member.role_hierarchy_level; const canUpdateRole = permissions.canUpdateRole(memberRoleHierarchy); @@ -304,19 +296,19 @@ function ActionsDropdown({ - + setIsUpdatingRole(true)}> - + setIsTransferring(true)}> - + setIsRemoving(true)}> diff --git a/public/locales/et/teams.json b/public/locales/et/teams.json index 5c1b966..2ab7ada 100644 --- a/public/locales/et/teams.json +++ b/public/locales/et/teams.json @@ -96,12 +96,12 @@ "updateRoleLoadingMessage": "Rolli uuendatakse...", "updateRoleSuccessMessage": "Roll edukalt uuendatud", "updatingRoleErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti.", - "updateBenefit": "Uuenda toetust", - "updateBenefitHeading": "Uuenda toetus staatust", - "removeBenefitDescription": "Eemalda töötaja toetusest", - "allowBenefitDescription": "Luba töötajale toetus", - "removeBenefitSubmitLabel": "Eemalda toetus", - "allowBenefitSubmitLabel": "Luba toetus", + "updateBenefit": "Tervisekonto staatus", + "updateBenefitHeading": "Muuda tervisekonto staatust", + "removeBenefitDescription": "Deaktiveeri töötaja tervisekonto", + "allowBenefitDescription": "Aktiveeri töötaja tervisekonto", + "removeBenefitSubmitLabel": "Deaktiveeri", + "allowBenefitSubmitLabel": "Aktiveeri", "updateBenefiErrorMessage": "Vabandus, tekkis viga. Palun proovi uuesti.", "updateMemberRoleModalHeading": "Uuenda töötaja rolli", "updateMemberRoleModalDescription": "Muuda valitud töötaja rolli. Roll määrab töötaja õigused.", diff --git a/supabase/migrations/20251002191000_add_new_type.sql b/supabase/migrations/20251002191000_add_new_type.sql index c111974..95fb34b 100644 --- a/supabase/migrations/20251002191000_add_new_type.sql +++ b/supabase/migrations/20251002191000_add_new_type.sql @@ -54,3 +54,97 @@ end;$function$ grant execute on function medreport.get_account_members (text) to authenticated, service_role; + +create policy "update_accounts_memberships" +on "medreport"."accounts_memberships" +as permissive +for update +to authenticated +using (medreport.is_account_owner(account_id)) +with check (medreport.is_account_owner(account_id)); + +drop policy "restrict_mfa_accounts_memberships" on "medreport"."accounts_memberships"; +grant update on table "medreport"."accounts_memberships" to "authenticated"; + +drop TRIGGER if exists prevent_memberships_update_check on "medreport"."accounts_memberships"; +drop function if exists kit.prevent_memberships_update(); + +create or replace function medreport.upsert_health_benefits( + p_benefit_distribution_schedule_id uuid +) +returns void +language plpgsql +security definer +as $$ +declare + member_record record; + expires_date timestamp with time zone; + v_company_id uuid; + v_benefit_amount numeric; + existing_entry_id uuid; + v_target_amount numeric; +begin + -- Expires on first day of next year. + expires_date := date_trunc('year', now() + interval '1 year'); + + -- Get company_id and benefit_amount from benefit_distribution_schedule + select company_id, benefit_amount into v_company_id, v_benefit_amount + from medreport.benefit_distribution_schedule + where id = p_benefit_distribution_schedule_id; + + -- Get all personal accounts that are members of this company + for member_record in + select distinct + a.id as personal_account_id, + coalesce(am.is_eligible_for_benefits) as is_eligible + from medreport.accounts a + join medreport.accounts_memberships am on a.id = am.user_id + where am.account_id = v_company_id + and a.is_personal_account = true + loop + v_target_amount := case when member_record.is_eligible + then v_benefit_amount + else 0 end; + + -- Check if there is already a balance entry for this personal account from the same company in same month + select id into existing_entry_id + from medreport.account_balance_entries + where entry_type = 'benefit' + and account_id = member_record.personal_account_id + and source_company_id = v_company_id + and date_trunc('month', created_at) = date_trunc('month', now()) + LIMIT 1; + + if existing_entry_id is not null then + update medreport.account_balance_entries set + amount = v_target_amount, + expires_at = expires_date, + benefit_distribution_schedule_id = p_benefit_distribution_schedule_id + where id = existing_entry_id; + else + -- Insert new balance entry for personal account + insert into medreport.account_balance_entries ( + account_id, + amount, + entry_type, + description, + source_company_id, + created_by, + expires_at, + benefit_distribution_schedule_id + ) values ( + member_record.personal_account_id, + v_target_amount, + 'benefit', + 'Health benefit from company', + v_company_id, + auth.uid(), + expires_date, + p_benefit_distribution_schedule_id + ); + end if; + end loop; +end; +$$; + +grant execute on function medreport.upsert_health_benefits(uuid) to authenticated, service_role; \ No newline at end of file From 8958972d78c75c40f39b7dc3f38f877186b9a1bf Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Mon, 6 Oct 2025 15:16:13 +0300 Subject: [PATCH 15/36] fix single analysis result page button --- app/api/job/medipost-retry-dispatch/route.ts | 11 +++++++--- .../order-confirmed-loading-wrapper.tsx | 11 ++++++++-- .../_components/home-menu-navigation.tsx | 22 +++++++++---------- .../(user)/_components/orders/order-block.tsx | 2 ++ .../features/user-analyses/src/server/api.ts | 13 ++++++++--- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/app/api/job/medipost-retry-dispatch/route.ts b/app/api/job/medipost-retry-dispatch/route.ts index 1fe75a0..06a2db6 100644 --- a/app/api/job/medipost-retry-dispatch/route.ts +++ b/app/api/job/medipost-retry-dispatch/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; +import { getLogger } from '@/packages/shared/src/logger'; import { retrieveOrder } from '@lib/data/orders'; import { getMedipostDispatchTries } from '~/lib/services/audit.service'; @@ -10,6 +11,10 @@ import loadEnv from '../handler/load-env'; import validateApiKey from '../handler/validate-api-key'; export const POST = async (request: NextRequest) => { + const logger = await getLogger(); + const ctx = { + api: '/job/medipost-retry-dispatch', + }; loadEnv(); const { medusaOrderId } = await request.json(); @@ -36,15 +41,15 @@ export const POST = async (request: NextRequest) => { medusaOrder, }); await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); - console.info('Successfully sent order to medipost'); + logger.info(ctx, 'Successfully sent order to medipost'); return NextResponse.json( { message: 'Successfully sent order to medipost', }, { status: 200 }, ); - } catch (e) { - console.error('Error sending order to medipost', e); + } catch (error) { + logger.error({ ...ctx, error }, 'Error sending order to medipost'); return NextResponse.json( { message: 'Failed to send order to medipost', diff --git a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx index 7e99300..202b5ae 100644 --- a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx +++ b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx @@ -13,7 +13,7 @@ import { GlobalLoader } from '@kit/ui/makerkit/global-loader'; import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; -import { AnalysisOrder } from '~/lib/types/analysis-order'; +import { AnalysisOrder } from '~/lib/types/order'; function OrderConfirmedLoadingWrapper({ medusaOrder: initialMedusaOrder, @@ -71,7 +71,14 @@ function OrderConfirmedLoadingWrapper({ } />
- + diff --git a/app/home/(user)/_components/home-menu-navigation.tsx b/app/home/(user)/_components/home-menu-navigation.tsx index 8af5961..d7751a3 100644 --- a/app/home/(user)/_components/home-menu-navigation.tsx +++ b/app/home/(user)/_components/home-menu-navigation.tsx @@ -60,24 +60,22 @@ export async function HomeMenuNavigation(props: { - {hasCartItems && ( - - )} diff --git a/app/home/(user)/_components/orders/order-block.tsx b/app/home/(user)/_components/orders/order-block.tsx index b43b3a2..9b07e8f 100644 --- a/app/home/(user)/_components/orders/order-block.tsx +++ b/app/home/(user)/_components/orders/order-block.tsx @@ -81,6 +81,8 @@ export default function OrderBlock({ items={itemsOther} title="orders:table.otherOrders" order={{ + medusaOrderId: analysisOrder?.medusa_order_id, + id: analysisOrder?.id, status: analysisOrder?.status, }} /> diff --git a/packages/features/user-analyses/src/server/api.ts b/packages/features/user-analyses/src/server/api.ts index 93222ce..fa738ec 100644 --- a/packages/features/user-analyses/src/server/api.ts +++ b/packages/features/user-analyses/src/server/api.ts @@ -1,5 +1,6 @@ import { SupabaseClient } from '@supabase/supabase-js'; +import { getLogger } from '@kit/shared/logger'; import type { UuringuVastus } from '@kit/shared/types/medipost-analysis'; import { toArray } from '@kit/shared/utils'; import { Database } from '@kit/supabase/database'; @@ -463,13 +464,19 @@ class UserAnalysesApi { medusaOrderId?: string; orderStatus: AnalysisOrderStatus; }) { + const logger = await getLogger(); const orderIdParam = orderId; const medusaOrderIdParam = medusaOrderId; + const ctx = { + action: 'update-analysis-order-status', + orderId, + medusaOrderId, + orderStatus, + }; - console.info( - `Updating order id=${orderId} medusaOrderId=${medusaOrderId} status=${orderStatus}`, - ); + logger.info(ctx, 'Updating order'); if (!orderIdParam && !medusaOrderIdParam) { + logger.error(ctx, 'Missing orderId or medusaOrderId'); throw new Error('Either orderId or medusaOrderId must be provided'); } await this.client From d2494f3456ed884d527a5c453a1bd44554a90b92 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Mon, 6 Oct 2025 19:44:09 +0300 Subject: [PATCH 16/36] fix doctor detail view --- app/doctor/_components/doctor-dashboard.tsx | 2 +- app/doctor/_components/doctor-sidebar.tsx | 2 +- app/doctor/analysis/[id]/page.tsx | 2 +- app/doctor/completed-jobs/page.tsx | 2 +- app/doctor/my-jobs/page.tsx | 2 +- app/doctor/open-jobs/page.tsx | 2 +- app/doctor/page.tsx | 6 +- .../order-confirmed-loading-wrapper.tsx | 2 +- .../server/actions/doctor-server-actions.ts | 14 +- .../actions/table-data-fetching-actions.ts | 2 +- .../doctor-analysis-detail-view.schema.ts | 12 +- .../server/schema/doctor-analysis.schema.ts | 4 +- .../services/doctor-analysis.service.ts | 147 ++++++++++-------- packages/features/doctor/tsconfig.json | 2 +- .../medusa-storefront/src/lib/data/orders.ts | 17 +- .../src/hooks/use-notifications-stream.ts | 2 +- 16 files changed, 126 insertions(+), 94 deletions(-) diff --git a/app/doctor/_components/doctor-dashboard.tsx b/app/doctor/_components/doctor-dashboard.tsx index 4f265c2..d748b93 100644 --- a/app/doctor/_components/doctor-dashboard.tsx +++ b/app/doctor/_components/doctor-dashboard.tsx @@ -9,7 +9,7 @@ import { import ResultsTableWrapper from './results-table-wrapper'; -export default function Dashboard() { +export default function DoctorDashboard() { return ( <> diff --git a/app/doctor/analysis/[id]/page.tsx b/app/doctor/analysis/[id]/page.tsx index ad7b311..8a68b25 100644 --- a/app/doctor/analysis/[id]/page.tsx +++ b/app/doctor/analysis/[id]/page.tsx @@ -36,7 +36,7 @@ async function AnalysisPage({ return ( <> - + - + - + - + - - + + ); diff --git a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx index 7e99300..b34030a 100644 --- a/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx +++ b/app/home/(user)/(dashboard)/order/[orderId]/confirmed/order-confirmed-loading-wrapper.tsx @@ -13,7 +13,7 @@ import { GlobalLoader } from '@kit/ui/makerkit/global-loader'; import { PageBody, PageHeader } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; -import { AnalysisOrder } from '~/lib/types/analysis-order'; +import { AnalysisOrder } from '~/lib/types/order'; function OrderConfirmedLoadingWrapper({ medusaOrder: initialMedusaOrder, diff --git a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts index 610b70a..0f49315 100644 --- a/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts +++ b/packages/features/doctor/src/lib/server/actions/doctor-server-actions.ts @@ -42,14 +42,14 @@ export const selectJobAction = doctorAction( revalidateDoctorAnalysis(); return { success: true }; - } catch (e) { - logger.error('Failed to select job', e); - if (e instanceof Error) { + } catch (error) { + logger.error({ error }, 'Failed to select job'); + if (error instanceof Error) { revalidateDoctorAnalysis(); return { success: false, reason: - e['message'] === ErrorReason.JOB_ASSIGNED + error['message'] === ErrorReason.JOB_ASSIGNED ? ErrorReason.JOB_ASSIGNED : ErrorReason.UNKNOWN, }; @@ -133,16 +133,16 @@ export const giveFeedbackAction = doctorAction( } return { success: true }; - } catch (e: any) { + } catch (error) { if (isCompleted) { await createNotificationLog({ action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED, status: 'FAIL', - comment: e?.message, + comment: error instanceof Error ? error.message : '', relatedRecordId: analysisOrderId, }); } - logger.error('Failed to give feedback', e); + logger.error({ error }, 'Failed to give feedback'); return { success: false, reason: ErrorReason.UNKNOWN }; } }, diff --git a/packages/features/doctor/src/lib/server/actions/table-data-fetching-actions.ts b/packages/features/doctor/src/lib/server/actions/table-data-fetching-actions.ts index 8802874..8741e9b 100644 --- a/packages/features/doctor/src/lib/server/actions/table-data-fetching-actions.ts +++ b/packages/features/doctor/src/lib/server/actions/table-data-fetching-actions.ts @@ -62,7 +62,7 @@ export const getOpenResponsesAction = doctorAction( const data = await getOpenResponses({ page, pageSize }); return { success: true, data }; } catch (error) { - logger.error(`Error fetching open analysis response jobs`, error); + logger.error({ error }, `Error fetching open analysis response jobs`); return { success: false, error: 'Failed to fetch data from the server.' }; } }, diff --git a/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts b/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts index 439e047..b4c1f16 100644 --- a/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts +++ b/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema.ts @@ -47,7 +47,7 @@ export type Patient = z.infer; export const AnalysisResponsesSchema = z.object({ user_id: z.string(), - analysis_order_id: AnalysisOrderIdSchema, + analysis_order: AnalysisOrderIdSchema, }); export type AnalysisResponses = z.infer; @@ -56,8 +56,8 @@ export const AnalysisResponseSchema = z.object({ analysis_response_id: z.number(), analysis_element_original_id: z.string(), unit: z.string().nullable(), - response_value: z.number(), - response_time: z.string(), + response_value: z.number().nullable(), + response_time: z.string().nullable(), norm_upper: z.number().nullable(), norm_upper_included: z.boolean().nullable(), norm_lower: z.number().nullable(), @@ -74,8 +74,8 @@ export const AnalysisResponseSchema = z.object({ analysis_response_id: z.number(), analysis_element_original_id: z.string(), unit: z.string().nullable(), - response_value: z.number(), - response_time: z.string(), + response_value: z.number().nullable(), + response_time: z.string().nullable(), norm_upper: z.number().nullable(), norm_upper_included: z.boolean().nullable(), norm_lower: z.number().nullable(), @@ -92,7 +92,7 @@ export const AnalysisResponseSchema = z.object({ export type AnalysisResponse = z.infer; export const AnalysisResultDetailsSchema = z.object({ - analysisResponse: z.array(AnalysisResponseSchema), + analysisResponse: z.array(AnalysisResponseSchema).nullable(), order: OrderSchema, doctorFeedback: DoctorFeedbackSchema, patient: PatientSchema, diff --git a/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts b/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts index efda780..ec0de70 100644 --- a/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts +++ b/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema.ts @@ -47,8 +47,8 @@ export const ElementSchema = z.object({ analysis_response_id: z.number(), analysis_element_original_id: z.string(), unit: z.string().nullable(), - response_value: z.number(), - response_time: z.string(), + response_value: z.number().nullable(), + response_time: z.string().nullable(), norm_upper: z.number().nullable(), norm_upper_included: z.boolean().nullable(), norm_lower: z.number().nullable(), diff --git a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts index 7033567..8068a32 100644 --- a/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts +++ b/packages/features/doctor/src/lib/server/services/doctor-analysis.service.ts @@ -1,8 +1,10 @@ import 'server-only'; +import { listOrdersByIds, retrieveOrder } from '@lib/data/orders'; import { isBefore } from 'date-fns'; import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates'; +import { getLogger } from '@kit/shared/logger'; import { getFullName } from '@kit/shared/utils'; import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { createUserAnalysesApi } from '@kit/user-analyses/api'; @@ -31,7 +33,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { const [ { data: doctorFeedbackItems }, - { data: medusaOrderItems }, + medusaOrders, { data: analysisResponseElements }, { data: accounts }, ] = await Promise.all([ @@ -43,11 +45,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { 'analysis_order_id', analysisResponses.map((r) => r.analysis_order_id.id), ), - supabase - .schema('public') - .from('order_item') - .select('order_id, item_id(product_title, product_type)') - .in('order_id', medusaOrderIds), + listOrdersByIds(medusaOrderIds), supabase .schema('medreport') .from('analysis_response_elements') @@ -56,7 +54,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { supabase .schema('medreport') .from('accounts') - .select('name, last_name, id, primary_owner_user_id, preferred_locale') + .select('name,last_name,id,primary_owner_user_id,preferred_locale,slug') .in('primary_owner_user_id', userIds), ]); @@ -69,7 +67,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { ? await supabase .schema('medreport') .from('accounts') - .select('name, last_name, id, primary_owner_user_id, preferred_locale') + .select('name,last_name,id,primary_owner_user_id,preferred_locale,slug') .in('primary_owner_user_id', doctorUserIds) : { data: [] }; @@ -82,21 +80,26 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { ) || []; const firstSampleGivenAt = responseElements.length - ? responseElements.reduce((earliest, current) => - new Date(current.response_time) < new Date(earliest.response_time) - ? current - : earliest, - )?.response_time + ? responseElements.reduce((earliest, current) => { + if (current.response_time && earliest.response_time) { + if ( + new Date(current.response_time) < new Date(earliest.response_time) + ) { + return current; + } + return earliest; + } + return current; + }).response_time : null; - const medusaOrder = medusaOrderItems?.find( - ({ order_id }) => - order_id === analysisResponse.analysis_order_id.medusa_order_id, + const medusaOrder = medusaOrders?.find( + ({ id }) => id === analysisResponse.analysis_order_id.medusa_order_id, ); const patientAccount = allAccounts?.find( - ({ primary_owner_user_id }) => - analysisResponse.user_id === primary_owner_user_id, + ({ primary_owner_user_id, slug }) => + analysisResponse.user_id === primary_owner_user_id && !slug, ); const feedback = doctorFeedbackItems?.find( @@ -110,9 +113,10 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { ); const order = { - title: medusaOrder?.item_id.product_title, + title: medusaOrder?.items?.[0]?.product_title, isPackage: - medusaOrder?.item_id.product_type?.toLowerCase() === 'analysis package', + medusaOrder?.items?.[0]?.product_type?.toLowerCase() === + 'analysis package', analysisOrderId: analysisResponse.analysis_order_id.id, status: analysisResponse.order_status, }; @@ -177,6 +181,7 @@ export async function getUserInProgressResponses({ `, { count: 'exact' }, ) + .neq('status', 'ON_HOLD') .in('analysis_order_id', analysisOrderIds) .range(offset, offset + pageSize - 1) .order('created_at', { ascending: false }); @@ -280,6 +285,7 @@ export async function getOpenResponses({ `, { count: 'exact' }, ) + .neq('order_status', 'ON_HOLD') .order('created_at', { ascending: false }); if (assignedIds.length > 0) { @@ -365,47 +371,50 @@ export async function getOtherResponses({ export async function getAnalysisResultsForDoctor( analysisResponseId: number, ): Promise { + const logger = await getLogger(); + const ctx = { + action: 'get-analysis-results-for-doctor', + analysisResponseId, + }; const supabase = getSupabaseServerClient(); - const { data: analysisResponseElements, error } = await supabase - .schema('medreport') - .from(`analysis_response_elements`) - .select( - `*, - analysis_responses(user_id, analysis_order_id(id,medusa_order_id, analysis_element_ids))`, - ) - .eq('analysis_response_id', analysisResponseId); + const { data: analysisResponsesData, error: analysisResponsesError } = + await supabase + .schema('medreport') + .from(`analysis_response_elements`) + .select( + `*, + analysis_responses(user_id, analysis_order:analysis_order_id(id,medusa_order_id, analysis_element_ids))`, + ) + .eq('analysis_response_id', analysisResponseId); - if (error) { - throw new Error('Something went wrong.'); + if (analysisResponsesError) { + logger.error( + { ...ctx, analysisResponsesError }, + 'No order response for this analysis response id', + ); + throw new Error('No order for this analysis id'); } - - const firstAnalysisResponse = analysisResponseElements?.[0]; + const firstAnalysisResponse = analysisResponsesData?.[0]; const userId = firstAnalysisResponse?.analysis_responses.user_id; const medusaOrderId = - firstAnalysisResponse?.analysis_responses?.analysis_order_id - ?.medusa_order_id; + firstAnalysisResponse?.analysis_responses?.analysis_order?.medusa_order_id; - if (!analysisResponseElements?.length || !userId || !medusaOrderId) { + if (!analysisResponsesData?.length || !userId || !medusaOrderId) { throw new Error('Failed to retrieve full analysis data.'); } - const responseElementAnalysisElementOriginalIds = - analysisResponseElements.map( - ({ analysis_element_original_id }) => analysis_element_original_id, - ); + const responseElementAnalysisElementOriginalIds = analysisResponsesData.map( + ({ analysis_element_original_id }) => analysis_element_original_id, + ); const [ - { data: medusaOrderItems, error: medusaOrderError }, + medusaOrder, { data: accountWithParams, error: accountError }, { data: doctorFeedback, error: feedbackError }, { data: previousAnalyses, error: previousAnalysesError }, ] = await Promise.all([ - supabase - .schema('public') - .from('order_item') - .select(`order_id, item_id(product_title, product_type)`) - .eq('order_id', medusaOrderId), + retrieveOrder(medusaOrderId, true, '*items'), supabase .schema('medreport') .from('accounts') @@ -422,7 +431,7 @@ export async function getAnalysisResultsForDoctor( .select(`*`) .eq( 'analysis_order_id', - firstAnalysisResponse.analysis_responses.analysis_order_id.id, + firstAnalysisResponse.analysis_responses.analysis_order.id, ) .limit(1), supabase @@ -452,12 +461,7 @@ export async function getAnalysisResultsForDoctor( .order('response_time'), ]); - if ( - medusaOrderError || - accountError || - feedbackError || - previousAnalysesError - ) { + if (!medusaOrder || accountError || feedbackError || previousAnalysesError) { throw new Error('Something went wrong.'); } @@ -478,15 +482,20 @@ export async function getAnalysisResultsForDoctor( } = accountWithParams[0]; const analysisResponseElementsWithPreviousData = []; - for (const analysisResponseElement of analysisResponseElements) { + for (const analysisResponseElement of analysisResponsesData) { const latestPreviousAnalysis = previousAnalyses.find( - ({ analysis_element_original_id, response_time }) => - analysis_element_original_id === - analysisResponseElement.analysis_element_original_id && - isBefore( - new Date(response_time), - new Date(analysisResponseElement.response_time), - ), + ({ analysis_element_original_id, response_time }) => { + if (response_time && analysisResponseElement.response_time) { + return ( + analysis_element_original_id === + analysisResponseElement.analysis_element_original_id && + isBefore( + new Date(response_time), + new Date(analysisResponseElement.response_time), + ) + ); + } + }, ); analysisResponseElementsWithPreviousData.push({ ...analysisResponseElement, @@ -497,12 +506,12 @@ export async function getAnalysisResultsForDoctor( return { analysisResponse: analysisResponseElementsWithPreviousData, order: { - title: medusaOrderItems?.[0]?.item_id.product_title ?? '-', + title: medusaOrder.items?.[0]?.product_title ?? '-', isPackage: - medusaOrderItems?.[0]?.item_id.product_type?.toLowerCase() === + medusaOrder.items?.[0]?.product_type?.toLowerCase() === 'analysis package', analysisOrderId: - firstAnalysisResponse.analysis_responses.analysis_order_id.id, + firstAnalysisResponse.analysis_responses.analysis_order.id, }, doctorFeedback: doctorFeedback?.[0], patient: { @@ -525,8 +534,15 @@ export async function selectJob(analysisOrderId: number, userId: string) { const { data: { user }, } = await supabase.auth.getUser(); + const logger = await getLogger(); + const ctx = { + action: 'select-job', + patientUserId: userId, + currentUserId: user?.id, + }; if (!user?.id) { + logger.error(ctx, 'No user logged in'); throw new Error('No user logged in.'); } @@ -541,6 +557,7 @@ export async function selectJob(analysisOrderId: number, userId: string) { const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id; if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) { + logger.error(ctx, 'Job assigned to a different user'); throw new Error(ErrorReason.JOB_ASSIGNED); } @@ -557,6 +574,10 @@ export async function selectJob(analysisOrderId: number, userId: string) { ); if (error || existingFeedbackError) { + logger.error( + { ...ctx, error, existingFeedbackError }, + 'Failed updating doctor feedback', + ); throw new Error('Something went wrong'); } diff --git a/packages/features/doctor/tsconfig.json b/packages/features/doctor/tsconfig.json index d05f5e7..9ad6dfe 100644 --- a/packages/features/doctor/tsconfig.json +++ b/packages/features/doctor/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@kit/tsconfig/base.json", + "extends": "../../../tsconfig.json", "compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, diff --git a/packages/features/medusa-storefront/src/lib/data/orders.ts b/packages/features/medusa-storefront/src/lib/data/orders.ts index 464a747..48b3638 100644 --- a/packages/features/medusa-storefront/src/lib/data/orders.ts +++ b/packages/features/medusa-storefront/src/lib/data/orders.ts @@ -6,7 +6,11 @@ import { HttpTypes } from '@medusajs/types'; import { getAuthHeaders, getCacheOptions } from './cookies'; -export const retrieveOrder = async (id: string, allowCache = true) => { +export const retrieveOrder = async ( + id: string, + allowCache = true, + fields = '*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product', +) => { const headers = { ...(await getAuthHeaders()), }; @@ -19,8 +23,7 @@ export const retrieveOrder = async (id: string, allowCache = true) => { .fetch(`/store/orders/${id}`, { method: 'GET', query: { - fields: - '*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product', + fields, }, headers, next, @@ -62,6 +65,14 @@ export const listOrders = async ( .catch((err) => medusaError(err)); }; +export const listOrdersByIds = async (ids: string[]) => { + try { + return Promise.all(ids.map((id) => retrieveOrder(id))); + } catch (error) { + console.error('response Error', error); + } +}; + export const createTransferRequest = async ( formData: FormData, ): Promise<{ diff --git a/packages/features/notifications/src/hooks/use-notifications-stream.ts b/packages/features/notifications/src/hooks/use-notifications-stream.ts index 6530716..0923d96 100644 --- a/packages/features/notifications/src/hooks/use-notifications-stream.ts +++ b/packages/features/notifications/src/hooks/use-notifications-stream.ts @@ -31,7 +31,7 @@ export function useNotificationsStream(params: { 'postgres_changes', { event: 'INSERT', - schema: 'public', + schema: 'medreport', filter: `account_id=in.(${params.accountIds.join(', ')})`, table: 'notifications', }, From 2f81002e815b738510d6646830d343e450a32d35 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 7 Oct 2025 16:10:55 +0300 Subject: [PATCH 17/36] MED-203: fix doctor feedback form --- app/doctor/_components/analysis-feedback.tsx | 156 ++++++++++++++++++ app/doctor/_components/analysis-view.tsx | 133 +-------------- .../doctor-analysis-detail-view.schema.ts | 2 +- ...155300_allow_doctor_to_update_analysis.sql | 12 ++ 4 files changed, 173 insertions(+), 130 deletions(-) create mode 100644 app/doctor/_components/analysis-feedback.tsx create mode 100644 supabase/migrations/20251007155300_allow_doctor_to_update_analysis.sql diff --git a/app/doctor/_components/analysis-feedback.tsx b/app/doctor/_components/analysis-feedback.tsx new file mode 100644 index 0000000..c1d4c54 --- /dev/null +++ b/app/doctor/_components/analysis-feedback.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react'; + +import { giveFeedbackAction } from '@/packages/features/doctor/src/lib/server/actions/doctor-server-actions'; +import { + DoctorFeedback, + Order, + Patient, +} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema'; +import { + DoctorAnalysisFeedbackForm, + doctorAnalysisFeedbackFormSchema, +} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema'; +import ConfirmationModal from '@/packages/shared/src/components/confirmation-modal'; +import { useUser } from '@/packages/supabase/src/hooks/use-user'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQueryClient } from '@tanstack/react-query'; +import { useForm } from 'react-hook-form'; + +import { Trans } from '@kit/ui/makerkit/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@kit/ui/shadcn/form'; +import { toast } from '@kit/ui/shadcn/sonner'; +import { Textarea } from '@kit/ui/shadcn/textarea'; + +const AnalysisFeedback = ({ + feedback, + patient, + order, +}: { + feedback?: DoctorFeedback; + patient: Patient; + order: Order; +}) => { + const [isDraftSubmitting, setIsDraftSubmitting] = useState(false); + const [isConfirmOpen, setIsConfirmOpen] = useState(false); + const { data: user } = useUser(); + const queryClient = useQueryClient(); + + const form = useForm({ + resolver: zodResolver(doctorAnalysisFeedbackFormSchema), + reValidateMode: 'onChange', + defaultValues: { + feedbackValue: feedback?.value ?? '', + userId: patient.userId, + }, + }); + + const isReadOnly = + !!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id; + + const handleDraftSubmit = async (e: React.FormEvent) => { + setIsDraftSubmitting(true); + e.preventDefault(); + + form.formState.errors.feedbackValue = undefined; + const formData = form.getValues(); + await onSubmit(formData, 'DRAFT'); + setIsDraftSubmitting(false); + }; + const handleCompleteSubmit = form.handleSubmit(async () => { + setIsConfirmOpen(true); + }); + + const onSubmit = async ( + data: DoctorAnalysisFeedbackForm, + status: 'DRAFT' | 'COMPLETED', + ) => { + const result = await giveFeedbackAction({ + ...data, + analysisOrderId: order.analysisOrderId, + status, + }); + + if (!result.success) { + return toast.error(); + } + + queryClient.invalidateQueries({ + predicate: (query) => query.queryKey.includes('doctor-jobs'), + }); + + toast.success(); + + return setIsConfirmOpen(false); + }; + + const confirmComplete = form.handleSubmit(async (data) => { + await onSubmit(data, 'COMPLETED'); + }); + + return ( + <> +

+ +

+

{feedback?.value ?? '-'}

+ {!isReadOnly && ( +
+ + ( + + +