B2B-30: adds personal code to account, company admins invites members
This commit is contained in:
2
.env
2
.env
@@ -40,6 +40,8 @@ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true
|
|||||||
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true
|
||||||
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
NEXT_PUBLIC_LANGUAGE_PRIORITY=application
|
||||||
NEXT_PUBLIC_ENABLE_NOTIFICATIONS=true
|
NEXT_PUBLIC_ENABLE_NOTIFICATIONS=true
|
||||||
|
NEXT_PUBLIC_REALTIME_NOTIFICATIONS=true
|
||||||
|
|
||||||
|
|
||||||
# NEXTJS
|
# NEXTJS
|
||||||
NEXT_TELEMETRY_DISABLED=1
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%`);
|
queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%,personal_code.ilike.%${query}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return queryBuilder;
|
return queryBuilder;
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
|
||||||
BorderedNavigationMenu,
|
|
||||||
BorderedNavigationMenuItem,
|
|
||||||
} from '@kit/ui/bordered-navigation-menu';
|
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
|
|
||||||
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';
|
||||||
import featuresFlagConfig from '~/config/feature-flags.config';
|
import featuresFlagConfig from '~/config/feature-flags.config';
|
||||||
import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
|
|
||||||
|
|
||||||
// home imports
|
// home imports
|
||||||
import { HomeAccountSelector } from '../_components/home-account-selector';
|
import { HomeAccountSelector } from '../_components/home-account-selector';
|
||||||
@@ -17,41 +13,17 @@ import { type UserWorkspace } from '../_lib/server/load-user-workspace';
|
|||||||
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
|
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
|
||||||
const { workspace, user, accounts } = props.workspace;
|
const { workspace, user, accounts } = props.workspace;
|
||||||
|
|
||||||
const routes = personalAccountNavigationConfig.routes.reduce<
|
|
||||||
Array<{
|
|
||||||
path: string;
|
|
||||||
label: string;
|
|
||||||
Icon?: React.ReactNode;
|
|
||||||
end?: boolean | ((path: string) => boolean);
|
|
||||||
}>
|
|
||||||
>((acc, item) => {
|
|
||||||
if ('children' in item) {
|
|
||||||
return [...acc, ...item.children];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('divider' in item) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...acc, item];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-1 justify-between'}>
|
<div className={'flex w-full flex-1 justify-between'}>
|
||||||
<div className={'flex items-center space-x-8'}>
|
<div className={'flex items-center space-x-8'}>
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
|
|
||||||
<BorderedNavigationMenu>
|
|
||||||
{routes.map((route) => (
|
|
||||||
<BorderedNavigationMenuItem {...route} key={route.path} />
|
|
||||||
))}
|
|
||||||
</BorderedNavigationMenu>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2.5'}>
|
<div className={'flex justify-end space-x-2.5'}>
|
||||||
<UserNotifications userId={user.id} />
|
<UserNotifications userId={user.id} />
|
||||||
|
|
||||||
<If condition={featuresFlagConfig.enableTeamAccounts}>
|
<If condition={featuresFlagConfig.enableTeamAccounts && accounts.length}>
|
||||||
<HomeAccountSelector userId={user.id} accounts={accounts} />
|
<HomeAccountSelector userId={user.id} accounts={accounts} />
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
|||||||
@@ -28,15 +28,20 @@ async function workspaceLoader() {
|
|||||||
|
|
||||||
const workspacePromise = api.getAccountWorkspace();
|
const workspacePromise = api.getAccountWorkspace();
|
||||||
|
|
||||||
const [accounts, workspace, user] = await Promise.all([
|
// TODO!: remove before deploy to prod
|
||||||
|
const tempAccountsPromise = () => api.loadTempUserAccounts();
|
||||||
|
|
||||||
|
const [accounts, workspace, user, tempVisibleAccounts] = await Promise.all([
|
||||||
accountsPromise(),
|
accountsPromise(),
|
||||||
workspacePromise,
|
workspacePromise,
|
||||||
requireUserInServerComponent(),
|
requireUserInServerComponent(),
|
||||||
|
tempAccountsPromise()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
workspace,
|
workspace,
|
||||||
user,
|
user,
|
||||||
|
tempVisibleAccounts
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { PageBody } from '@kit/ui/page';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
@@ -6,6 +5,8 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { HomeLayoutPageHeader } from './_components/home-page-header';
|
import { HomeLayoutPageHeader } from './_components/home-page-header';
|
||||||
|
import { use } from 'react';
|
||||||
|
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
@@ -17,14 +18,21 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function UserHomePage() {
|
function UserHomePage() {
|
||||||
|
const { tempVisibleAccounts } = use(loadUserWorkspace());
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HomeLayoutPageHeader
|
<HomeLayoutPageHeader
|
||||||
title={<Trans i18nKey={'common:routes.home'} />}
|
title={<Trans i18nKey={'common:routes.home'} />}
|
||||||
description={<Trans i18nKey={'common:homeTabDescription'} />}
|
description={<Trans i18nKey={'common:homeTabDescription'} />}
|
||||||
/>
|
/>
|
||||||
|
{tempVisibleAccounts.length && (
|
||||||
|
<>
|
||||||
|
Member of companies:
|
||||||
|
<pre>{JSON.stringify(tempVisibleAccounts, null, 2)}</pre>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
<PageBody></PageBody>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { cookies } from 'next/headers';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { TeamAccountWorkspaceContextProvider } from '@kit/team-accounts/components';
|
import { CompanyGuard, TeamAccountWorkspaceContextProvider } from '@kit/team-accounts/components';
|
||||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
@@ -144,4 +144,4 @@ async function getLayoutState(account: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(TeamWorkspaceLayout);
|
export default withI18n(CompanyGuard(TeamWorkspaceLayout));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
import { DashboardDemo } from './_components/dashboard-demo';
|
import { DashboardDemo } from './_components/dashboard-demo';
|
||||||
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
|
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
|
||||||
|
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
|
||||||
|
|
||||||
interface TeamAccountHomePageProps {
|
interface TeamAccountHomePageProps {
|
||||||
params: Promise<{ account: string }>;
|
params: Promise<{ account: string }>;
|
||||||
@@ -41,4 +42,4 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(TeamAccountHomePage);
|
export default withI18n(CompanyGuard(TeamAccountHomePage));
|
||||||
|
|||||||
@@ -109,10 +109,7 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
|
|||||||
const signOutNext = `${pathsConfig.auth.signIn}?invite_token=${token}`;
|
const signOutNext = `${pathsConfig.auth.signIn}?invite_token=${token}`;
|
||||||
|
|
||||||
// once the user accepts the invitation, we redirect them to the account home page
|
// once the user accepts the invitation, we redirect them to the account home page
|
||||||
const accountHome = pathsConfig.app.accountHome.replace(
|
const accountHome = pathsConfig.app.home;
|
||||||
'[account]',
|
|
||||||
invitation.account.slug,
|
|
||||||
);
|
|
||||||
|
|
||||||
const email = auth.data.email ?? '';
|
const email = auth.data.email ?? '';
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
|||||||
"supabase:db:diff": "supabase db diff",
|
"supabase:db:diff": "supabase db diff",
|
||||||
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
|
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
|
||||||
"supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app",
|
"supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app",
|
||||||
"supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts",
|
"supabase:typegen:packages": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts",
|
||||||
"supabase:typegen:app": "supabase gen types typescript --local > ./lib/database.types.ts",
|
"supabase:typegen:app": "supabase gen types typescript --local > ./lib/database.types.ts",
|
||||||
"supabase:db:dump:local": "supabase db dump --local --data-only",
|
"supabase:db:dump:local": "supabase db dump --local --data-only",
|
||||||
"sync-data:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts"
|
"sync-data:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
Home,
|
Home,
|
||||||
LogOut,
|
LogOut,
|
||||||
MessageCircleQuestion,
|
UserCircle,
|
||||||
Shield,
|
Shield,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
@@ -170,12 +170,12 @@ 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={'/docs'}
|
href={'/home/settings'}
|
||||||
>
|
>
|
||||||
<MessageCircleQuestion className={'h-5'} />
|
<UserCircle className={'h-5'} />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'common:documentation'} />
|
<Trans i18nKey={'common:routes.profile'} />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -48,22 +48,60 @@ class AccountsApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @name loadUserAccounts
|
* @name loadUserAccounts
|
||||||
* Load the user accounts.
|
* Load only user-owned accounts (not just memberships).
|
||||||
*/
|
*/
|
||||||
async loadUserAccounts() {
|
async loadUserAccounts() {
|
||||||
|
const authUser = await this.client.auth.getUser();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
error: userError,
|
||||||
|
} = authUser
|
||||||
|
|
||||||
|
if (userError) {
|
||||||
|
throw userError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = data;
|
||||||
|
|
||||||
const { data: accounts, error } = await this.client
|
const { data: accounts, error } = await this.client
|
||||||
.from('user_accounts')
|
.from('accounts_memberships')
|
||||||
.select(`name, slug, picture_url`);
|
.select(`
|
||||||
|
account_id,
|
||||||
|
user_accounts (
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
picture_url
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.eq('account_role', 'owner');
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts.map(({ name, slug, picture_url }) => {
|
return accounts.map(({ user_accounts }) => ({
|
||||||
|
label: user_accounts.name,
|
||||||
|
value: user_accounts.slug,
|
||||||
|
image: user_accounts.picture_url,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async loadTempUserAccounts() {
|
||||||
|
const { data: accounts, error } = await this.client
|
||||||
|
.from('user_accounts')
|
||||||
|
.select(`name, slug`);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts.map(({ name, slug }) => {
|
||||||
return {
|
return {
|
||||||
label: name,
|
label: name,
|
||||||
value: slug,
|
value: slug,
|
||||||
image: picture_url,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ import { AdminMembersTable } from './admin-members-table';
|
|||||||
import { AdminMembershipsTable } from './admin-memberships-table';
|
import { AdminMembershipsTable } from './admin-memberships-table';
|
||||||
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
|
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AccountInvitationsTable,
|
||||||
|
AccountMembersTable,
|
||||||
|
InviteMembersDialogContainer,
|
||||||
|
} from '@kit/team-accounts/components';
|
||||||
|
|
||||||
type Account = Tables<'accounts'>;
|
type Account = Tables<'accounts'>;
|
||||||
type Membership = Tables<'accounts_memberships'>;
|
type Membership = Tables<'accounts_memberships'>;
|
||||||
|
|
||||||
@@ -146,8 +152,6 @@ async function PersonalAccountPage(props: { account: Account }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex flex-col gap-y-8'}>
|
<div className={'flex flex-col gap-y-8'}>
|
||||||
<SubscriptionsTable accountId={props.account.id} />
|
|
||||||
|
|
||||||
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
|
||||||
<Heading level={6}>Companies</Heading>
|
<Heading level={6}>Companies</Heading>
|
||||||
|
|
||||||
@@ -213,7 +217,7 @@ async function TeamAccountPage(props: {
|
|||||||
<div className={'flex flex-col gap-y-8'}>
|
<div className={'flex flex-col gap-y-8'}>
|
||||||
|
|
||||||
<div className={'flex flex-col gap-y-2.5'}>
|
<div className={'flex flex-col gap-y-2.5'}>
|
||||||
<Heading level={6}>Company Employees</Heading>
|
<Heading level={6}>Company Members</Heading>
|
||||||
|
|
||||||
<AdminMembersTable members={members} />
|
<AdminMembersTable members={members} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -179,6 +179,14 @@ function getColumns(): ColumnDef<Account>[] {
|
|||||||
header: 'Email',
|
header: 'Email',
|
||||||
accessorKey: 'email',
|
accessorKey: 'email',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'personalCode',
|
||||||
|
header: 'Personal Code',
|
||||||
|
accessorKey: 'personalCode',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return row.original.personal_code ?? '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'type',
|
id: 'type',
|
||||||
header: 'Type',
|
header: 'Type',
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
|||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
emailConfirm: false,
|
emailConfirm: false,
|
||||||
|
personalCode: ''
|
||||||
},
|
},
|
||||||
mode: 'onChange',
|
mode: 'onBlur',
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit = (data: CreateUserSchemaType) => {
|
const onSubmit = (data: CreateUserSchemaType) => {
|
||||||
@@ -98,6 +99,25 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
name={'personalCode'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Personal code</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
placeholder={'48506040199'}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name={'email'}
|
name={'email'}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ function getColumns(): ColumnDef<Memberships>[] {
|
|||||||
{
|
{
|
||||||
header: 'Role',
|
header: 'Role',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return row.original.role === 'owner' ? 'HR' : 'Employee';
|
return row.original.role === 'owner' ? 'Admin' : 'Member';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const deleteAccountAction = adminAction(
|
|||||||
*/
|
*/
|
||||||
export const createUserAction = adminAction(
|
export const createUserAction = adminAction(
|
||||||
enhanceAction(
|
enhanceAction(
|
||||||
async ({ email, password, emailConfirm }) => {
|
async ({ email, password, emailConfirm, personalCode }) => {
|
||||||
const adminClient = getSupabaseServerAdminClient();
|
const adminClient = getSupabaseServerAdminClient();
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
@@ -182,6 +182,16 @@ export const createUserAction = adminAction(
|
|||||||
`Super Admin has successfully created a new user`,
|
`Super Admin has successfully created a new user`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { error: accountError } = await adminClient
|
||||||
|
.from('accounts')
|
||||||
|
.update({ personal_code: personalCode })
|
||||||
|
.eq('id', data.user.id);
|
||||||
|
|
||||||
|
if (accountError) {
|
||||||
|
logger.error({ accountError }, 'Error inserting personal code to accounts');
|
||||||
|
throw new Error(`Error saving personal code: ${accountError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
revalidateAdmin();
|
revalidateAdmin();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const CreateUserProfileSchema = z.object({
|
||||||
|
personalCode: z.string().regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
||||||
|
message: 'Invalid Estonian personal code format',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreateUserProfileSchemaType = z.infer<typeof CreateUserProfileSchema>;
|
||||||
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const CreateUserSchema = z.object({
|
export const CreateUserSchema = z.object({
|
||||||
|
personalCode: z.string().regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
||||||
|
message: 'Invalid Estonian personal code format',
|
||||||
|
}),
|
||||||
email: z.string().email({ message: 'Please enter a valid email address' }),
|
email: z.string().email({ message: 'Please enter a valid email address' }),
|
||||||
password: z
|
password: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface PasswordSignUpFormProps {
|
|||||||
displayTermsCheckbox?: boolean;
|
displayTermsCheckbox?: boolean;
|
||||||
|
|
||||||
onSubmit: (params: {
|
onSubmit: (params: {
|
||||||
|
personalCode: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
repeatPassword: string;
|
repeatPassword: string;
|
||||||
@@ -48,6 +49,7 @@ export function PasswordSignUpForm({
|
|||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(PasswordSignUpSchema),
|
resolver: zodResolver(PasswordSignUpSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
personalCode: '',
|
||||||
email: defaultValues?.email ?? '',
|
email: defaultValues?.email ?? '',
|
||||||
password: '',
|
password: '',
|
||||||
repeatPassword: '',
|
repeatPassword: '',
|
||||||
@@ -60,6 +62,29 @@ export function PasswordSignUpForm({
|
|||||||
className={'flex w-full flex-col gap-y-4'}
|
className={'flex w-full flex-col gap-y-4'}
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={'personalCode'}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'common:personalCode'} />
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
data-test={'personal-code-input'}
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
placeholder={t('personalCodePlaceholder')}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={'email'}
|
name={'email'}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { MagicLinkAuthContainer } from './magic-link-auth-container';
|
import { MagicLinkAuthContainer } from './magic-link-auth-container';
|
||||||
import { OauthProviders } from './oauth-providers';
|
import { OauthProviders } from './oauth-providers';
|
||||||
import { EmailPasswordSignUpContainer } from './password-sign-up-container';
|
import { EmailPasswordSignUpContainer } from './password-sign-up-container';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export function SignUpMethodsContainer(props: {
|
export function SignUpMethodsContainer(props: {
|
||||||
paths: {
|
paths: {
|
||||||
@@ -41,6 +42,7 @@ export function SignUpMethodsContainer(props: {
|
|||||||
emailRedirectTo={redirectUrl}
|
emailRedirectTo={redirectUrl}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={props.displayTermsCheckbox}
|
displayTermsCheckbox={props.displayTermsCheckbox}
|
||||||
|
onSignUp={() => redirect(redirectUrl)}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAppEvents } from '@kit/shared/events';
|
|||||||
import { useSignUpWithEmailAndPassword } from '@kit/supabase/hooks/use-sign-up-with-email-password';
|
import { useSignUpWithEmailAndPassword } from '@kit/supabase/hooks/use-sign-up-with-email-password';
|
||||||
|
|
||||||
type SignUpCredentials = {
|
type SignUpCredentials = {
|
||||||
|
personalCode: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
@@ -46,7 +47,6 @@ export function usePasswordSignUpFlow({
|
|||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
captchaToken,
|
captchaToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
// emit event to track sign up
|
// emit event to track sign up
|
||||||
appEvents.emit({
|
appEvents.emit({
|
||||||
type: 'user.signedUp',
|
type: 'user.signedUp',
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { RefinedPasswordSchema, refineRepeatPassword } from './password.schema';
|
|||||||
|
|
||||||
export const PasswordSignUpSchema = z
|
export const PasswordSignUpSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
personalCode: z.string().regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
||||||
|
message: 'Invalid Estonian personal code format',
|
||||||
|
}),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: RefinedPasswordSchema,
|
password: RefinedPasswordSchema,
|
||||||
repeatPassword: RefinedPasswordSchema,
|
repeatPassword: RefinedPasswordSchema,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
"./api": "./src/server/api.ts",
|
"./api": "./src/server/api.ts",
|
||||||
"./components": "./src/components/index.ts",
|
"./components": "./src/components/index.ts",
|
||||||
"./hooks/*": "./src/hooks/*.ts",
|
"./hooks/*": "./src/hooks/*.ts",
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
import { isCompanyAdmin } from '../server/utils/is-company-admin';
|
||||||
|
import { isSuperAdmin } from '@kit/admin'
|
||||||
|
|
||||||
|
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CompanyGuard is a server component wrapper that checks if the user is a company admin before rendering the component.
|
||||||
|
* If the user is not a company admin, we redirect to a 404.
|
||||||
|
* @param Component - The Page or Layout component to wrap
|
||||||
|
*/
|
||||||
|
export function CompanyGuard<Params extends object>(
|
||||||
|
Component: LayoutOrPageComponent<Params>,
|
||||||
|
) {
|
||||||
|
return async function AdminGuardServerComponentWrapper(params: Params) {
|
||||||
|
//@ts-ignore
|
||||||
|
const { account } = await params.params;
|
||||||
|
const client = getSupabaseServerClient();
|
||||||
|
const [isUserSuperAdmin, isUserCompanyAdmin] = await Promise.all(
|
||||||
|
[isSuperAdmin(client), isCompanyAdmin(client, account)]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log({ isUserSuperAdmin, isUserCompanyAdmin , params: account})
|
||||||
|
if (isUserSuperAdmin || isUserCompanyAdmin) {
|
||||||
|
return <Component {...params} />;
|
||||||
|
}
|
||||||
|
// if the user is not a company admin, we redirect to a 404
|
||||||
|
notFound();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,3 +6,4 @@ export * from './settings/team-account-settings-container';
|
|||||||
export * from './invitations/accept-invitation-container';
|
export * from './invitations/accept-invitation-container';
|
||||||
export * from './create-team-account-dialog';
|
export * from './create-team-account-dialog';
|
||||||
export * from './team-account-workspace-context';
|
export * from './team-account-workspace-context';
|
||||||
|
export * from './company-guard';
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ function useGetColumns(permissions: {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: t('personalCode'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const { personal_code } = row.original;
|
||||||
|
|
||||||
|
return personal_code;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: t('roleLabel'),
|
header: t('roleLabel'),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export function AccountMembersTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
displayName.includes(searchString) ||
|
displayName.includes(searchString) ||
|
||||||
member.role.toLowerCase().includes(searchString)
|
member.role.toLowerCase().includes(searchString) ||
|
||||||
|
(member.personal_code || '').includes(searchString)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.sort((prev, next) => {
|
.sort((prev, next) => {
|
||||||
@@ -160,6 +161,13 @@ function useGetColumns(
|
|||||||
return row.original.email ?? '-';
|
return row.original.email ?? '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: t('personalCode'),
|
||||||
|
accessorKey: 'personal_code',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return row.original.personal_code ?? '-';
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: t('roleLabel'),
|
header: t('roleLabel'),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function InviteMembersDialogContainer({
|
|||||||
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
<DialogContent className="max-w-[800px]" onInteractOutside={(e) => e.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
||||||
@@ -142,13 +142,39 @@ function InviteMembersForm({
|
|||||||
{fieldArray.fields.map((field, index) => {
|
{fieldArray.fields.map((field, index) => {
|
||||||
const isFirst = index === 0;
|
const isFirst = index === 0;
|
||||||
|
|
||||||
|
const personalCodeInputName = `invitations.${index}.personal_code` as const;
|
||||||
const emailInputName = `invitations.${index}.email` as const;
|
const emailInputName = `invitations.${index}.email` as const;
|
||||||
const roleInputName = `invitations.${index}.role` as const;
|
const roleInputName = `invitations.${index}.role` as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test={'invite-member-form-item'} key={field.id}>
|
<div data-test={'invite-member-form-item'} key={field.id}>
|
||||||
<div className={'flex items-end gap-x-1 md:space-x-2'}>
|
<div className={'flex items-end gap-x-1 md:space-x-2'}>
|
||||||
<div className={'w-7/12'}>
|
<div className={'w-4/12'}>
|
||||||
|
<FormField
|
||||||
|
name={personalCodeInputName}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<If condition={isFirst}>
|
||||||
|
<FormLabel>{t('Personal code')}</FormLabel>
|
||||||
|
</If>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={t('personalCode')}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={'w-4/12'}>
|
||||||
<FormField
|
<FormField
|
||||||
name={emailInputName}
|
name={emailInputName}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
@@ -273,5 +299,5 @@ function InviteMembersForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyInviteModel() {
|
function createEmptyInviteModel() {
|
||||||
return { email: '', role: 'member' as Role };
|
return { email: '', role: 'member' as Role, personal_code: '' };
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/features/team-accounts/src/index.ts
Normal file
1
packages/features/team-accounts/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './server/utils/is-company-admin';
|
||||||
@@ -3,6 +3,9 @@ import { z } from 'zod';
|
|||||||
const InviteSchema = z.object({
|
const InviteSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
role: z.string().min(1).max(100),
|
role: z.string().min(1).max(100),
|
||||||
|
personal_code: z.string().regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
||||||
|
message: 'Invalid Estonian personal code format',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const InviteMembersSchema = z
|
export const InviteMembersSchema = z
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { RenewInvitationSchema } from '../../schema/renew-invitation.schema';
|
|||||||
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
|
||||||
import { createAccountInvitationsService } from '../services/account-invitations.service';
|
import { createAccountInvitationsService } from '../services/account-invitations.service';
|
||||||
import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service';
|
import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service';
|
||||||
|
import { createNotificationsApi } from '@kit/notifications/api';
|
||||||
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name createInvitationsAction
|
* @name createInvitationsAction
|
||||||
@@ -23,14 +25,55 @@ import { createAccountPerSeatBillingService } from '../services/account-per-seat
|
|||||||
*/
|
*/
|
||||||
export const createInvitationsAction = enhanceAction(
|
export const createInvitationsAction = enhanceAction(
|
||||||
async (params) => {
|
async (params) => {
|
||||||
|
const logger = await getLogger();
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
const serviceClient = getSupabaseServerAdminClient();
|
||||||
|
|
||||||
// Create the service
|
|
||||||
const service = createAccountInvitationsService(client);
|
const service = createAccountInvitationsService(client);
|
||||||
|
const api = createNotificationsApi(serviceClient);
|
||||||
|
|
||||||
// send invitations
|
|
||||||
await service.sendInvitations(params);
|
await service.sendInvitations(params);
|
||||||
|
|
||||||
|
const { invitations: invitationParams, accountSlug } = params;
|
||||||
|
const personalCodes = invitationParams.map(({ personal_code }) => personal_code);
|
||||||
|
|
||||||
|
const { data: company, error: companyError } = await client
|
||||||
|
.from('accounts')
|
||||||
|
.select('id')
|
||||||
|
.eq('slug', accountSlug);
|
||||||
|
|
||||||
|
logger.debug({ company, companyError, personalCodes })
|
||||||
|
|
||||||
|
if (companyError || !company?.length || !company[0]) {
|
||||||
|
throw new Error(`Failed to fetch company id: ${companyError?.message || 'not found'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: invitations, error: invitationError } = await serviceClient.rpc(
|
||||||
|
'get_invitations_with_account_ids',
|
||||||
|
{
|
||||||
|
company_id: company[0].id,
|
||||||
|
personal_codes: personalCodes,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug({ invitations, invitationError })
|
||||||
|
|
||||||
|
if (invitationError) {
|
||||||
|
throw new Error(`Failed to fetch invitations with accounts: ${invitationError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationPromises = invitations
|
||||||
|
.map(({ invite_token, account_id }) =>
|
||||||
|
api.createNotification({
|
||||||
|
account_id: account_id!,
|
||||||
|
body: `You are invited to join the company: ${accountSlug}`,
|
||||||
|
link: `/join?invite_token=${invite_token}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(notificationPromises);
|
||||||
|
|
||||||
|
logger.info('All invitation notifications are sent')
|
||||||
revalidateMemberPage();
|
revalidateMemberPage();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name isCompanyAdmin
|
||||||
|
* @description Check if the current user is a super admin.
|
||||||
|
* @param client
|
||||||
|
*/
|
||||||
|
export async function isCompanyAdmin(client: SupabaseClient<Database>, accountSlug: string) {
|
||||||
|
try {
|
||||||
|
const { data, error } = await client.rpc('is_company_admin', {
|
||||||
|
account_slug: accountSlug,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ import { useMutation } from '@tanstack/react-query';
|
|||||||
import { useSupabase } from './use-supabase';
|
import { useSupabase } from './use-supabase';
|
||||||
|
|
||||||
interface Credentials {
|
interface Credentials {
|
||||||
|
personalCode: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
emailRedirectTo: string;
|
emailRedirectTo: string;
|
||||||
@@ -14,13 +15,17 @@ export function useSignUpWithEmailAndPassword() {
|
|||||||
const mutationKey = ['auth', 'sign-up-with-email-password'];
|
const mutationKey = ['auth', 'sign-up-with-email-password'];
|
||||||
|
|
||||||
const mutationFn = async (params: Credentials) => {
|
const mutationFn = async (params: Credentials) => {
|
||||||
const { emailRedirectTo, captchaToken, ...credentials } = params;
|
const { emailRedirectTo, captchaToken, personalCode, ...credentials } = params;
|
||||||
|
|
||||||
|
// TODO?: should be a validation of unique personal code before registration
|
||||||
const response = await client.auth.signUp({
|
const response = await client.auth.signUp({
|
||||||
...credentials,
|
...credentials,
|
||||||
options: {
|
options: {
|
||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
captchaToken,
|
captchaToken,
|
||||||
|
data: {
|
||||||
|
personalCode
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
||||||
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
||||||
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
||||||
"noPermissionsAlertBody": "Please contact your account owner to change the billing settings for your account.",
|
"noPermissionsAlertBody": "Please contact your account admin to change the billing settings for your account.",
|
||||||
"checkoutSuccessTitle": "Done! You're all set.",
|
"checkoutSuccessTitle": "Done! You're all set.",
|
||||||
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Proceed to App",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account admin.",
|
||||||
"manageTeamPlan": "Manage your Company Plan",
|
"manageTeamPlan": "Manage your Company Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Base Plan",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Proceed to Payment",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Start Trial",
|
||||||
"perTeamMember": "Per company employee",
|
"perTeamMember": "Per company member",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "Per {{unit}} usage",
|
||||||
"teamMembers": "Company Employees",
|
"teamMembers": "Company Members",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "above {{ previousTier }} {{ unit }}",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"homeTabLabel": "Home",
|
"homeTabLabel": "Home",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Welcome to your home page",
|
||||||
"accountMembers": "Company Employees",
|
"accountMembers": "Company Members",
|
||||||
"membersTabDescription": "Here you can manage the employees of your company.",
|
"membersTabDescription": "Here you can manage the members of your company.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Billing",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Manage your billing and subscription",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Dashboard",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"members": "Employees",
|
"members": "Members",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"owner": {
|
"owner": {
|
||||||
"label": "Owner"
|
"label": "Admin"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Employee"
|
"label": "Member"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Employees"
|
"pageTitle": "Members"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Billing"
|
||||||
@@ -23,17 +23,17 @@
|
|||||||
"creatingTeam": "Creating Company...",
|
"creatingTeam": "Creating Company...",
|
||||||
"personalAccount": "Personal Account",
|
"personalAccount": "Personal Account",
|
||||||
"searchAccount": "Search Account...",
|
"searchAccount": "Search Account...",
|
||||||
"membersTabLabel": "Employees",
|
"membersTabLabel": "Members",
|
||||||
"memberName": "Name",
|
"memberName": "Name",
|
||||||
"youLabel": "You",
|
"youLabel": "You",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"roleLabel": "Role",
|
"roleLabel": "Role",
|
||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Admin",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited at",
|
"invitedAtLabel": "Invited at",
|
||||||
"inviteMembersPageSubheading": "Invite employees to your Company",
|
"inviteMembersPageSubheading": "Invite members to your Company",
|
||||||
"createTeamModalHeading": "Create Company",
|
"createTeamModalHeading": "Create Company",
|
||||||
"createTeamModalDescription": "Create a new Company to manage your projects and employees.",
|
"createTeamModalDescription": "Create a new Company to manage your projects and members.",
|
||||||
"teamNameLabel": "Company Name",
|
"teamNameLabel": "Company Name",
|
||||||
"teamNameDescription": "Your company name should be unique and descriptive",
|
"teamNameDescription": "Your company name should be unique and descriptive",
|
||||||
"createTeamSubmitLabel": "Create Company",
|
"createTeamSubmitLabel": "Create Company",
|
||||||
@@ -44,34 +44,34 @@
|
|||||||
"createTeamDropdownLabel": "New company",
|
"createTeamDropdownLabel": "New company",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove from Account",
|
"removeMember": "Remove from Account",
|
||||||
"inviteMembersSuccess": "Employees invited successfully!",
|
"inviteMembersSuccess": "Members invited successfully!",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
||||||
"inviteMembersLoading": "Inviting employees...",
|
"inviteMembersLoading": "Inviting members...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"removeInviteButtonLabel": "Remove invite",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"addAnotherMemberButtonLabel": "Add another one",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"inviteMembersButtonLabel": "Send Invites",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"removeMemberModalHeading": "You are removing this user",
|
||||||
"removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.",
|
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.",
|
||||||
"removeMemberSuccessMessage": "Employee removed successfully",
|
"removeMemberSuccessMessage": "Member removed successfully",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
||||||
"removeMemberLoadingMessage": "Removing employee...",
|
"removeMemberLoadingMessage": "Removing member...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Company",
|
"removeMemberSubmitLabel": "Remove User from Company",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRole": "Update Role",
|
"updateRole": "Update Role",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
||||||
"updateMemberRoleModalHeading": "Update Employee's Role",
|
"updateMemberRoleModalHeading": "Update Member's Role",
|
||||||
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
||||||
"roleMustBeDifferent": "Role must be different from the current one",
|
"roleMustBeDifferent": "Role must be different from the current one",
|
||||||
"memberRoleInputLabel": "Member role",
|
"memberRoleInputLabel": "Member role",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"updateRoleDescription": "Pick a role for this member.",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"updateRoleSubmitLabel": "Update Role",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"transferOwnership": "Transfer Ownership",
|
||||||
"transferOwnershipDescription": "Transfer ownership of the company account to another employee.",
|
"transferOwnershipDescription": "Transfer ownership of the company account to another member.",
|
||||||
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
||||||
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the company account.",
|
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.",
|
||||||
"deleteInvitation": "Delete Invitation",
|
"deleteInvitation": "Delete Invitation",
|
||||||
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
||||||
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
||||||
@@ -92,21 +92,21 @@
|
|||||||
"teamLogoInputHeading": "Upload your company's Logo",
|
"teamLogoInputHeading": "Upload your company's Logo",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
||||||
"updateTeamSubmitLabel": "Update Company",
|
"updateTeamSubmitLabel": "Update Company",
|
||||||
"inviteMembersHeading": "Invite Employees to your Company",
|
"inviteMembersHeading": "Invite Members to your Company",
|
||||||
"inviteMembersDescription": "Invite employees to your company by entering their email and role.",
|
"inviteMembersDescription": "Invite member to your company by entering their email and role.",
|
||||||
"emailPlaceholder": "employee@email.com",
|
"emailPlaceholder": "member@email.com",
|
||||||
"membersPageHeading": "Employees",
|
"membersPageHeading": "Members",
|
||||||
"inviteMembersButton": "Invite Employees",
|
"inviteMembersButton": "Invite Members",
|
||||||
"invitingMembers": "Inviting employees...",
|
"invitingMembers": "Inviting members...",
|
||||||
"inviteMembersSuccessMessage": "Employees invited successfully",
|
"inviteMembersSuccessMessage": "Members invited successfully",
|
||||||
"inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.",
|
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"pendingInvitesHeading": "Pending Invites",
|
||||||
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
||||||
"noPendingInvites": "No pending invites found",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading employees...",
|
"loadingMembers": "Loading members...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your company's employees.",
|
"loadMembersError": "Sorry, we couldn't fetch your company's members.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.",
|
||||||
"loadingInvitedMembers": "Loading invited employees...",
|
"loadingInvitedMembers": "Loading invited members...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
@@ -126,13 +126,13 @@
|
|||||||
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
||||||
"searchMembersPlaceholder": "Search employees",
|
"searchMembersPlaceholder": "Search members",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
||||||
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.",
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
||||||
"searchInvitations": "Search Invitations",
|
"searchInvitations": "Search Invitations",
|
||||||
"updateInvitation": "Update Invitation",
|
"updateInvitation": "Update Invitation",
|
||||||
"removeInvitation": "Remove Invitation",
|
"removeInvitation": "Remove Invitation",
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inviteStatus": "Status",
|
"inviteStatus": "Status",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"inviteNotFoundOrExpired": "Invite not found or expired",
|
||||||
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company HR to renew the invite.",
|
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.",
|
||||||
"backToHome": "Back to Home",
|
"backToHome": "Back to Home",
|
||||||
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
||||||
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
||||||
@@ -159,5 +159,6 @@
|
|||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
||||||
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
|
||||||
|
"personalCode": "Personal Code"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
||||||
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
||||||
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
||||||
"noPermissionsAlertBody": "Please contact your account owner to change the billing settings for your account.",
|
"noPermissionsAlertBody": "Please contact your account admin to change the billing settings for your account.",
|
||||||
"checkoutSuccessTitle": "Done! You're all set.",
|
"checkoutSuccessTitle": "Done! You're all set.",
|
||||||
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Proceed to App",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account admin.",
|
||||||
"manageTeamPlan": "Manage your Company Plan",
|
"manageTeamPlan": "Manage your Company Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Base Plan",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Proceed to Payment",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Start Trial",
|
||||||
"perTeamMember": "Per company employee",
|
"perTeamMember": "Per company member",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "Per {{unit}} usage",
|
||||||
"teamMembers": "Company Employees",
|
"teamMembers": "Company Members",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "above {{ previousTier }} {{ unit }}",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"homeTabLabel": "Home",
|
"homeTabLabel": "Home",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Welcome to your home page",
|
||||||
"accountMembers": "Company Employees",
|
"accountMembers": "Company Members",
|
||||||
"membersTabDescription": "Here you can manage the employees of your company.",
|
"membersTabDescription": "Here you can manage the members of your company.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Billing",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Manage your billing and subscription",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Dashboard",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"members": "Employees",
|
"members": "Members",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"owner": {
|
"owner": {
|
||||||
"label": "Owner"
|
"label": "Admin"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Employee"
|
"label": "Member"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
|||||||
@@ -36,5 +36,5 @@
|
|||||||
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
"contactErrorDescription": "An error occurred while sending your message. Please try again later",
|
||||||
"footerDescription": "Here you can add a description about your company or product",
|
"footerDescription": "Here you can add a description about your company or product",
|
||||||
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
|
||||||
"heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest"
|
"heroSubtitle": "A simple, convenient, and quick overview of your health condition"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Employees"
|
"pageTitle": "Members"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Billing"
|
||||||
@@ -23,17 +23,17 @@
|
|||||||
"creatingTeam": "Creating Company...",
|
"creatingTeam": "Creating Company...",
|
||||||
"personalAccount": "Personal Account",
|
"personalAccount": "Personal Account",
|
||||||
"searchAccount": "Search Account...",
|
"searchAccount": "Search Account...",
|
||||||
"membersTabLabel": "Employees",
|
"membersTabLabel": "Members",
|
||||||
"memberName": "Name",
|
"memberName": "Name",
|
||||||
"youLabel": "You",
|
"youLabel": "You",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"roleLabel": "Role",
|
"roleLabel": "Role",
|
||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Admin",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited at",
|
"invitedAtLabel": "Invited at",
|
||||||
"inviteMembersPageSubheading": "Invite employees to your Company",
|
"inviteMembersPageSubheading": "Invite members to your Company",
|
||||||
"createTeamModalHeading": "Create Company",
|
"createTeamModalHeading": "Create Company",
|
||||||
"createTeamModalDescription": "Create a new Company to manage your projects and employees.",
|
"createTeamModalDescription": "Create a new Company to manage your projects and members.",
|
||||||
"teamNameLabel": "Company Name",
|
"teamNameLabel": "Company Name",
|
||||||
"teamNameDescription": "Your company name should be unique and descriptive",
|
"teamNameDescription": "Your company name should be unique and descriptive",
|
||||||
"createTeamSubmitLabel": "Create Company",
|
"createTeamSubmitLabel": "Create Company",
|
||||||
@@ -44,34 +44,34 @@
|
|||||||
"createTeamDropdownLabel": "New company",
|
"createTeamDropdownLabel": "New company",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove from Account",
|
"removeMember": "Remove from Account",
|
||||||
"inviteMembersSuccess": "Employees invited successfully!",
|
"inviteMembersSuccess": "Members invited successfully!",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
||||||
"inviteMembersLoading": "Inviting employees...",
|
"inviteMembersLoading": "Inviting members...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"removeInviteButtonLabel": "Remove invite",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"addAnotherMemberButtonLabel": "Add another one",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"inviteMembersButtonLabel": "Send Invites",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"removeMemberModalHeading": "You are removing this user",
|
||||||
"removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.",
|
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.",
|
||||||
"removeMemberSuccessMessage": "Employee removed successfully",
|
"removeMemberSuccessMessage": "Member removed successfully",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
||||||
"removeMemberLoadingMessage": "Removing employee...",
|
"removeMemberLoadingMessage": "Removing member...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Company",
|
"removeMemberSubmitLabel": "Remove User from Company",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRole": "Update Role",
|
"updateRole": "Update Role",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
||||||
"updateMemberRoleModalHeading": "Update Employee's Role",
|
"updateMemberRoleModalHeading": "Update Member's Role",
|
||||||
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
||||||
"roleMustBeDifferent": "Role must be different from the current one",
|
"roleMustBeDifferent": "Role must be different from the current one",
|
||||||
"memberRoleInputLabel": "Member role",
|
"memberRoleInputLabel": "Member role",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"updateRoleDescription": "Pick a role for this member.",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"updateRoleSubmitLabel": "Update Role",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"transferOwnership": "Transfer Ownership",
|
||||||
"transferOwnershipDescription": "Transfer ownership of the company account to another employee.",
|
"transferOwnershipDescription": "Transfer ownership of the company account to another member.",
|
||||||
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
||||||
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the company account.",
|
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.",
|
||||||
"deleteInvitation": "Delete Invitation",
|
"deleteInvitation": "Delete Invitation",
|
||||||
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
||||||
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
||||||
@@ -92,21 +92,21 @@
|
|||||||
"teamLogoInputHeading": "Upload your company's Logo",
|
"teamLogoInputHeading": "Upload your company's Logo",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
||||||
"updateTeamSubmitLabel": "Update Company",
|
"updateTeamSubmitLabel": "Update Company",
|
||||||
"inviteMembersHeading": "Invite Employees to your Company",
|
"inviteMembersHeading": "Invite Members to your Company",
|
||||||
"inviteMembersDescription": "Invite employees to your company by entering their email and role.",
|
"inviteMembersDescription": "Invite member to your company by entering their email and role.",
|
||||||
"emailPlaceholder": "employee@email.com",
|
"emailPlaceholder": "member@email.com",
|
||||||
"membersPageHeading": "Employees",
|
"membersPageHeading": "Members",
|
||||||
"inviteMembersButton": "Invite Employees",
|
"inviteMembersButton": "Invite Members",
|
||||||
"invitingMembers": "Inviting employees...",
|
"invitingMembers": "Inviting members...",
|
||||||
"inviteMembersSuccessMessage": "Employees invited successfully",
|
"inviteMembersSuccessMessage": "Members invited successfully",
|
||||||
"inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.",
|
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"pendingInvitesHeading": "Pending Invites",
|
||||||
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
||||||
"noPendingInvites": "No pending invites found",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading employees...",
|
"loadingMembers": "Loading members...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your company's employees.",
|
"loadMembersError": "Sorry, we couldn't fetch your company's members.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.",
|
||||||
"loadingInvitedMembers": "Loading invited employees...",
|
"loadingInvitedMembers": "Loading invited members...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
@@ -126,13 +126,13 @@
|
|||||||
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
||||||
"searchMembersPlaceholder": "Search employees",
|
"searchMembersPlaceholder": "Search members",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
||||||
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.",
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
||||||
"searchInvitations": "Search Invitations",
|
"searchInvitations": "Search Invitations",
|
||||||
"updateInvitation": "Update Invitation",
|
"updateInvitation": "Update Invitation",
|
||||||
"removeInvitation": "Remove Invitation",
|
"removeInvitation": "Remove Invitation",
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inviteStatus": "Status",
|
"inviteStatus": "Status",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"inviteNotFoundOrExpired": "Invite not found or expired",
|
||||||
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company HR to renew the invite.",
|
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.",
|
||||||
"backToHome": "Back to Home",
|
"backToHome": "Back to Home",
|
||||||
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
||||||
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
||||||
@@ -159,5 +159,6 @@
|
|||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
||||||
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
|
||||||
|
"personalCode": "Isikukood"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
||||||
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
||||||
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
||||||
"noPermissionsAlertBody": "Please contact your account owner to change the billing settings for your account.",
|
"noPermissionsAlertBody": "Please contact your account admin to change the billing settings for your account.",
|
||||||
"checkoutSuccessTitle": "Done! You're all set.",
|
"checkoutSuccessTitle": "Done! You're all set.",
|
||||||
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
||||||
"checkoutSuccessBackButton": "Proceed to App",
|
"checkoutSuccessBackButton": "Proceed to App",
|
||||||
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
"cannotManageBillingAlertTitle": "You cannot manage billing",
|
||||||
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account owner.",
|
"cannotManageBillingAlertDescription": "You do not have permissions to manage billing. Please contact your account admin.",
|
||||||
"manageTeamPlan": "Manage your Company Plan",
|
"manageTeamPlan": "Manage your Company Plan",
|
||||||
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
"manageTeamPlanDescription": "Choose a plan that fits your company's needs. You can upgrade or downgrade your plan at any time.",
|
||||||
"basePlan": "Base Plan",
|
"basePlan": "Base Plan",
|
||||||
@@ -34,9 +34,9 @@
|
|||||||
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
"redirectingToPayment": "Redirecting to checkout. Please wait...",
|
||||||
"proceedToPayment": "Proceed to Payment",
|
"proceedToPayment": "Proceed to Payment",
|
||||||
"startTrial": "Start Trial",
|
"startTrial": "Start Trial",
|
||||||
"perTeamMember": "Per company employee",
|
"perTeamMember": "Per company member",
|
||||||
"perUnit": "Per {{unit}} usage",
|
"perUnit": "Per {{unit}} usage",
|
||||||
"teamMembers": "Company Employees",
|
"teamMembers": "Company Members",
|
||||||
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
"includedUpTo": "Up to {{upTo}} {{unit}} included in the plan",
|
||||||
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
"fromPreviousTierUpTo": "for each {{unit}} for the next {{ upTo }} {{ unit }}",
|
||||||
"andAbove": "above {{ previousTier }} {{ unit }}",
|
"andAbove": "above {{ previousTier }} {{ unit }}",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"homeTabLabel": "Home",
|
"homeTabLabel": "Home",
|
||||||
"homeTabDescription": "Welcome to your home page",
|
"homeTabDescription": "Welcome to your home page",
|
||||||
"accountMembers": "Company Employees",
|
"accountMembers": "Company Members",
|
||||||
"membersTabDescription": "Here you can manage the employees of your company.",
|
"membersTabDescription": "Here you can manage the members of your company.",
|
||||||
"billingTabLabel": "Billing",
|
"billingTabLabel": "Billing",
|
||||||
"billingTabDescription": "Manage your billing and subscription",
|
"billingTabDescription": "Manage your billing and subscription",
|
||||||
"dashboardTabLabel": "Dashboard",
|
"dashboardTabLabel": "Dashboard",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"account": "Account",
|
"account": "Account",
|
||||||
"members": "Employees",
|
"members": "Members",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"owner": {
|
"owner": {
|
||||||
"label": "Owner"
|
"label": "Admin"
|
||||||
},
|
},
|
||||||
"member": {
|
"member": {
|
||||||
"label": "Employee"
|
"label": "Member"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"otp": {
|
"otp": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||||
},
|
},
|
||||||
"members": {
|
"members": {
|
||||||
"pageTitle": "Employees"
|
"pageTitle": "Members"
|
||||||
},
|
},
|
||||||
"billing": {
|
"billing": {
|
||||||
"pageTitle": "Billing"
|
"pageTitle": "Billing"
|
||||||
@@ -23,17 +23,17 @@
|
|||||||
"creatingTeam": "Creating Company...",
|
"creatingTeam": "Creating Company...",
|
||||||
"personalAccount": "Personal Account",
|
"personalAccount": "Personal Account",
|
||||||
"searchAccount": "Search Account...",
|
"searchAccount": "Search Account...",
|
||||||
"membersTabLabel": "Employees",
|
"membersTabLabel": "Members",
|
||||||
"memberName": "Name",
|
"memberName": "Name",
|
||||||
"youLabel": "You",
|
"youLabel": "You",
|
||||||
"emailLabel": "Email",
|
"emailLabel": "Email",
|
||||||
"roleLabel": "Role",
|
"roleLabel": "Role",
|
||||||
"primaryOwnerLabel": "Primary Owner",
|
"primaryOwnerLabel": "Primary Admin",
|
||||||
"joinedAtLabel": "Joined at",
|
"joinedAtLabel": "Joined at",
|
||||||
"invitedAtLabel": "Invited at",
|
"invitedAtLabel": "Invited at",
|
||||||
"inviteMembersPageSubheading": "Invite employees to your Company",
|
"inviteMembersPageSubheading": "Invite members to your Company",
|
||||||
"createTeamModalHeading": "Create Company",
|
"createTeamModalHeading": "Create Company",
|
||||||
"createTeamModalDescription": "Create a new Company to manage your projects and employees.",
|
"createTeamModalDescription": "Create a new Company to manage your projects and members.",
|
||||||
"teamNameLabel": "Company Name",
|
"teamNameLabel": "Company Name",
|
||||||
"teamNameDescription": "Your company name should be unique and descriptive",
|
"teamNameDescription": "Your company name should be unique and descriptive",
|
||||||
"createTeamSubmitLabel": "Create Company",
|
"createTeamSubmitLabel": "Create Company",
|
||||||
@@ -44,34 +44,34 @@
|
|||||||
"createTeamDropdownLabel": "New company",
|
"createTeamDropdownLabel": "New company",
|
||||||
"changeRole": "Change Role",
|
"changeRole": "Change Role",
|
||||||
"removeMember": "Remove from Account",
|
"removeMember": "Remove from Account",
|
||||||
"inviteMembersSuccess": "Employees invited successfully!",
|
"inviteMembersSuccess": "Members invited successfully!",
|
||||||
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
"inviteMembersError": "Sorry, we encountered an error! Please try again",
|
||||||
"inviteMembersLoading": "Inviting employees...",
|
"inviteMembersLoading": "Inviting members...",
|
||||||
"removeInviteButtonLabel": "Remove invite",
|
"removeInviteButtonLabel": "Remove invite",
|
||||||
"addAnotherMemberButtonLabel": "Add another one",
|
"addAnotherMemberButtonLabel": "Add another one",
|
||||||
"inviteMembersButtonLabel": "Send Invites",
|
"inviteMembersButtonLabel": "Send Invites",
|
||||||
"removeMemberModalHeading": "You are removing this user",
|
"removeMemberModalHeading": "You are removing this user",
|
||||||
"removeMemberModalDescription": "Remove this employee from the company. They will no longer have access to the company.",
|
"removeMemberModalDescription": "Remove this member from the company. They will no longer have access to the company.",
|
||||||
"removeMemberSuccessMessage": "Employee removed successfully",
|
"removeMemberSuccessMessage": "Member removed successfully",
|
||||||
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
"removeMemberErrorMessage": "Sorry, we encountered an error. Please try again",
|
||||||
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected employee.",
|
"removeMemberErrorHeading": "Sorry, we couldn't remove the selected member.",
|
||||||
"removeMemberLoadingMessage": "Removing employee...",
|
"removeMemberLoadingMessage": "Removing member...",
|
||||||
"removeMemberSubmitLabel": "Remove User from Company",
|
"removeMemberSubmitLabel": "Remove User from Company",
|
||||||
"chooseDifferentRoleError": "Role is the same as the current one",
|
"chooseDifferentRoleError": "Role is the same as the current one",
|
||||||
"updateRole": "Update Role",
|
"updateRole": "Update Role",
|
||||||
"updateRoleLoadingMessage": "Updating role...",
|
"updateRoleLoadingMessage": "Updating role...",
|
||||||
"updateRoleSuccessMessage": "Role updated successfully",
|
"updateRoleSuccessMessage": "Role updated successfully",
|
||||||
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
"updatingRoleErrorMessage": "Sorry, we encountered an error. Please try again.",
|
||||||
"updateMemberRoleModalHeading": "Update Employee's Role",
|
"updateMemberRoleModalHeading": "Update Member's Role",
|
||||||
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
"updateMemberRoleModalDescription": "Change the role of the selected member. The role determines the permissions of the member.",
|
||||||
"roleMustBeDifferent": "Role must be different from the current one",
|
"roleMustBeDifferent": "Role must be different from the current one",
|
||||||
"memberRoleInputLabel": "Member role",
|
"memberRoleInputLabel": "Member role",
|
||||||
"updateRoleDescription": "Pick a role for this member.",
|
"updateRoleDescription": "Pick a role for this member.",
|
||||||
"updateRoleSubmitLabel": "Update Role",
|
"updateRoleSubmitLabel": "Update Role",
|
||||||
"transferOwnership": "Transfer Ownership",
|
"transferOwnership": "Transfer Ownership",
|
||||||
"transferOwnershipDescription": "Transfer ownership of the company account to another employee.",
|
"transferOwnershipDescription": "Transfer ownership of the company account to another member.",
|
||||||
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
"transferOwnershipInputLabel": "Please type TRANSFER to confirm the transfer of ownership.",
|
||||||
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary owner of the company account.",
|
"transferOwnershipInputDescription": "By transferring ownership, you will no longer be the primary admin of the company account.",
|
||||||
"deleteInvitation": "Delete Invitation",
|
"deleteInvitation": "Delete Invitation",
|
||||||
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
"deleteInvitationDialogDescription": "You are about to delete the invitation. The user will no longer be able to join the company account.",
|
||||||
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
"deleteInviteSuccessMessage": "Invite deleted successfully",
|
||||||
@@ -92,21 +92,21 @@
|
|||||||
"teamLogoInputHeading": "Upload your company's Logo",
|
"teamLogoInputHeading": "Upload your company's Logo",
|
||||||
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
"teamLogoInputSubheading": "Please choose a photo to upload as your company logo.",
|
||||||
"updateTeamSubmitLabel": "Update Company",
|
"updateTeamSubmitLabel": "Update Company",
|
||||||
"inviteMembersHeading": "Invite Employees to your Company",
|
"inviteMembersHeading": "Invite Members to your Company",
|
||||||
"inviteMembersDescription": "Invite employees to your company by entering their email and role.",
|
"inviteMembersDescription": "Invite member to your company by entering their email and role.",
|
||||||
"emailPlaceholder": "employee@email.com",
|
"emailPlaceholder": "member@email.com",
|
||||||
"membersPageHeading": "Employees",
|
"membersPageHeading": "Members",
|
||||||
"inviteMembersButton": "Invite Employees",
|
"inviteMembersButton": "Invite Members",
|
||||||
"invitingMembers": "Inviting employees...",
|
"invitingMembers": "Inviting members...",
|
||||||
"inviteMembersSuccessMessage": "Employees invited successfully",
|
"inviteMembersSuccessMessage": "Members invited successfully",
|
||||||
"inviteMembersErrorMessage": "Sorry, employees could not be invited. Please try again.",
|
"inviteMembersErrorMessage": "Sorry, members could not be invited. Please try again.",
|
||||||
"pendingInvitesHeading": "Pending Invites",
|
"pendingInvitesHeading": "Pending Invites",
|
||||||
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
"pendingInvitesDescription": " Here you can manage the pending invitations to your company.",
|
||||||
"noPendingInvites": "No pending invites found",
|
"noPendingInvites": "No pending invites found",
|
||||||
"loadingMembers": "Loading employees...",
|
"loadingMembers": "Loading members...",
|
||||||
"loadMembersError": "Sorry, we couldn't fetch your company's employees.",
|
"loadMembersError": "Sorry, we couldn't fetch your company's members.",
|
||||||
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited employees.",
|
"loadInvitedMembersError": "Sorry, we couldn't fetch your company's invited members.",
|
||||||
"loadingInvitedMembers": "Loading invited employees...",
|
"loadingInvitedMembers": "Loading invited members...",
|
||||||
"invitedBadge": "Invited",
|
"invitedBadge": "Invited",
|
||||||
"duplicateInviteEmailError": "You have already entered this email address",
|
"duplicateInviteEmailError": "You have already entered this email address",
|
||||||
"invitingOwnAccountError": "Hey, that's your email!",
|
"invitingOwnAccountError": "Hey, that's your email!",
|
||||||
@@ -126,13 +126,13 @@
|
|||||||
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
"leaveTeamDisclaimer": "You are leaving the company {{ teamName }}. You will no longer have access to it.",
|
||||||
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
"deleteTeamErrorHeading": "Sorry, we couldn't delete your company.",
|
||||||
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
"leaveTeamErrorHeading": "Sorry, we couldn't leave your company.",
|
||||||
"searchMembersPlaceholder": "Search employees",
|
"searchMembersPlaceholder": "Search members",
|
||||||
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
"createTeamErrorHeading": "Sorry, we couldn't create your company.",
|
||||||
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
"createTeamErrorMessage": "We encountered an error creating your company. Please try again.",
|
||||||
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
"transferTeamErrorHeading": "Sorry, we couldn't transfer ownership of your company account.",
|
||||||
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
"transferTeamErrorMessage": "We encountered an error transferring ownership of your company account. Please try again.",
|
||||||
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected employee.",
|
"updateRoleErrorHeading": "Sorry, we couldn't update the role of the selected member.",
|
||||||
"updateRoleErrorMessage": "We encountered an error updating the role of the selected employee. Please try again.",
|
"updateRoleErrorMessage": "We encountered an error updating the role of the selected member. Please try again.",
|
||||||
"searchInvitations": "Search Invitations",
|
"searchInvitations": "Search Invitations",
|
||||||
"updateInvitation": "Update Invitation",
|
"updateInvitation": "Update Invitation",
|
||||||
"removeInvitation": "Remove Invitation",
|
"removeInvitation": "Remove Invitation",
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"active": "Active",
|
"active": "Active",
|
||||||
"inviteStatus": "Status",
|
"inviteStatus": "Status",
|
||||||
"inviteNotFoundOrExpired": "Invite not found or expired",
|
"inviteNotFoundOrExpired": "Invite not found or expired",
|
||||||
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company HR to renew the invite.",
|
"inviteNotFoundOrExpiredDescription": "The invite you are looking for is either expired or does not exist. Please contact the company admin to renew the invite.",
|
||||||
"backToHome": "Back to Home",
|
"backToHome": "Back to Home",
|
||||||
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
"renewInvitationDialogDescription": "You are about to renew the invitation to {{ email }}. The user will be able to join the company.",
|
||||||
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
"renewInvitationErrorTitle": "Sorry, we couldn't renew the invitation.",
|
||||||
@@ -159,5 +159,6 @@
|
|||||||
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
"leaveTeamInputLabel": "Please type LEAVE to confirm leaving the company.",
|
||||||
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
"leaveTeamInputDescription": "By leaving the company, you will no longer have access to it.",
|
||||||
"reservedNameError": "This name is reserved. Please choose a different one.",
|
"reservedNameError": "This name is reserved. Please choose a different one.",
|
||||||
"specialCharactersError": "This name cannot contain special characters. Please choose a different one."
|
"specialCharactersError": "This name cannot contain special characters. Please choose a different one.",
|
||||||
|
"personalCode": "Личный код"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
alter table public.accounts
|
||||||
|
add column if not exists personal_code char(11) unique;
|
||||||
|
|
||||||
|
alter table public.invitations
|
||||||
|
add column if not exists personal_code char(11) unique;
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
drop function if exists public.add_invitations_to_account(text, public.invitation[]);
|
||||||
|
|
||||||
|
drop type if exists public.invitation;
|
||||||
|
|
||||||
|
create type public.invitation as (
|
||||||
|
email text,
|
||||||
|
role text,
|
||||||
|
personal_code char(11)
|
||||||
|
);
|
||||||
|
|
||||||
|
create or replace function public.add_invitations_to_account (
|
||||||
|
account_slug text,
|
||||||
|
invitations public.invitation[]
|
||||||
|
) returns public.invitations[]
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
new_invitation public.invitations;
|
||||||
|
all_invitations public.invitations[] := array[]::public.invitations[];
|
||||||
|
invite_token text;
|
||||||
|
invite public.invitation;
|
||||||
|
begin
|
||||||
|
foreach invite in array invitations loop
|
||||||
|
invite_token := extensions.uuid_generate_v4();
|
||||||
|
|
||||||
|
insert into public.invitations (
|
||||||
|
email,
|
||||||
|
account_id,
|
||||||
|
invited_by,
|
||||||
|
role,
|
||||||
|
invite_token,
|
||||||
|
personal_code
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
invite.email,
|
||||||
|
(select id from public.accounts where slug = account_slug),
|
||||||
|
auth.uid(),
|
||||||
|
invite.role,
|
||||||
|
invite_token,
|
||||||
|
invite.personal_code
|
||||||
|
)
|
||||||
|
returning * into new_invitation;
|
||||||
|
|
||||||
|
all_invitations := array_append(all_invitations, new_invitation);
|
||||||
|
end loop;
|
||||||
|
|
||||||
|
return all_invitations;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
grant execute on function public.add_invitations_to_account(text, public.invitation[]) to authenticated;
|
||||||
|
|
||||||
|
drop function if exists public.get_account_invitations(text);
|
||||||
|
|
||||||
|
create function public.get_account_invitations (account_slug text)
|
||||||
|
returns table (
|
||||||
|
id integer,
|
||||||
|
email varchar(255),
|
||||||
|
account_id uuid,
|
||||||
|
invited_by uuid,
|
||||||
|
role varchar(50),
|
||||||
|
created_at timestamptz,
|
||||||
|
updated_at timestamptz,
|
||||||
|
expires_at timestamptz,
|
||||||
|
personal_code char(11),
|
||||||
|
inviter_name varchar,
|
||||||
|
inviter_email varchar
|
||||||
|
)
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
begin
|
||||||
|
return query
|
||||||
|
select
|
||||||
|
invitation.id,
|
||||||
|
invitation.email,
|
||||||
|
invitation.account_id,
|
||||||
|
invitation.invited_by,
|
||||||
|
invitation.role,
|
||||||
|
invitation.created_at,
|
||||||
|
invitation.updated_at,
|
||||||
|
invitation.expires_at,
|
||||||
|
invitation.personal_code,
|
||||||
|
account.name,
|
||||||
|
account.email
|
||||||
|
from
|
||||||
|
public.invitations as invitation
|
||||||
|
join public.accounts as account on invitation.account_id = account.id
|
||||||
|
where
|
||||||
|
account.slug = account_slug;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
grant execute on function public.get_account_invitations(text) to authenticated, service_role;
|
||||||
|
|
||||||
|
|
||||||
|
drop function if exists public.get_account_members(text);
|
||||||
|
|
||||||
|
-- Functions "public.get_account_members"
|
||||||
|
-- Function to get the members of an account by the account slug
|
||||||
|
create
|
||||||
|
or replace function public.get_account_members (account_slug text) returns table (
|
||||||
|
id uuid,
|
||||||
|
user_id uuid,
|
||||||
|
account_id uuid,
|
||||||
|
role varchar(50),
|
||||||
|
role_hierarchy_level int,
|
||||||
|
primary_owner_user_id uuid,
|
||||||
|
name varchar,
|
||||||
|
email varchar,
|
||||||
|
personal_code char(11),
|
||||||
|
picture_url varchar,
|
||||||
|
created_at timestamptz,
|
||||||
|
updated_at timestamptz
|
||||||
|
) language plpgsql
|
||||||
|
set
|
||||||
|
search_path = '' as $$
|
||||||
|
begin
|
||||||
|
return QUERY
|
||||||
|
select
|
||||||
|
acc.id,
|
||||||
|
am.user_id,
|
||||||
|
am.account_id,
|
||||||
|
am.account_role,
|
||||||
|
r.hierarchy_level,
|
||||||
|
a.primary_owner_user_id,
|
||||||
|
acc.name,
|
||||||
|
acc.email,
|
||||||
|
acc.personal_code,
|
||||||
|
acc.picture_url,
|
||||||
|
am.created_at,
|
||||||
|
am.updated_at
|
||||||
|
from
|
||||||
|
public.accounts_memberships am
|
||||||
|
join public.accounts a on a.id = am.account_id
|
||||||
|
join public.accounts acc on acc.id = am.user_id
|
||||||
|
join public.roles r on r.name = am.account_role
|
||||||
|
where
|
||||||
|
a.slug = account_slug;
|
||||||
|
|
||||||
|
end;
|
||||||
|
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant
|
||||||
|
execute on function public.get_account_members (text) to authenticated,
|
||||||
|
service_role;
|
||||||
22
supabase/migrations/20250620191934_is_company_admin.sql
Normal file
22
supabase/migrations/20250620191934_is_company_admin.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
create or replace function public.is_company_admin(account_slug text)
|
||||||
|
returns boolean
|
||||||
|
set search_path = ''
|
||||||
|
language plpgsql
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
is_owner boolean;
|
||||||
|
begin
|
||||||
|
select exists (
|
||||||
|
select 1
|
||||||
|
from public.accounts_memberships am
|
||||||
|
join public.accounts a on a.id = am.account_id
|
||||||
|
where am.user_id = auth.uid()
|
||||||
|
and am.account_role = 'owner'
|
||||||
|
and a.slug = account_slug
|
||||||
|
) into is_owner;
|
||||||
|
|
||||||
|
return is_owner;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
grant execute on function public.is_company_admin(text) to authenticated, service_role;
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
create or replace function kit.setup_new_user()
|
||||||
|
returns trigger
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = ''
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
user_name text;
|
||||||
|
picture_url text;
|
||||||
|
personal_code text;
|
||||||
|
begin
|
||||||
|
if new.raw_user_meta_data ->> 'name' is not null then
|
||||||
|
user_name := new.raw_user_meta_data ->> 'name';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if user_name is null and new.email is not null then
|
||||||
|
user_name := split_part(new.email, '@', 1);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if user_name is null then
|
||||||
|
user_name := '';
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if new.raw_user_meta_data ->> 'avatar_url' is not null then
|
||||||
|
picture_url := new.raw_user_meta_data ->> 'avatar_url';
|
||||||
|
else
|
||||||
|
picture_url := null;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
personal_code := new.raw_user_meta_data ->> 'personalCode';
|
||||||
|
|
||||||
|
insert into public.accounts (
|
||||||
|
id,
|
||||||
|
primary_owner_user_id,
|
||||||
|
name,
|
||||||
|
is_personal_account,
|
||||||
|
picture_url,
|
||||||
|
email,
|
||||||
|
personal_code
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
new.id,
|
||||||
|
new.id,
|
||||||
|
user_name,
|
||||||
|
true,
|
||||||
|
picture_url,
|
||||||
|
new.email,
|
||||||
|
personal_code
|
||||||
|
);
|
||||||
|
|
||||||
|
return new;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
create or replace function public.get_invitations_with_account_ids(
|
||||||
|
company_id uuid,
|
||||||
|
personal_codes text[]
|
||||||
|
)
|
||||||
|
returns table (
|
||||||
|
invite_token text,
|
||||||
|
personal_code text,
|
||||||
|
account_id uuid
|
||||||
|
)
|
||||||
|
language sql
|
||||||
|
as $$
|
||||||
|
select
|
||||||
|
i.invite_token,
|
||||||
|
i.personal_code,
|
||||||
|
a.id as account_id
|
||||||
|
from public.invitations i
|
||||||
|
join public.accounts a on a.personal_code = i.personal_code
|
||||||
|
where i.account_id = company_id
|
||||||
|
and i.personal_code = any(personal_codes);
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user