Merge branch 'main' into MED-105-v3

This commit is contained in:
2025-08-14 01:19:48 +03:00
36 changed files with 615 additions and 37 deletions

1
.env
View File

@@ -44,6 +44,7 @@ NEXT_PUBLIC_REALTIME_NOTIFICATIONS=true
WEBHOOK_SENDER_PROVIDER=postgres
# MAILER DEV
CONTACT_EMAIL=info@medreport.ee
MAILER_PROVIDER=nodemailer
EMAIL_SENDER=info@medreport.ee
EMAIL_USER= # refer to your email provider's documentation

View File

@@ -24,6 +24,9 @@ const ModeToggle = dynamic(() =>
const paths = {
home: pathsConfig.app.home,
admin: pathsConfig.app.admin,
doctor: pathsConfig.app.doctor,
personalAccountSettings: pathsConfig.app.personalAccountSettings,
};
const features = {

View File

@@ -0,0 +1,63 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { LayoutDashboard } from 'lucide-react';
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
useSidebar,
} from '@kit/ui/shadcn-sidebar';
import { Trans } from '@kit/ui/trans';
import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
export function DoctorSidebar({
accounts,
}: {
accounts: UserWorkspace['accounts'];
}) {
const path = usePathname();
const { open } = useSidebar();
return (
<Sidebar collapsible="icon">
<SidebarHeader className={'m-2'}>
<AppLogo href={'/doctor'} className="max-w-full" compact={!open} />
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>
<Trans i18nKey="common:doctor" />
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuButton isActive={path === '/doctor'} asChild>
<Link className={'flex gap-2.5'} href={'/doctor'}>
<LayoutDashboard className={'h-4'} />
<span>Dashboard</span>
</Link>
</SidebarMenuButton>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<ProfileAccountDropdownContainer accounts={accounts} />
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -0,0 +1,28 @@
import Link from 'next/link';
import { Menu } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import pathsConfig from '../../../config/paths.config';
export function DoctorMobileNavigation() {
return (
<DropdownMenu>
<DropdownMenuTrigger>
<Menu className={'h-8 w-8'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link href={pathsConfig.app.doctor}>Home</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

47
app/doctor/layout.tsx Normal file
View File

@@ -0,0 +1,47 @@
import { use } from 'react';
import { cookies } from 'next/headers';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
import { DoctorSidebar } from './_components/doctor-sidebar';
import { DoctorMobileNavigation } from './_components/mobile-navigation';
export const metadata = {
title: `Doctor`,
};
export const dynamic = 'force-dynamic';
export default function DoctorLayout(props: React.PropsWithChildren) {
const state = use(getLayoutState());
const workspace = use(loadUserWorkspace());
return (
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<DoctorSidebar accounts={workspace.accounts} />
</PageNavigation>
<PageMobileNavigation>
<DoctorMobileNavigation />
</PageMobileNavigation>
{props.children}
</Page>
</SidebarProvider>
);
}
async function getLayoutState() {
const cookieStore = await cookies();
const sidebarOpenCookie = cookieStore.get('sidebar:state');
return {
open: sidebarOpenCookie?.value !== 'true',
};
}

3
app/doctor/loading.tsx Normal file
View File

@@ -0,0 +1,3 @@
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;

17
app/doctor/page.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { DoctorGuard } from '@kit/doctor/components/doctor-guard';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
function DoctorPage() {
return (
<>
<PageHeader description={<Trans i18nKey="common:doctor" />} />
<PageBody>
<div>TBD</div>
</PageBody>
</>
);
}
export default DoctorGuard(DoctorPage);

View File

@@ -11,6 +11,9 @@ import pathsConfig from '~/config/paths.config';
const paths = {
home: pathsConfig.app.home,
admin: pathsConfig.app.admin,
doctor: pathsConfig.app.doctor,
personalAccountSettings: pathsConfig.app.personalAccountSettings
};
const features = {
@@ -30,7 +33,7 @@ export function ProfileAccountDropdownContainer(props: {
label: string | null;
value: string | null;
image?: string | null;
}[]
}[];
}) {
const signOut = useSignOut();
const user = useUser(props.user);

View File

@@ -30,6 +30,8 @@ const PathsSchema = z.object({
accountMembers: z.string().min(1),
accountBillingReturn: z.string().min(1),
joinTeam: z.string().min(1),
doctor: z.string().min(1),
admin: z.string().min(1),
}),
});
@@ -63,6 +65,8 @@ const pathsConfig = PathsSchema.parse({
analysisResults: '/home/analysis-results',
orderAnalysis: '/home/order-analysis',
orderHealthAnalysis: '/home/order-health-analysis',
doctor: '/doctor',
admin: '/admin',
},
} satisfies z.infer<typeof PathsSchema>);

View File

@@ -11,7 +11,7 @@ export const sendCompanyOfferEmail = async (
language: string,
) => {
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
const { html, subject, to } = await renderCompanyOfferEmail({
const { html, subject } = await renderCompanyOfferEmail({
language,
companyData: data,
});
@@ -19,7 +19,7 @@ export const sendCompanyOfferEmail = async (
await sendEmail({
subject,
html,
to,
to: process.env.CONTACT_EMAIL || '',
});
};

View File

@@ -9,6 +9,7 @@ const INTERNAL_PACKAGES = [
'@kit/auth',
'@kit/accounts',
'@kit/admin',
'@kit/doctor',
'@kit/team-accounts',
'@kit/shared',
'@kit/supabase',

View File

@@ -33,6 +33,7 @@
"@hookform/resolvers": "^5.1.1",
"@kit/accounts": "workspace:*",
"@kit/admin": "workspace:*",
"@kit/doctor": "workspace:*",
"@kit/analytics": "workspace:*",
"@kit/auth": "workspace:*",
"@kit/billing": "workspace:*",

View File

@@ -6,7 +6,14 @@ import Link from 'next/link';
import type { User } from '@supabase/supabase-js';
import { ChevronsUpDown, Home, LogOut, Shield, UserCircle } from 'lucide-react';
import {
ChevronsUpDown,
Cross,
Home,
LogOut,
Shield,
UserCircle,
} from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import {
@@ -44,16 +51,21 @@ export function PersonalAccountDropdown({
id: string | null;
name: string | null;
picture_url: string | null;
application_role: string;
};
accounts: {
label: string | null;
value: string | null;
image?: string | null;
application_role: string;
}[];
signOutRequested: () => unknown;
paths: {
home: string;
admin: string;
doctor: string;
personalAccountSettings: string;
};
features: {
@@ -64,10 +76,7 @@ export function PersonalAccountDropdown({
className?: string;
}) {
const { data: personalAccountData } = usePersonalAccountData(
user.id,
account,
);
const { data: personalAccountData } = usePersonalAccountData(user.id);
const signedInAsLabel = useMemo(() => {
const email = user?.email ?? undefined;
@@ -79,15 +88,25 @@ export function PersonalAccountDropdown({
const displayName =
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
const isSuperAdmin = useMemo(() => {
const hasTotpFactor = useMemo(() => {
const factors = user?.factors ?? [];
const hasAdminRole = user?.app_metadata.role === 'super-admin';
const hasTotpFactor = factors.some(
return factors.some(
(factor) => factor.factor_type === 'totp' && factor.status === 'verified',
);
}, [user.factors]);
const isSuperAdmin = useMemo(() => {
const hasAdminRole =
personalAccountData?.application_role === 'super_admin';
return hasAdminRole && hasTotpFactor;
}, [user]);
}, [user, personalAccountData, hasTotpFactor]);
const isDoctor = useMemo(() => {
const hasDoctorRole = personalAccountData?.application_role === 'doctor';
return hasDoctorRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]);
return (
<DropdownMenu>
@@ -177,7 +196,7 @@ export function PersonalAccountDropdown({
<DropdownMenuItem key={account.value} asChild>
<Link
className={'s-full flex cursor-pointer items-center space-x-2'}
href={`/home/${account.value}`}
href={`${paths.home}/${account.value}`}
>
<div className={'flex items-center'}>
<Avatar className={'h-5 w-5 rounded-xs ' + account.image}>
@@ -209,7 +228,7 @@ export function PersonalAccountDropdown({
<DropdownMenuItem asChild>
<Link
className={'s-full flex cursor-pointer items-center space-x-2'}
href={'/home/settings'}
href={paths.personalAccountSettings}
>
<UserCircle className={'h-5'} />
@@ -227,7 +246,7 @@ export function PersonalAccountDropdown({
className={
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
}
href={'/admin'}
href={paths.admin}
>
<Shield className={'h-5'} />
@@ -236,6 +255,25 @@ export function PersonalAccountDropdown({
</DropdownMenuItem>
</If>
<If condition={isDoctor}>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link
className={
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
}
href={paths.doctor}
>
<Cross className={'h-5'} />
<span>
<Trans i18nKey="common:doctor" />
</span>
</Link>
</DropdownMenuItem>
</If>
<DropdownMenuSeparator />
<If condition={features.enableThemeToggle}>

View File

@@ -4,9 +4,13 @@ import { Database } from '@kit/supabase/database';
import { UserAnalysis } from '../types/accounts';
export type AccountWithParams = Database['medreport']['Tables']['accounts']['Row'] & {
account_params: Pick<Database['medreport']['Tables']['account_params']['Row'], 'weight' | 'height'> | null;
};
export type AccountWithParams =
Database['medreport']['Tables']['accounts']['Row'] & {
account_params: Pick<
Database['medreport']['Tables']['account_params']['Row'],
'weight' | 'height'
> | null;
};
/**
* Class representing an API for interacting with user accounts.
@@ -79,7 +83,8 @@ class AccountsApi {
accounts (
name,
slug,
picture_url
picture_url,
application_role
)
`,
)
@@ -95,6 +100,7 @@ class AccountsApi {
label: accounts.name,
value: accounts.slug,
image: accounts.picture_url,
application_role: accounts.application_role,
}));
}
@@ -209,11 +215,12 @@ class AccountsApi {
return null;
}
return analysisResponses
.map((r) => ({
...r,
elements: analysisResponseElements.filter((e) => e.analysis_response_id === r.id),
}));
return analysisResponses.map((r) => ({
...r,
elements: analysisResponseElements.filter(
(e) => e.analysis_response_id === r.id,
),
}));
}
}

View File

@@ -1,5 +1,7 @@
'use client';
import { useTransition } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
@@ -11,6 +13,7 @@ import { z } from 'zod';
import { Database } from '@kit/supabase/database';
import { Button } from '@kit/ui/button';
import { Checkbox } from '@kit/ui/checkbox';
import {
DropdownMenu,
DropdownMenuContent,
@@ -32,7 +35,10 @@ import {
SelectTrigger,
SelectValue,
} from '@kit/ui/select';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
import { updateRoleAction } from '../lib/server/admin-server-actions';
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
@@ -204,6 +210,39 @@ function getColumns(): ColumnDef<Account>[] {
header: 'Updated At',
accessorKey: 'updated_at',
},
{
id: 'isDoctor',
header: 'Doctor',
cell: ({ row }) => {
const [isPending, startTransition] = useTransition();
const handleToggle = () => {
startTransition(async () => {
const isDoctor = row.original.application_role === 'doctor';
const newRole = isDoctor ? 'user' : 'doctor';
const promise = updateRoleAction({
accountId: row.original.id,
role: newRole,
});
toast.promise(() => promise, {
success: <Trans i18nKey={'account:updateRoleSuccess'} />,
error: <Trans i18nKey={'account:updateRoleError'} />,
loading: <Trans i18nKey={'account:updateRoleLoading'} />,
});
});
};
return (
<Checkbox
checked={row.original.application_role === 'doctor'}
onCheckedChange={handleToggle}
disabled={isPending}
/>
);
},
},
{
id: 'actions',
header: '',

View File

@@ -14,6 +14,7 @@ import {
DeleteUserSchema,
ImpersonateUserSchema,
ReactivateUserSchema,
UpdateAccountRoleSchema,
} from './schema/admin-actions.schema';
import { CreateCompanySchema } from './schema/create-company.schema';
import { CreateUserSchema } from './schema/create-user.schema';
@@ -273,6 +274,32 @@ export const createCompanyAccountAction = enhanceAction(
},
);
/**
* @name updateRoleAction
* @description Update application role for user
*/
export const updateRoleAction = adminAction(
enhanceAction(
async ({ accountId, role }) => {
const service = getAdminAccountsService();
const logger = await getLogger();
logger.info({ accountId }, `Super Admin is updating account role...`);
await service.updateRole(accountId, role);
logger.info({ accountId }, `Successfully changed role`);
revalidateAdmin();
return { success: true };
},
{
schema: UpdateAccountRoleSchema,
},
),
);
function revalidateAdmin() {
revalidatePath('/admin', 'layout');
}

View File

@@ -1,5 +1,7 @@
import { z } from 'zod';
import { Database } from '@kit/supabase/database';
const ConfirmationSchema = z.object({
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
});
@@ -16,3 +18,10 @@ export const DeleteUserSchema = UserIdSchema;
export const DeleteAccountSchema = ConfirmationSchema.extend({
accountId: z.string().uuid(),
});
type ApplicationRoleType =
Database['medreport']['Tables']['accounts']['Row']['application_role'];
export const UpdateAccountRoleSchema = z.object({
accountId: z.string().uuid(),
role: z.string() as z.ZodType<ApplicationRoleType>,
});

View File

@@ -22,4 +22,19 @@ class AdminAccountsService {
throw error;
}
}
async updateRole(
accountId: string,
role: Database['medreport']['Tables']['accounts']['Row']['application_role'],
) {
const { error } = await this.adminClient
.schema('medreport')
.from('accounts')
.update({ application_role: role })
.eq('id', accountId);
if (error) {
throw error;
}
}
}

View File

@@ -0,0 +1,3 @@
import eslintConfigBase from '@kit/eslint-config/base.js';
export default eslintConfigBase;

View File

@@ -0,0 +1,43 @@
{
"name": "@kit/doctor",
"private": true,
"version": "0.1.0",
"scripts": {
"clean": "git clean -xdf .turbo node_modules",
"format": "prettier --check \"**/*.{ts,tsx}\"",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"prettier": "@kit/prettier-config",
"devDependencies": {
"@hookform/resolvers": "^5.0.1",
"@kit/eslint-config": "workspace:*",
"@kit/next": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/shared": "workspace:*",
"@kit/supabase": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@kit/ui": "workspace:*",
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@supabase/supabase-js": "2.49.4",
"@tanstack/react-query": "5.76.1",
"@tanstack/react-table": "^8.21.3",
"@types/react": "19.1.4",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0"
},
"exports": {
".": "./src/index.ts",
"./components/*": "./src/components/*.tsx"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
}
}

View File

@@ -0,0 +1,28 @@
import { notFound } from 'next/navigation';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { isDoctor } from '../lib/server/utils/is-doctor';
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
/**
* DoctorGuard is a server component wrapper that checks if the user is a doctor before rendering the component.
* If the user is not a doctor, we redirect to a 404.
* @param Component - The Page or Layout component to wrap
*/
export function DoctorGuard<Params extends object>(
Component: LayoutOrPageComponent<Params>,
) {
return async function DoctorGuardServerComponentWrapper(params: Params) {
const client = getSupabaseServerClient();
const isUserDoctor = await isDoctor(client);
// if the user is not a super-admin, we redirect to a 404
if (!isUserDoctor) {
notFound();
}
return <Component {...params} />;
};
}

View File

@@ -0,0 +1 @@
export * from './lib/server/utils/is-doctor';

View File

@@ -0,0 +1,24 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
/**
* @name isDoctor
* @description Check if the current user is a doctor.
* @param client
*/
export async function isDoctor(client: SupabaseClient<Database>) {
try {
const { data, error } = await client
.schema('medreport')
.rpc('is_doctor');
if (error) {
throw error;
}
return data;
} catch {
return false;
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": [
"node_modules"
]
}

View File

@@ -228,7 +228,6 @@ export function NotificationsPopover(params: {
size="icon"
variant="ghost"
onClick={() => {
console.log('test');
setNotifications((existing) => {
return existing.filter(
(existingNotification) =>

View File

@@ -264,6 +264,7 @@ export type Database = {
}
accounts: {
Row: {
application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null
created_at: string | null
created_by: string | null
@@ -284,6 +285,7 @@ export type Database = {
updated_by: string | null
}
Insert: {
application_role?: Database["medreport"]["Enums"]["application_role"]
city?: string | null
created_at?: string | null
created_by?: string | null
@@ -304,6 +306,7 @@ export type Database = {
updated_by?: string | null
}
Update: {
application_role?: Database["medreport"]["Enums"]["application_role"]
city?: string | null
created_at?: string | null
created_by?: string | null
@@ -1633,6 +1636,7 @@ export type Database = {
create_team_account: {
Args: { account_name: string; new_personal_code: string }
Returns: {
application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null
created_at: string | null
created_by: string | null
@@ -1761,6 +1765,10 @@ export type Database = {
Args: { account_slug: string }
Returns: boolean
}
is_doctor: {
Args: Record<PropertyKey, never>
Returns: boolean
}
is_mfa_compliant: {
Args: Record<PropertyKey, never>
Returns: boolean
@@ -1897,6 +1905,7 @@ export type Database = {
| "settings.manage"
| "members.manage"
| "invites.manage"
application_role: "user" | "doctor" | "super_admin"
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
notification_channel: "in_app" | "email"
notification_type: "info" | "warning" | "error"
@@ -7791,6 +7800,7 @@ export const Constants = {
"members.manage",
"invites.manage",
],
application_role: ["user", "doctor", "super_admin"],
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],
notification_channel: ["in_app", "email"],
notification_type: ["info", "warning", "error"],

60
pnpm-lock.yaml generated
View File

@@ -38,6 +38,9 @@ importers:
'@kit/database-webhooks':
specifier: workspace:*
version: link:packages/database-webhooks
'@kit/doctor':
specifier: workspace:*
version: link:packages/features/doctor
'@kit/email-templates':
specifier: workspace:*
version: link:packages/email-templates
@@ -783,6 +786,63 @@ importers:
specifier: ^2.0.3
version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
packages/features/doctor:
devDependencies:
'@hookform/resolvers':
specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/eslint-config':
specifier: workspace:*
version: link:../../../tooling/eslint
'@kit/next':
specifier: workspace:*
version: link:../../next
'@kit/prettier-config':
specifier: workspace:*
version: link:../../../tooling/prettier
'@kit/shared':
specifier: workspace:*
version: link:../../shared
'@kit/supabase':
specifier: workspace:*
version: link:../../supabase
'@kit/tsconfig':
specifier: workspace:*
version: link:../../../tooling/typescript
'@kit/ui':
specifier: workspace:*
version: link:../../ui
'@makerkit/data-loader-supabase-core':
specifier: ^0.0.10
version: 0.0.10(@supabase/postgrest-js@1.19.4)(@supabase/supabase-js@2.49.4)
'@makerkit/data-loader-supabase-nextjs':
specifier: ^1.2.5
version: 1.2.5(@supabase/postgrest-js@1.19.4)(@supabase/supabase-js@2.49.4)(@tanstack/react-query@5.76.1(react@19.1.0))(next@15.3.2(@babel/core@7.27.4)(@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)
'@supabase/supabase-js':
specifier: 2.49.4
version: 2.49.4
'@tanstack/react-query':
specifier: 5.76.1
version: 5.76.1(react@19.1.0)
'@tanstack/react-table':
specifier: ^8.21.3
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@types/react':
specifier: 19.1.4
version: 19.1.4
lucide-react:
specifier: ^0.510.0
version: 0.510.0(react@19.1.0)
next:
specifier: 15.3.2
version: 15.3.2(@babel/core@7.27.4)(@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:
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
packages/features/medusa-storefront:
dependencies:
'@headlessui/react':

View File

@@ -122,5 +122,8 @@
"consentToAnonymizedCompanyData": {
"label": "Consent to be included in employer statistics",
"description": "Consent to be included in anonymized company statistics"
}
},
"updateRoleSuccess": "Role updated",
"updateRoleError": "Something went wrong, please try again",
"updateRoleLoading": "Updating role..."
}

View File

@@ -109,5 +109,6 @@
"description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject",
"accept": "Accept"
}
},
"doctor": "Doctor"
}

View File

@@ -145,5 +145,8 @@
"successTitle": "Tere, {{firstName}} {{lastName}}",
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
"successButton": "Jätka"
}
},
"updateRoleSuccess": "Roll uuendatud",
"updateRoleError": "Midagi läks valesti. Palun proovi uuesti",
"updateRoleLoading": "Rolli uuendatakse..."
}

View File

@@ -127,5 +127,6 @@
"wallet": {
"balance": "Sinu MedReporti konto seis",
"expiredAt": "Kehtiv kuni {{expiredAt}}"
}
},
"doctor": "Arst"
}

View File

@@ -122,5 +122,8 @@
},
"analysisResults": {
"pageTitle": "My analysis results"
}
},
"updateRoleSuccess": "Role updated",
"updateRoleError": "Something went wrong, please try again",
"updateRoleLoading": "Updating role..."
}

View File

@@ -107,5 +107,6 @@
"description": "This website uses cookies to ensure you get the best experience on our website.",
"reject": "Reject",
"accept": "Accept"
}
},
"doctor": "Doctor"
}

47
run-test-sync-local.sh Normal file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
MEDUSA_ORDER_ID="order_01K1TQQHZGPXKDHAH81TDSNGXR"
# HOSTNAME="https://test.medreport.ee"
# JOBS_API_TOKEN="fd26ec26-70ed-11f0-9e95-431ac3b15a84"
HOSTNAME="http://localhost:3000"
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197
function send_medipost_test_response() {
curl -X POST "$HOSTNAME/api/order/medipost-test-response" \
--header "x-jobs-api-key: $JOBS_API_TOKEN" \
--header 'Content-Type: application/json' \
--data '{ "medusaOrderId": "'$MEDUSA_ORDER_ID'" }'
}
function sync_analysis_results() {
curl -X POST "$HOSTNAME/api/job/sync-analysis-results" \
--header "x-jobs-api-key: $JOBS_API_TOKEN"
}
function sync_analysis_groups() {
curl -X POST "$HOSTNAME/api/job/sync-analysis-groups" \
--header "x-jobs-api-key: $JOBS_API_TOKEN"
}
function sync_analysis_groups_store() {
curl -X POST "$HOSTNAME/api/job/sync-analysis-groups-store" \
--header "x-jobs-api-key: $JOBS_API_TOKEN"
}
# Requirements
# 1. Sync analysis groups from Medipost to B2B
sync_analysis_groups
# 2. Optional - sync all Medipost analysis groups from B2B to Medusa (or add manually)
#sync_analysis_groups_store
# 3. Set up products configurations in Medusa so B2B "Telli analüüs" page shows the product and you can do payment flow
# 4. After payment is done, run `send_medipost_test_response` to send the fake test results to Medipost
# send_medipost_test_response
# 5. Run `sync_analysis_results` to sync the all new Medipost results to B2B
# sync_analysis_results

View File

@@ -0,0 +1,37 @@
create type "medreport"."application_role" as enum ('user', 'doctor', 'super_admin');
ALTER TABLE medreport.accounts
ADD COLUMN "application_role" medreport.application_role NOT NULL DEFAULT 'user';
CREATE OR REPLACE FUNCTION medreport.is_doctor()
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
RETURN medreport.is_aal2() AND (EXISTS (
SELECT 1
FROM medreport.accounts
WHERE primary_owner_user_id = auth.uid()
AND application_role = 'doctor'
));
END;
$$;
grant execute on function medreport.is_doctor() to authenticated;
CREATE OR REPLACE FUNCTION medreport.is_super_admin()
RETURNS BOOLEAN
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
RETURN medreport.is_aal2() AND (EXISTS (
SELECT 1
FROM medreport.accounts
WHERE primary_owner_user_id = auth.uid()
AND application_role = 'super_admin'
));
END;
$$;
grant execute on function medreport.is_super_admin() to authenticated;

View File

@@ -1,2 +0,0 @@
-- Update your user role to Super Admin
update auth.users set raw_app_meta_data='{"provider": "email", "providers": ["email"], "role": "super-admin" }' where email='test2@test.ee';