Merge branch 'main' into MED-105-v3
This commit is contained in:
1
.env
1
.env
@@ -44,6 +44,7 @@ NEXT_PUBLIC_REALTIME_NOTIFICATIONS=true
|
|||||||
WEBHOOK_SENDER_PROVIDER=postgres
|
WEBHOOK_SENDER_PROVIDER=postgres
|
||||||
|
|
||||||
# MAILER DEV
|
# MAILER DEV
|
||||||
|
CONTACT_EMAIL=info@medreport.ee
|
||||||
MAILER_PROVIDER=nodemailer
|
MAILER_PROVIDER=nodemailer
|
||||||
EMAIL_SENDER=info@medreport.ee
|
EMAIL_SENDER=info@medreport.ee
|
||||||
EMAIL_USER= # refer to your email provider's documentation
|
EMAIL_USER= # refer to your email provider's documentation
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const ModeToggle = dynamic(() =>
|
|||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
home: pathsConfig.app.home,
|
home: pathsConfig.app.home,
|
||||||
|
admin: pathsConfig.app.admin,
|
||||||
|
doctor: pathsConfig.app.doctor,
|
||||||
|
personalAccountSettings: pathsConfig.app.personalAccountSettings,
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
|
|||||||
63
app/doctor/_components/doctor-sidebar.tsx
Normal file
63
app/doctor/_components/doctor-sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
app/doctor/_components/mobile-navigation.tsx
Normal file
28
app/doctor/_components/mobile-navigation.tsx
Normal 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
47
app/doctor/layout.tsx
Normal 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
3
app/doctor/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||||
|
|
||||||
|
export default GlobalLoader;
|
||||||
17
app/doctor/page.tsx
Normal file
17
app/doctor/page.tsx
Normal 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);
|
||||||
@@ -11,6 +11,9 @@ import pathsConfig from '~/config/paths.config';
|
|||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
home: pathsConfig.app.home,
|
home: pathsConfig.app.home,
|
||||||
|
admin: pathsConfig.app.admin,
|
||||||
|
doctor: pathsConfig.app.doctor,
|
||||||
|
personalAccountSettings: pathsConfig.app.personalAccountSettings
|
||||||
};
|
};
|
||||||
|
|
||||||
const features = {
|
const features = {
|
||||||
@@ -30,7 +33,7 @@ export function ProfileAccountDropdownContainer(props: {
|
|||||||
label: string | null;
|
label: string | null;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
}[]
|
}[];
|
||||||
}) {
|
}) {
|
||||||
const signOut = useSignOut();
|
const signOut = useSignOut();
|
||||||
const user = useUser(props.user);
|
const user = useUser(props.user);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const PathsSchema = z.object({
|
|||||||
accountMembers: z.string().min(1),
|
accountMembers: z.string().min(1),
|
||||||
accountBillingReturn: z.string().min(1),
|
accountBillingReturn: z.string().min(1),
|
||||||
joinTeam: 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',
|
analysisResults: '/home/analysis-results',
|
||||||
orderAnalysis: '/home/order-analysis',
|
orderAnalysis: '/home/order-analysis',
|
||||||
orderHealthAnalysis: '/home/order-health-analysis',
|
orderHealthAnalysis: '/home/order-health-analysis',
|
||||||
|
doctor: '/doctor',
|
||||||
|
admin: '/admin',
|
||||||
},
|
},
|
||||||
} satisfies z.infer<typeof PathsSchema>);
|
} satisfies z.infer<typeof PathsSchema>);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const sendCompanyOfferEmail = async (
|
|||||||
language: string,
|
language: string,
|
||||||
) => {
|
) => {
|
||||||
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
|
const { renderCompanyOfferEmail } = await import('@kit/email-templates');
|
||||||
const { html, subject, to } = await renderCompanyOfferEmail({
|
const { html, subject } = await renderCompanyOfferEmail({
|
||||||
language,
|
language,
|
||||||
companyData: data,
|
companyData: data,
|
||||||
});
|
});
|
||||||
@@ -19,7 +19,7 @@ export const sendCompanyOfferEmail = async (
|
|||||||
await sendEmail({
|
await sendEmail({
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
to,
|
to: process.env.CONTACT_EMAIL || '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const INTERNAL_PACKAGES = [
|
|||||||
'@kit/auth',
|
'@kit/auth',
|
||||||
'@kit/accounts',
|
'@kit/accounts',
|
||||||
'@kit/admin',
|
'@kit/admin',
|
||||||
|
'@kit/doctor',
|
||||||
'@kit/team-accounts',
|
'@kit/team-accounts',
|
||||||
'@kit/shared',
|
'@kit/shared',
|
||||||
'@kit/supabase',
|
'@kit/supabase',
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@kit/accounts": "workspace:*",
|
"@kit/accounts": "workspace:*",
|
||||||
"@kit/admin": "workspace:*",
|
"@kit/admin": "workspace:*",
|
||||||
|
"@kit/doctor": "workspace:*",
|
||||||
"@kit/analytics": "workspace:*",
|
"@kit/analytics": "workspace:*",
|
||||||
"@kit/auth": "workspace:*",
|
"@kit/auth": "workspace:*",
|
||||||
"@kit/billing": "workspace:*",
|
"@kit/billing": "workspace:*",
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import Link from 'next/link';
|
|||||||
|
|
||||||
import type { User } from '@supabase/supabase-js';
|
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 { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
|
||||||
import {
|
import {
|
||||||
@@ -44,16 +51,21 @@ export function PersonalAccountDropdown({
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
picture_url: string | null;
|
picture_url: string | null;
|
||||||
|
application_role: string;
|
||||||
};
|
};
|
||||||
accounts: {
|
accounts: {
|
||||||
label: string | null;
|
label: string | null;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
image?: string | null;
|
image?: string | null;
|
||||||
|
application_role: string;
|
||||||
}[];
|
}[];
|
||||||
signOutRequested: () => unknown;
|
signOutRequested: () => unknown;
|
||||||
|
|
||||||
paths: {
|
paths: {
|
||||||
home: string;
|
home: string;
|
||||||
|
admin: string;
|
||||||
|
doctor: string;
|
||||||
|
personalAccountSettings: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
features: {
|
features: {
|
||||||
@@ -64,10 +76,7 @@ export function PersonalAccountDropdown({
|
|||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const { data: personalAccountData } = usePersonalAccountData(
|
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
||||||
user.id,
|
|
||||||
account,
|
|
||||||
);
|
|
||||||
|
|
||||||
const signedInAsLabel = useMemo(() => {
|
const signedInAsLabel = useMemo(() => {
|
||||||
const email = user?.email ?? undefined;
|
const email = user?.email ?? undefined;
|
||||||
@@ -79,15 +88,25 @@ export function PersonalAccountDropdown({
|
|||||||
const displayName =
|
const displayName =
|
||||||
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
|
personalAccountData?.name ?? account?.name ?? user?.email ?? '';
|
||||||
|
|
||||||
const isSuperAdmin = useMemo(() => {
|
const hasTotpFactor = useMemo(() => {
|
||||||
const factors = user?.factors ?? [];
|
const factors = user?.factors ?? [];
|
||||||
const hasAdminRole = user?.app_metadata.role === 'super-admin';
|
return factors.some(
|
||||||
const hasTotpFactor = factors.some(
|
|
||||||
(factor) => factor.factor_type === 'totp' && factor.status === 'verified',
|
(factor) => factor.factor_type === 'totp' && factor.status === 'verified',
|
||||||
);
|
);
|
||||||
|
}, [user.factors]);
|
||||||
|
|
||||||
|
const isSuperAdmin = useMemo(() => {
|
||||||
|
const hasAdminRole =
|
||||||
|
personalAccountData?.application_role === 'super_admin';
|
||||||
|
|
||||||
return hasAdminRole && hasTotpFactor;
|
return hasAdminRole && hasTotpFactor;
|
||||||
}, [user]);
|
}, [user, personalAccountData, hasTotpFactor]);
|
||||||
|
|
||||||
|
const isDoctor = useMemo(() => {
|
||||||
|
const hasDoctorRole = personalAccountData?.application_role === 'doctor';
|
||||||
|
|
||||||
|
return hasDoctorRole && hasTotpFactor;
|
||||||
|
}, [user, personalAccountData, hasTotpFactor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -177,7 +196,7 @@ export function PersonalAccountDropdown({
|
|||||||
<DropdownMenuItem key={account.value} asChild>
|
<DropdownMenuItem key={account.value} asChild>
|
||||||
<Link
|
<Link
|
||||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
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'}>
|
<div className={'flex items-center'}>
|
||||||
<Avatar className={'h-5 w-5 rounded-xs ' + account.image}>
|
<Avatar className={'h-5 w-5 rounded-xs ' + account.image}>
|
||||||
@@ -209,7 +228,7 @@ export function PersonalAccountDropdown({
|
|||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link
|
<Link
|
||||||
className={'s-full flex cursor-pointer items-center space-x-2'}
|
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||||
href={'/home/settings'}
|
href={paths.personalAccountSettings}
|
||||||
>
|
>
|
||||||
<UserCircle className={'h-5'} />
|
<UserCircle className={'h-5'} />
|
||||||
|
|
||||||
@@ -227,7 +246,7 @@ export function PersonalAccountDropdown({
|
|||||||
className={
|
className={
|
||||||
's-full flex cursor-pointer items-center space-x-2 text-yellow-700 dark:text-yellow-500'
|
'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'} />
|
<Shield className={'h-5'} />
|
||||||
|
|
||||||
@@ -236,6 +255,25 @@ export function PersonalAccountDropdown({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</If>
|
</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 />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<If condition={features.enableThemeToggle}>
|
<If condition={features.enableThemeToggle}>
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import { Database } from '@kit/supabase/database';
|
|||||||
|
|
||||||
import { UserAnalysis } from '../types/accounts';
|
import { UserAnalysis } from '../types/accounts';
|
||||||
|
|
||||||
export type AccountWithParams = Database['medreport']['Tables']['accounts']['Row'] & {
|
export type AccountWithParams =
|
||||||
account_params: Pick<Database['medreport']['Tables']['account_params']['Row'], 'weight' | 'height'> | null;
|
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.
|
* Class representing an API for interacting with user accounts.
|
||||||
@@ -79,7 +83,8 @@ class AccountsApi {
|
|||||||
accounts (
|
accounts (
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
picture_url
|
picture_url,
|
||||||
|
application_role
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
@@ -95,6 +100,7 @@ class AccountsApi {
|
|||||||
label: accounts.name,
|
label: accounts.name,
|
||||||
value: accounts.slug,
|
value: accounts.slug,
|
||||||
image: accounts.picture_url,
|
image: accounts.picture_url,
|
||||||
|
application_role: accounts.application_role,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,11 +215,12 @@ class AccountsApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return analysisResponses
|
return analysisResponses.map((r) => ({
|
||||||
.map((r) => ({
|
...r,
|
||||||
...r,
|
elements: analysisResponseElements.filter(
|
||||||
elements: analysisResponseElements.filter((e) => e.analysis_response_id === r.id),
|
(e) => e.analysis_response_id === r.id,
|
||||||
}));
|
),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useTransition } from 'react';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
|
import { Checkbox } from '@kit/ui/checkbox';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -32,7 +35,10 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@kit/ui/select';
|
} 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 { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
|
||||||
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
|
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
|
||||||
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
||||||
@@ -204,6 +210,39 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
header: 'Updated At',
|
header: 'Updated At',
|
||||||
accessorKey: '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',
|
id: 'actions',
|
||||||
header: '',
|
header: '',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
DeleteUserSchema,
|
DeleteUserSchema,
|
||||||
ImpersonateUserSchema,
|
ImpersonateUserSchema,
|
||||||
ReactivateUserSchema,
|
ReactivateUserSchema,
|
||||||
|
UpdateAccountRoleSchema,
|
||||||
} from './schema/admin-actions.schema';
|
} from './schema/admin-actions.schema';
|
||||||
import { CreateCompanySchema } from './schema/create-company.schema';
|
import { CreateCompanySchema } from './schema/create-company.schema';
|
||||||
import { CreateUserSchema } from './schema/create-user.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() {
|
function revalidateAdmin() {
|
||||||
revalidatePath('/admin', 'layout');
|
revalidatePath('/admin', 'layout');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
const ConfirmationSchema = z.object({
|
const ConfirmationSchema = z.object({
|
||||||
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
|
confirmation: z.custom<string>((value) => value === 'CONFIRM'),
|
||||||
});
|
});
|
||||||
@@ -16,3 +18,10 @@ export const DeleteUserSchema = UserIdSchema;
|
|||||||
export const DeleteAccountSchema = ConfirmationSchema.extend({
|
export const DeleteAccountSchema = ConfirmationSchema.extend({
|
||||||
accountId: z.string().uuid(),
|
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>,
|
||||||
|
});
|
||||||
|
|||||||
@@ -22,4 +22,19 @@ class AdminAccountsService {
|
|||||||
throw error;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
packages/features/doctor/eslint.config.mjs
Normal file
3
packages/features/doctor/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||||
|
|
||||||
|
export default eslintConfigBase;
|
||||||
43
packages/features/doctor/package.json
Normal file
43
packages/features/doctor/package.json
Normal 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/features/doctor/src/components/doctor-guard.tsx
Normal file
28
packages/features/doctor/src/components/doctor-guard.tsx
Normal 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} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
1
packages/features/doctor/src/index.ts
Normal file
1
packages/features/doctor/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './lib/server/utils/is-doctor';
|
||||||
24
packages/features/doctor/src/lib/server/utils/is-doctor.ts
Normal file
24
packages/features/doctor/src/lib/server/utils/is-doctor.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/features/doctor/tsconfig.json
Normal file
10
packages/features/doctor/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "@kit/tsconfig/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
|
},
|
||||||
|
"include": ["*.ts", "src"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -228,7 +228,6 @@ export function NotificationsPopover(params: {
|
|||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log('test');
|
|
||||||
setNotifications((existing) => {
|
setNotifications((existing) => {
|
||||||
return existing.filter(
|
return existing.filter(
|
||||||
(existingNotification) =>
|
(existingNotification) =>
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
accounts: {
|
accounts: {
|
||||||
Row: {
|
Row: {
|
||||||
|
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||||
city: string | null
|
city: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
created_by: string | null
|
created_by: string | null
|
||||||
@@ -284,6 +285,7 @@ export type Database = {
|
|||||||
updated_by: string | null
|
updated_by: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
|
application_role?: Database["medreport"]["Enums"]["application_role"]
|
||||||
city?: string | null
|
city?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
created_by?: string | null
|
created_by?: string | null
|
||||||
@@ -304,6 +306,7 @@ export type Database = {
|
|||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
|
application_role?: Database["medreport"]["Enums"]["application_role"]
|
||||||
city?: string | null
|
city?: string | null
|
||||||
created_at?: string | null
|
created_at?: string | null
|
||||||
created_by?: string | null
|
created_by?: string | null
|
||||||
@@ -1633,6 +1636,7 @@ export type Database = {
|
|||||||
create_team_account: {
|
create_team_account: {
|
||||||
Args: { account_name: string; new_personal_code: string }
|
Args: { account_name: string; new_personal_code: string }
|
||||||
Returns: {
|
Returns: {
|
||||||
|
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||||
city: string | null
|
city: string | null
|
||||||
created_at: string | null
|
created_at: string | null
|
||||||
created_by: string | null
|
created_by: string | null
|
||||||
@@ -1761,6 +1765,10 @@ export type Database = {
|
|||||||
Args: { account_slug: string }
|
Args: { account_slug: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
}
|
}
|
||||||
|
is_doctor: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: boolean
|
||||||
|
}
|
||||||
is_mfa_compliant: {
|
is_mfa_compliant: {
|
||||||
Args: Record<PropertyKey, never>
|
Args: Record<PropertyKey, never>
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
@@ -1897,6 +1905,7 @@ export type Database = {
|
|||||||
| "settings.manage"
|
| "settings.manage"
|
||||||
| "members.manage"
|
| "members.manage"
|
||||||
| "invites.manage"
|
| "invites.manage"
|
||||||
|
application_role: "user" | "doctor" | "super_admin"
|
||||||
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
|
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
|
||||||
notification_channel: "in_app" | "email"
|
notification_channel: "in_app" | "email"
|
||||||
notification_type: "info" | "warning" | "error"
|
notification_type: "info" | "warning" | "error"
|
||||||
@@ -7791,6 +7800,7 @@ export const Constants = {
|
|||||||
"members.manage",
|
"members.manage",
|
||||||
"invites.manage",
|
"invites.manage",
|
||||||
],
|
],
|
||||||
|
application_role: ["user", "doctor", "super_admin"],
|
||||||
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],
|
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],
|
||||||
notification_channel: ["in_app", "email"],
|
notification_channel: ["in_app", "email"],
|
||||||
notification_type: ["info", "warning", "error"],
|
notification_type: ["info", "warning", "error"],
|
||||||
|
|||||||
60
pnpm-lock.yaml
generated
60
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
|||||||
'@kit/database-webhooks':
|
'@kit/database-webhooks':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/database-webhooks
|
version: link:packages/database-webhooks
|
||||||
|
'@kit/doctor':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:packages/features/doctor
|
||||||
'@kit/email-templates':
|
'@kit/email-templates':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/email-templates
|
version: link:packages/email-templates
|
||||||
@@ -783,6 +786,63 @@ importers:
|
|||||||
specifier: ^2.0.3
|
specifier: ^2.0.3
|
||||||
version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
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:
|
packages/features/medusa-storefront:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@headlessui/react':
|
'@headlessui/react':
|
||||||
|
|||||||
@@ -122,5 +122,8 @@
|
|||||||
"consentToAnonymizedCompanyData": {
|
"consentToAnonymizedCompanyData": {
|
||||||
"label": "Consent to be included in employer statistics",
|
"label": "Consent to be included in employer statistics",
|
||||||
"description": "Consent to be included in anonymized company statistics"
|
"description": "Consent to be included in anonymized company statistics"
|
||||||
}
|
},
|
||||||
}
|
"updateRoleSuccess": "Role updated",
|
||||||
|
"updateRoleError": "Something went wrong, please try again",
|
||||||
|
"updateRoleLoading": "Updating role..."
|
||||||
|
}
|
||||||
@@ -109,5 +109,6 @@
|
|||||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
"accept": "Accept"
|
"accept": "Accept"
|
||||||
}
|
},
|
||||||
|
"doctor": "Doctor"
|
||||||
}
|
}
|
||||||
@@ -145,5 +145,8 @@
|
|||||||
"successTitle": "Tere, {{firstName}} {{lastName}}",
|
"successTitle": "Tere, {{firstName}} {{lastName}}",
|
||||||
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
|
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
|
||||||
"successButton": "Jätka"
|
"successButton": "Jätka"
|
||||||
}
|
},
|
||||||
}
|
"updateRoleSuccess": "Roll uuendatud",
|
||||||
|
"updateRoleError": "Midagi läks valesti. Palun proovi uuesti",
|
||||||
|
"updateRoleLoading": "Rolli uuendatakse..."
|
||||||
|
}
|
||||||
@@ -127,5 +127,6 @@
|
|||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Sinu MedReporti konto seis",
|
"balance": "Sinu MedReporti konto seis",
|
||||||
"expiredAt": "Kehtiv kuni {{expiredAt}}"
|
"expiredAt": "Kehtiv kuni {{expiredAt}}"
|
||||||
}
|
},
|
||||||
}
|
"doctor": "Arst"
|
||||||
|
}
|
||||||
@@ -122,5 +122,8 @@
|
|||||||
},
|
},
|
||||||
"analysisResults": {
|
"analysisResults": {
|
||||||
"pageTitle": "My analysis results"
|
"pageTitle": "My analysis results"
|
||||||
}
|
},
|
||||||
}
|
"updateRoleSuccess": "Role updated",
|
||||||
|
"updateRoleError": "Something went wrong, please try again",
|
||||||
|
"updateRoleLoading": "Updating role..."
|
||||||
|
}
|
||||||
@@ -107,5 +107,6 @@
|
|||||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||||
"reject": "Reject",
|
"reject": "Reject",
|
||||||
"accept": "Accept"
|
"accept": "Accept"
|
||||||
}
|
},
|
||||||
|
"doctor": "Doctor"
|
||||||
}
|
}
|
||||||
47
run-test-sync-local.sh
Normal file
47
run-test-sync-local.sh
Normal 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
|
||||||
@@ -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;
|
||||||
@@ -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';
|
|
||||||
Reference in New Issue
Block a user