diff --git a/.env b/.env index 164e333..f279691 100644 --- a/.env +++ b/.env @@ -40,6 +40,8 @@ NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS=true NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION=true NEXT_PUBLIC_LANGUAGE_PRIORITY=application NEXT_PUBLIC_ENABLE_NOTIFICATIONS=true +NEXT_PUBLIC_REALTIME_NOTIFICATIONS=true + # NEXTJS NEXT_TELEMETRY_DISABLED=1 diff --git a/app/admin/accounts/page.tsx b/app/admin/accounts/page.tsx index 21c1104..688987f 100644 --- a/app/admin/accounts/page.tsx +++ b/app/admin/accounts/page.tsx @@ -55,7 +55,7 @@ async function AccountsPage(props: AdminAccountsPageProps) { } if (query) { - queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%`); + queryBuilder.or(`name.ilike.%${query}%,email.ilike.%${query}%,personal_code.ilike.%${query}%`); } return queryBuilder; diff --git a/app/home/(user)/_components/home-menu-navigation.tsx b/app/home/(user)/_components/home-menu-navigation.tsx index 644c3cc..96ad1c9 100644 --- a/app/home/(user)/_components/home-menu-navigation.tsx +++ b/app/home/(user)/_components/home-menu-navigation.tsx @@ -1,6 +1,4 @@ -import { ShoppingCart } from 'lucide-react'; -import { Button } from '@kit/ui/button'; import { Trans } from '@kit/ui/trans'; import { AppLogo } from '~/components/app-logo'; @@ -11,14 +9,17 @@ import { SIDEBAR_WIDTH } from '../../../../packages/ui/src/shadcn/constants'; // home imports import { UserNotifications } from '../_components/user-notifications'; import { type UserWorkspace } from '../_lib/server/load-user-workspace'; +import { Button } from '@kit/ui/button'; +import { ShoppingCart } from 'lucide-react'; export function HomeMenuNavigation(props: { workspace: UserWorkspace }) { - const { workspace, user } = props.workspace; + const { workspace, user, accounts } = props.workspace; return (
+
- + @@ -37,6 +41,7 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) { user={user} account={workspace} showProfileName + accounts={accounts} />
diff --git a/app/home/(user)/_lib/server/load-user-workspace.ts b/app/home/(user)/_lib/server/load-user-workspace.ts index b48e37d..2a9db62 100644 --- a/app/home/(user)/_lib/server/load-user-workspace.ts +++ b/app/home/(user)/_lib/server/load-user-workspace.ts @@ -28,15 +28,20 @@ async function workspaceLoader() { 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(), workspacePromise, requireUserInServerComponent(), + tempAccountsPromise() ]); return { accounts, workspace, user, + tempVisibleAccounts }; } diff --git a/app/home/(user)/page.tsx b/app/home/(user)/page.tsx index cc64e11..6a91bba 100644 --- a/app/home/(user)/page.tsx +++ b/app/home/(user)/page.tsx @@ -1,4 +1,3 @@ -import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; @@ -7,6 +6,9 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import Dashboard from './_components/dashboard'; // local imports import { HomeLayoutPageHeader } from './_components/home-page-header'; +import { use } from 'react'; +import { loadUserWorkspace } from './_lib/server/load-user-workspace'; +import { PageBody } from '@kit/ui/page'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -18,6 +20,7 @@ export const generateMetadata = async () => { }; function UserHomePage() { + const { tempVisibleAccounts } = use(loadUserWorkspace()); return ( <> rawAccounts.map((account) => ({ + label: account.name, + value: account.slug, + image: account.picture_url, + })),[rawAccounts]) const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce< Array<{ @@ -40,34 +46,16 @@ export function TeamAccountNavigationMenu(props: {
- - - {routes.map((route) => ( - - ))} -
-
+
- - ({ - label: account.name, - value: account.slug, - image: account.picture_url, - }))} + - -
- -
); diff --git a/app/home/[account]/_lib/server/team-account-workspace.loader.ts b/app/home/[account]/_lib/server/team-account-workspace.loader.ts index 4a38994..7ca21ed 100644 --- a/app/home/[account]/_lib/server/team-account-workspace.loader.ts +++ b/app/home/[account]/_lib/server/team-account-workspace.loader.ts @@ -28,12 +28,8 @@ export const loadTeamWorkspace = cache(workspaceLoader); async function workspaceLoader(accountSlug: string) { const client = getSupabaseServerClient(); const api = createTeamAccountsApi(client); - - const [workspace, user] = await Promise.all([ - api.getAccountWorkspace(accountSlug), - requireUserInServerComponent(), - ]); - + const user = await requireUserInServerComponent(); + const workspace = await api.getAccountWorkspace(accountSlug, user.id); // we cannot find any record for the selected account // so we redirect the user to the home page if (!workspace.data?.account) { @@ -42,6 +38,7 @@ async function workspaceLoader(accountSlug: string) { return { ...workspace.data, + accounts: workspace.data.accounts.map(({ user_accounts }) => ({...user_accounts})), user, }; } diff --git a/app/home/[account]/layout.tsx b/app/home/[account]/layout.tsx index ffefb5f..f597a3b 100644 --- a/app/home/[account]/layout.tsx +++ b/app/home/[account]/layout.tsx @@ -4,7 +4,7 @@ import { cookies } from 'next/headers'; 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 { SidebarProvider } from '@kit/ui/shadcn-sidebar'; @@ -113,7 +113,32 @@ function HeaderLayout({
- {children} + + + + + + + + + +
+ +
+
+ + {children} +
+
); @@ -144,4 +169,4 @@ async function getLayoutState(account: string) { }; } -export default withI18n(TeamWorkspaceLayout); +export default withI18n(CompanyGuard(TeamWorkspaceLayout)); diff --git a/app/home/[account]/page.tsx b/app/home/[account]/page.tsx index 982f21a..4684ea3 100644 --- a/app/home/[account]/page.tsx +++ b/app/home/[account]/page.tsx @@ -9,6 +9,7 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import { DashboardDemo } from './_components/dashboard-demo'; import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header'; +import { CompanyGuard } from '@/packages/features/team-accounts/src/components'; interface TeamAccountHomePageProps { params: Promise<{ account: string }>; @@ -41,4 +42,4 @@ function TeamAccountHomePage({ params }: TeamAccountHomePageProps) { ); } -export default withI18n(TeamAccountHomePage); +export default withI18n(CompanyGuard(TeamAccountHomePage)); diff --git a/app/join/page.tsx b/app/join/page.tsx index 6fa5c77..ffe4674 100644 --- a/app/join/page.tsx +++ b/app/join/page.tsx @@ -109,10 +109,7 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) { const signOutNext = `${pathsConfig.auth.signIn}?invite_token=${token}`; // once the user accepts the invitation, we redirect them to the account home page - const accountHome = pathsConfig.app.accountHome.replace( - '[account]', - invitation.account.slug, - ); + const accountHome = pathsConfig.app.home; const email = auth.data.email ?? ''; diff --git a/components/personal-account-dropdown-container.tsx b/components/personal-account-dropdown-container.tsx index 50b28bc..4e94a8f 100644 --- a/components/personal-account-dropdown-container.tsx +++ b/components/personal-account-dropdown-container.tsx @@ -26,6 +26,11 @@ export function ProfileAccountDropdownContainer(props: { name: string | null; picture_url: string | null; }; + accounts: { + label: string | null; + value: string | null; + image?: string | null; +}[] }) { const signOut = useSignOut(); const user = useUser(props.user); @@ -42,6 +47,7 @@ export function ProfileAccountDropdownContainer(props: { features={features} user={userData} account={props.account} + accounts={props.accounts} signOutRequested={() => signOut.mutateAsync()} showProfileName={props.showProfileName} /> diff --git a/packages/features/accounts/src/components/account-selector.tsx b/packages/features/accounts/src/components/account-selector.tsx index 909a929..f9cee97 100644 --- a/packages/features/accounts/src/components/account-selector.tsx +++ b/packages/features/accounts/src/components/account-selector.tsx @@ -112,10 +112,10 @@ export function AccountSelector({ role="combobox" aria-expanded={open} className={cn( - 'dark:shadow-primary/10 group w-full min-w-0 px-2 lg:w-auto lg:max-w-fit', + 'dark:shadow-primary/10 group w-full min-w-0 px-4 py-2 h-10 border-1 lg:w-auto lg:max-w-fit', { 'justify-start': !collapsed, - 'm-auto justify-center px-2 lg:w-full': collapsed, + 'm-auto justify-center px-4 lg:w-full': collapsed, }, className, )} @@ -124,7 +124,7 @@ export function AccountSelector({ condition={selected} fallback={ - + + ); diff --git a/packages/features/accounts/src/components/personal-account-dropdown.tsx b/packages/features/accounts/src/components/personal-account-dropdown.tsx index ea2733d..7648a3f 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -10,7 +10,7 @@ import { ChevronsUpDown, Home, LogOut, - MessageCircleQuestion, + UserCircle, Shield, } from 'lucide-react'; @@ -28,6 +28,9 @@ import { Trans } from '@kit/ui/trans'; import { cn } from '@kit/ui/utils'; import { usePersonalAccountData } from '../hooks/use-personal-account-data'; +import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar'; + +const PERSONAL_ACCOUNT_SLUG = 'personal'; export function PersonalAccountDropdown({ className, @@ -37,6 +40,7 @@ export function PersonalAccountDropdown({ paths, features, account, + accounts = [] }: { user: User; @@ -45,7 +49,11 @@ export function PersonalAccountDropdown({ name: string | null; picture_url: string | null; }; - + accounts: { + label: string | null; + value: string | null; + image?: string | null; + }[]; signOutRequested: () => unknown; paths: { @@ -95,7 +103,7 @@ export function PersonalAccountDropdown({ className ?? '', { ['active:bg-secondary/50 items-center gap-4 rounded-md' + - ' hover:bg-secondary p-2 transition-colors']: showProfileName, + ' hover:bg-secondary m-0 transition-colors border-1 rounded-md px-4 py-1 h-10']: showProfileName, }, )} > @@ -119,12 +127,6 @@ export function PersonalAccountDropdown({ {displayName} - - {signedInAsLabel} - + 0}> + + + + + {accounts.map((account) => ( + + +
+ + + + + {account.label ? account.label[0] : ''} + + + + + {account.label} + +
+ +
+ ))} +
+ + + - + - + diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index 50b9ad4..35a4465 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -47,23 +47,61 @@ class AccountsApi { } /** - * @name loadUserAccounts - * Load the user accounts. - */ + * @name loadUserAccounts + * Load only user-owned accounts (not just memberships). + */ 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 - .from('user_accounts') - .select(`name, slug, picture_url`); + .from('accounts_memberships') + .select(` + account_id, + user_accounts ( + name, + slug, + picture_url + ) + `) + .eq('user_id', user.id) + .eq('account_role', 'owner'); if (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 { label: name, value: slug, - image: picture_url, }; }); } diff --git a/packages/features/admin/src/components/admin-account-page.tsx b/packages/features/admin/src/components/admin-account-page.tsx index 3fb2269..71f67bc 100644 --- a/packages/features/admin/src/components/admin-account-page.tsx +++ b/packages/features/admin/src/components/admin-account-page.tsx @@ -28,6 +28,12 @@ import { AdminMembersTable } from './admin-members-table'; import { AdminMembershipsTable } from './admin-memberships-table'; import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog'; +import { + AccountInvitationsTable, + AccountMembersTable, + InviteMembersDialogContainer, +} from '@kit/team-accounts/components'; + type Account = Tables<'accounts'>; type Membership = Tables<'accounts_memberships'>; @@ -146,8 +152,6 @@ async function PersonalAccountPage(props: { account: Account }) {
- -
Companies @@ -212,7 +216,7 @@ async function TeamAccountPage(props: {
- Company Employees + Company Members
diff --git a/packages/features/admin/src/components/admin-accounts-table.tsx b/packages/features/admin/src/components/admin-accounts-table.tsx index 06a14c7..a6ee29e 100644 --- a/packages/features/admin/src/components/admin-accounts-table.tsx +++ b/packages/features/admin/src/components/admin-accounts-table.tsx @@ -179,6 +179,14 @@ function getColumns(): ColumnDef[] { header: 'Email', accessorKey: 'email', }, + { + id: 'personalCode', + header: 'Personal Code', + accessorKey: 'personalCode', + cell: ({ row }) => { + return row.original.personal_code ?? '-'; + }, + }, { id: 'type', header: 'Type', diff --git a/packages/features/admin/src/components/admin-create-user-dialog.tsx b/packages/features/admin/src/components/admin-create-user-dialog.tsx index bc21c4a..5b4bffc 100644 --- a/packages/features/admin/src/components/admin-create-user-dialog.tsx +++ b/packages/features/admin/src/components/admin-create-user-dialog.tsx @@ -48,8 +48,9 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) { email: '', password: '', emailConfirm: false, + personalCode: '' }, - mode: 'onChange', + mode: 'onBlur', }); const onSubmit = (data: CreateUserSchemaType) => { @@ -98,6 +99,25 @@ export function AdminCreateUserDialog(props: React.PropsWithChildren) { + ( + + Personal code + + + + + + + )} + /> + ( diff --git a/packages/features/admin/src/components/admin-members-table.tsx b/packages/features/admin/src/components/admin-members-table.tsx index 8d256be..d970645 100644 --- a/packages/features/admin/src/components/admin-members-table.tsx +++ b/packages/features/admin/src/components/admin-members-table.tsx @@ -52,7 +52,7 @@ function getColumns(): ColumnDef[] { { header: 'Role', cell: ({ row }) => { - return row.original.role === 'owner' ? 'HR' : 'Employee'; + return row.original.role === 'owner' ? 'Admin' : 'Member'; }, }, { diff --git a/packages/features/admin/src/lib/server/admin-server-actions.ts b/packages/features/admin/src/lib/server/admin-server-actions.ts index cac9ae9..7da94b9 100644 --- a/packages/features/admin/src/lib/server/admin-server-actions.ts +++ b/packages/features/admin/src/lib/server/admin-server-actions.ts @@ -160,7 +160,7 @@ export const deleteAccountAction = adminAction( */ export const createUserAction = adminAction( enhanceAction( - async ({ email, password, emailConfirm }) => { + async ({ email, password, emailConfirm, personalCode }) => { const adminClient = getSupabaseServerAdminClient(); const logger = await getLogger(); @@ -182,6 +182,16 @@ export const createUserAction = adminAction( `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(); return { diff --git a/packages/features/admin/src/lib/server/schema/create-user-profile.schema.ts b/packages/features/admin/src/lib/server/schema/create-user-profile.schema.ts new file mode 100644 index 0000000..03e971d --- /dev/null +++ b/packages/features/admin/src/lib/server/schema/create-user-profile.schema.ts @@ -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; + diff --git a/packages/features/admin/src/lib/server/schema/create-user.schema.ts b/packages/features/admin/src/lib/server/schema/create-user.schema.ts index 586474f..9b05b4e 100644 --- a/packages/features/admin/src/lib/server/schema/create-user.schema.ts +++ b/packages/features/admin/src/lib/server/schema/create-user.schema.ts @@ -1,6 +1,9 @@ import { z } from 'zod'; 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' }), password: z .string() diff --git a/packages/features/auth/src/components/password-sign-up-form.tsx b/packages/features/auth/src/components/password-sign-up-form.tsx index dbba6ea..1b29b97 100644 --- a/packages/features/auth/src/components/password-sign-up-form.tsx +++ b/packages/features/auth/src/components/password-sign-up-form.tsx @@ -30,6 +30,7 @@ interface PasswordSignUpFormProps { displayTermsCheckbox?: boolean; onSubmit: (params: { + personalCode: string; email: string; password: string; repeatPassword: string; @@ -48,6 +49,7 @@ export function PasswordSignUpForm({ const form = useForm({ resolver: zodResolver(PasswordSignUpSchema), defaultValues: { + personalCode: '', email: defaultValues?.email ?? '', password: '', repeatPassword: '', @@ -60,6 +62,29 @@ export function PasswordSignUpForm({ className={'flex w-full flex-col gap-y-4'} onSubmit={form.handleSubmit(onSubmit)} > + ( + + + + + + + + + + + + )} + /> redirect(redirectUrl)} /> diff --git a/packages/features/auth/src/hooks/use-sign-up-flow.ts b/packages/features/auth/src/hooks/use-sign-up-flow.ts index a6ba52e..1732b68 100644 --- a/packages/features/auth/src/hooks/use-sign-up-flow.ts +++ b/packages/features/auth/src/hooks/use-sign-up-flow.ts @@ -8,6 +8,7 @@ import { useAppEvents } from '@kit/shared/events'; import { useSignUpWithEmailAndPassword } from '@kit/supabase/hooks/use-sign-up-with-email-password'; type SignUpCredentials = { + personalCode: string; email: string; password: string; }; @@ -46,7 +47,6 @@ export function usePasswordSignUpFlow({ emailRedirectTo, captchaToken, }); - // emit event to track sign up appEvents.emit({ type: 'user.signedUp', diff --git a/packages/features/auth/src/schemas/password-sign-up.schema.ts b/packages/features/auth/src/schemas/password-sign-up.schema.ts index 828924d..b2fcab4 100644 --- a/packages/features/auth/src/schemas/password-sign-up.schema.ts +++ b/packages/features/auth/src/schemas/password-sign-up.schema.ts @@ -4,6 +4,9 @@ import { RefinedPasswordSchema, refineRepeatPassword } from './password.schema'; export const PasswordSignUpSchema = 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(), password: RefinedPasswordSchema, repeatPassword: RefinedPasswordSchema, diff --git a/packages/features/notifications/src/components/notifications-popover.tsx b/packages/features/notifications/src/components/notifications-popover.tsx index 208351b..96030fc 100644 --- a/packages/features/notifications/src/components/notifications-popover.tsx +++ b/packages/features/notifications/src/components/notifications-popover.tsx @@ -121,8 +121,8 @@ export function NotificationsPopover(params: { return ( -