Fix: add application_role to account-related fields (#48)

* Fix: fix accounts view, menu

* add migration

* add application_role to account-related fields
This commit is contained in:
Helena
2025-08-14 11:40:53 +03:00
committed by GitHub
parent bbb5e83ed9
commit b4b75438d2
9 changed files with 146 additions and 33 deletions

View File

@@ -1,5 +1,6 @@
import type { User } from '@supabase/supabase-js'; import type { User } from '@supabase/supabase-js';
import { ApplicationRole } from '@kit/accounts/types/accounts';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -18,6 +19,7 @@ type AccountModel = {
label: string | null; label: string | null;
value: string | null; value: string | null;
image: string | null; image: string | null;
application_role: ApplicationRole | null;
}; };
export function TeamAccountLayoutSidebar(props: { export function TeamAccountLayoutSidebar(props: {
@@ -73,7 +75,10 @@ function SidebarContainer(props: {
<SidebarFooter> <SidebarFooter>
<SidebarContent> <SidebarContent>
<ProfileAccountDropdownContainer user={props.user} /> <ProfileAccountDropdownContainer
user={props.user}
accounts={accounts}
/>
</SidebarContent> </SidebarContent>
</SidebarFooter> </SidebarFooter>
</Sidebar> </Sidebar>

View File

@@ -1,7 +1,4 @@
import { import { useMemo } from 'react';
BorderedNavigationMenu,
BorderedNavigationMenuItem,
} from '@kit/ui/bordered-navigation-menu';
import { AppLogo } from '~/components/app-logo'; import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
@@ -10,18 +7,22 @@ import { getTeamAccountSidebarConfig } from '~/config/team-account-navigation.co
// local imports // local imports
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader'; import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
import { TeamAccountNotifications } from './team-account-notifications'; import { TeamAccountNotifications } from './team-account-notifications';
import { useMemo } from 'react';
export function TeamAccountNavigationMenu(props: { export function TeamAccountNavigationMenu(props: {
workspace: TeamAccountWorkspace; workspace: TeamAccountWorkspace;
}) { }) {
const { account, user, accounts: rawAccounts } = props.workspace; const { account, user, accounts: rawAccounts } = props.workspace;
const accounts = useMemo(() => rawAccounts.map((account) => ({ const accounts = useMemo(
label: account.name, () =>
value: account.slug, rawAccounts.map((account) => ({
image: account.picture_url, label: account.name,
})),[rawAccounts]) value: account.slug,
image: account.picture_url,
application_role: account.application_role,
})),
[rawAccounts],
);
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce< const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
Array<{ Array<{
@@ -48,7 +49,7 @@ export function TeamAccountNavigationMenu(props: {
<AppLogo /> <AppLogo />
</div> </div>
<div className={'flex items-center justify-end space-x-2.5 gap-2'}> <div className={'flex items-center justify-end gap-2 space-x-2.5'}>
<TeamAccountNotifications accountId={account.id} userId={user.id} /> <TeamAccountNotifications accountId={account.id} userId={user.id} />
<ProfileAccountDropdownContainer <ProfileAccountDropdownContainer
user={user} user={user}

View File

@@ -45,11 +45,14 @@ function SidebarLayout({
const data = use(loadTeamWorkspace(account)); const data = use(loadTeamWorkspace(account));
const state = use(getLayoutState(account)); const state = use(getLayoutState(account));
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({ const accounts = data.accounts.map(
label: name, ({ name, slug, picture_url, application_role }) => ({
value: slug, label: name,
image: picture_url, value: slug,
})); image: picture_url,
application_role,
}),
);
return ( return (
<TeamAccountWorkspaceContextProvider value={data}> <TeamAccountWorkspaceContextProvider value={data}>
@@ -91,11 +94,14 @@ function HeaderLayout({
}>) { }>) {
const data = use(loadTeamWorkspace(account)); const data = use(loadTeamWorkspace(account));
const accounts = data.accounts.map(({ name, slug, picture_url }) => ({ const accounts = data.accounts.map(
label: name, ({ name, slug, picture_url, application_role }) => ({
value: slug, label: name,
image: picture_url, value: slug,
})); image: picture_url,
application_role,
}),
);
return ( return (
<TeamAccountWorkspaceContextProvider value={data}> <TeamAccountWorkspaceContextProvider value={data}>

View File

@@ -3,6 +3,7 @@
import type { User } from '@supabase/supabase-js'; import type { User } from '@supabase/supabase-js';
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown'; import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
import { ApplicationRole } from '@kit/accounts/types/accounts';
import { useSignOut } from '@kit/supabase/hooks/use-sign-out'; import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
import { useUser } from '@kit/supabase/hooks/use-user'; import { useUser } from '@kit/supabase/hooks/use-user';
@@ -13,7 +14,7 @@ const paths = {
home: pathsConfig.app.home, home: pathsConfig.app.home,
admin: pathsConfig.app.admin, admin: pathsConfig.app.admin,
doctor: pathsConfig.app.doctor, doctor: pathsConfig.app.doctor,
personalAccountSettings: pathsConfig.app.personalAccountSettings personalAccountSettings: pathsConfig.app.personalAccountSettings,
}; };
const features = { const features = {
@@ -28,11 +29,13 @@ export function ProfileAccountDropdownContainer(props: {
id: string | null; id: string | null;
name: string | null; name: string | null;
picture_url: string | null; picture_url: string | null;
application_role: ApplicationRole | null;
}; };
accounts: { accounts: {
label: string | null; label: string | null;
value: string | null; value: string | null;
image?: string | null; image?: string | null;
application_role: ApplicationRole | null;
}[]; }[];
}) { }) {
const signOut = useSignOut(); const signOut = useSignOut();

View File

@@ -32,6 +32,7 @@ import { cn } from '@kit/ui/utils';
import { toTitleCase } from '~/lib/utils'; import { toTitleCase } from '~/lib/utils';
import { usePersonalAccountData } from '../hooks/use-personal-account-data'; import { usePersonalAccountData } from '../hooks/use-personal-account-data';
import { ApplicationRole, ApplicationRoleEnum } from '../types/accounts';
const PERSONAL_ACCOUNT_SLUG = 'personal'; const PERSONAL_ACCOUNT_SLUG = 'personal';
@@ -51,13 +52,13 @@ 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; application_role: ApplicationRole | null;
}; };
accounts: { accounts: {
label: string | null; label: string | null;
value: string | null; value: string | null;
image?: string | null; image?: string | null;
application_role: string; application_role: ApplicationRole | null;
}[]; }[];
signOutRequested: () => unknown; signOutRequested: () => unknown;
@@ -97,13 +98,14 @@ export function PersonalAccountDropdown({
const isSuperAdmin = useMemo(() => { const isSuperAdmin = useMemo(() => {
const hasAdminRole = const hasAdminRole =
personalAccountData?.application_role === 'super_admin'; personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
return hasAdminRole && hasTotpFactor; return hasAdminRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]); }, [user, personalAccountData, hasTotpFactor]);
const isDoctor = useMemo(() => { const isDoctor = useMemo(() => {
const hasDoctorRole = personalAccountData?.application_role === 'doctor'; const hasDoctorRole =
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
return hasDoctorRole && hasTotpFactor; return hasDoctorRole && hasTotpFactor;
}, [user, personalAccountData, hasTotpFactor]); }, [user, personalAccountData, hasTotpFactor]);

View File

@@ -1,7 +1,17 @@
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
export type UserAnalysisElement = Database['medreport']['Tables']['analysis_response_elements']['Row']; export type UserAnalysisElement =
export type UserAnalysisResponse = Database['medreport']['Tables']['analysis_responses']['Row'] & { Database['medreport']['Tables']['analysis_response_elements']['Row'];
elements: UserAnalysisElement[]; export type UserAnalysisResponse =
}; Database['medreport']['Tables']['analysis_responses']['Row'] & {
elements: UserAnalysisElement[];
};
export type UserAnalysis = UserAnalysisResponse[]; export type UserAnalysis = UserAnalysisResponse[];
export type ApplicationRole =
Database['medreport']['Tables']['accounts']['Row']['application_role'];
export enum ApplicationRoleEnum {
User = 'user',
Doctor = 'doctor',
SuperAdmin = 'super_admin',
}

View File

@@ -114,7 +114,8 @@ export class TeamAccountsApi {
role, role,
name, name,
slug, slug,
picture_url picture_url,
application_role
) )
`, `,
) )

View File

@@ -1563,6 +1563,9 @@ export type Database = {
Views: { Views: {
user_account_workspace: { user_account_workspace: {
Row: { Row: {
application_role:
| Database["medreport"]["Enums"]["application_role"]
| null
id: string | null id: string | null
name: string | null name: string | null
picture_url: string | null picture_url: string | null
@@ -1574,6 +1577,9 @@ export type Database = {
} }
user_accounts: { user_accounts: {
Row: { Row: {
application_role:
| Database["medreport"]["Enums"]["application_role"]
| null
id: string | null id: string | null
name: string | null name: string | null
picture_url: string | null picture_url: string | null
@@ -1634,7 +1640,9 @@ export type Database = {
Returns: Json Returns: Json
} }
create_team_account: { create_team_account: {
Args: { account_name: string; new_personal_code: string } Args:
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: { Returns: {
application_role: Database["medreport"]["Enums"]["application_role"] application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null city: string | null
@@ -1801,6 +1809,7 @@ export type Database = {
primary_owner_user_id: string primary_owner_user_id: string
subscription_status: Database["medreport"]["Enums"]["subscription_status"] subscription_status: Database["medreport"]["Enums"]["subscription_status"]
permissions: Database["medreport"]["Enums"]["app_permissions"][] permissions: Database["medreport"]["Enums"]["app_permissions"][]
application_role: Database["medreport"]["Enums"]["application_role"]
}[] }[]
} }
transfer_team_account_ownership: { transfer_team_account_ownership: {

View File

@@ -0,0 +1,76 @@
CREATE OR REPLACE VIEW medreport.user_accounts AS
SELECT
account.id,
account.name,
account.picture_url,
account.slug,
membership.account_role AS role,
COALESCE(account.application_role, 'user') AS application_role
FROM medreport.accounts account
JOIN medreport.accounts_memberships membership ON (account.id = membership.account_id)
WHERE (
membership.user_id = (SELECT auth.uid())
AND account.is_personal_account = false
AND account.id IN (
SELECT accounts_memberships.account_id
FROM medreport.accounts_memberships
WHERE accounts_memberships.user_id = (SELECT auth.uid())
)
);
GRANT SELECT ON medreport.user_accounts TO authenticated, service_role;
DROP FUNCTION IF EXISTS medreport.team_account_workspace(text);
CREATE FUNCTION medreport.team_account_workspace(account_slug text)
RETURNS TABLE(id uuid, name character varying, picture_url character varying, slug text, role character varying, role_hierarchy_level integer, primary_owner_user_id uuid, subscription_status medreport.subscription_status, permissions medreport.app_permissions[], application_role medreport.application_role)
LANGUAGE plpgsql
SET search_path TO ''
AS $function$begin
return QUERY
select
accounts.id,
accounts.name,
accounts.picture_url,
accounts.slug,
accounts.application_role,
accounts_memberships.account_role,
roles.hierarchy_level,
accounts.primary_owner_user_id,
subscriptions.status,
array_agg(role_permissions.permission)
from
medreport.accounts
join medreport.accounts_memberships on accounts.id = accounts_memberships.account_id
left join medreport.subscriptions on accounts.id = subscriptions.account_id
join medreport.roles on accounts_memberships.account_role = roles.name
left join medreport.role_permissions on accounts_memberships.account_role = role_permissions.role
where
accounts.slug = account_slug
and medreport.accounts_memberships.user_id = (select auth.uid())
group by
accounts.id,
accounts_memberships.account_role,
subscriptions.status,
roles.hierarchy_level;
end;$function$;
GRANT EXECUTE ON FUNCTION medreport.team_account_workspace(text) TO authenticated, service_role;
create or replace view medreport.user_account_workspace as SELECT accounts.id,
accounts.name,
accounts.picture_url,
( SELECT subscriptions.status
FROM medreport.subscriptions
WHERE (subscriptions.account_id = accounts.id)
LIMIT 1) AS subscription_status,
accounts.application_role
FROM medreport.accounts
WHERE ((accounts.primary_owner_user_id = ( SELECT auth.uid() AS uid)) AND (accounts.is_personal_account = true))
LIMIT 1;
grant
select
on medreport.user_account_workspace to authenticated,
service_role;