diff --git a/app/home/(user)/layout.tsx b/app/home/(user)/(dashboard)/layout.tsx similarity index 89% rename from app/home/(user)/layout.tsx rename to app/home/(user)/(dashboard)/layout.tsx index 97f38ae..281c34c 100644 --- a/app/home/(user)/layout.tsx +++ b/app/home/(user)/(dashboard)/layout.tsx @@ -13,10 +13,10 @@ import { personalAccountNavigationConfig } from '~/config/personal-account-navig import { withI18n } from '~/lib/i18n/with-i18n'; // home imports -import { HomeMenuNavigation } from './_components/home-menu-navigation'; -import { HomeMobileNavigation } from './_components/home-mobile-navigation'; -import { HomeSidebar } from './_components/home-sidebar'; -import { loadUserWorkspace } from './_lib/server/load-user-workspace'; +import { HomeMenuNavigation } from '../_components/home-menu-navigation'; +import { HomeMobileNavigation } from '../_components/home-mobile-navigation'; +import { HomeSidebar } from '../_components/home-sidebar'; +import { loadUserWorkspace } from '../_lib/server/load-user-workspace'; function UserHomeLayout({ children }: React.PropsWithChildren) { const state = use(getLayoutState()); @@ -58,8 +58,8 @@ function HeaderLayout({ children }: React.PropsWithChildren) { return ( - - + + diff --git a/app/home/(user)/page.tsx b/app/home/(user)/(dashboard)/page.tsx similarity index 79% rename from app/home/(user)/page.tsx rename to app/home/(user)/(dashboard)/page.tsx index 6a91bba..71aba23 100644 --- a/app/home/(user)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -3,11 +3,11 @@ import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; -import Dashboard from './_components/dashboard'; +import Dashboard from '../_components/dashboard'; // 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'; +import { loadUserWorkspace } from '../_lib/server/load-user-workspace'; import { PageBody } from '@kit/ui/page'; export const generateMetadata = async () => { diff --git a/app/home/(user)/_components/consent-dialog.tsx b/app/home/(user)/_components/consent-dialog.tsx new file mode 100644 index 0000000..647f812 --- /dev/null +++ b/app/home/(user)/_components/consent-dialog.tsx @@ -0,0 +1,73 @@ +'use client'; + +import Image from 'next/image'; +import { useRouter } from 'next/navigation'; + +import { CaretRightIcon } from '@radix-ui/react-icons'; + +import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data'; +import { useUpdateAccountData } from '@kit/accounts/hooks/use-update-account'; +import { Button } from '@kit/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@kit/ui/dialog'; +import { toast } from '@kit/ui/sonner'; +import { Trans } from '@kit/ui/trans'; + +export default function ConsentDialog({ userId }: { userId: string }) { + const router = useRouter(); + + const updateAccountMutation = useUpdateAccountData(userId); + const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery(); + + const updateConsent = (consentToCompanyStatistics: boolean) => { + const promise = updateAccountMutation + .mutateAsync({ + has_consent_anonymized_company_statistics: consentToCompanyStatistics, + }) + .then(() => { + revalidateUserDataQuery(userId); + }); + + toast.promise(() => promise, { + success: , + error: , + loading: , + }); + + return router.refresh(); + }; + + return ( + + + + Toggle + + + + + + + + + + + + + + ); +} diff --git a/app/home/(user)/_components/home-menu-navigation.tsx b/app/home/(user)/_components/home-menu-navigation.tsx index 96ad1c9..cc77954 100644 --- a/app/home/(user)/_components/home-menu-navigation.tsx +++ b/app/home/(user)/_components/home-menu-navigation.tsx @@ -5,7 +5,7 @@ import { AppLogo } from '~/components/app-logo'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; import { Search } from '~/components/ui/search'; -import { SIDEBAR_WIDTH } from '../../../../packages/ui/src/shadcn/constants'; +import { SIDEBAR_WIDTH_PROPERTY } from '../../../../packages/ui/src/shadcn/constants'; // home imports import { UserNotifications } from '../_components/user-notifications'; import { type UserWorkspace } from '../_lib/server/load-user-workspace'; @@ -17,7 +17,7 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) { return (
-
+
diff --git a/app/home/(user)/_lib/server/load-user-account.ts b/app/home/(user)/_lib/server/load-user-account.ts new file mode 100644 index 0000000..de88993 --- /dev/null +++ b/app/home/(user)/_lib/server/load-user-account.ts @@ -0,0 +1,21 @@ +import { cache } from 'react'; + +import { createAccountsApi } from '@kit/accounts/api'; +import { getSupabaseServerClient } from '@kit/supabase/server-client'; + +export type UserAccount = Awaited>; + +/** + * @name loadUserAccount + * @description + * Load the user account. It's a cached per-request function that fetches the user workspace data. + * It can be used across the server components to load the user workspace data. + */ +export const loadUserAccount = cache(accountLoader); + +async function accountLoader(accountId: string) { + const client = getSupabaseServerClient(); + const api = createAccountsApi(client); + + return api.getAccount(accountId); +} diff --git a/app/home/layout.tsx b/app/home/layout.tsx new file mode 100644 index 0000000..08a2b19 --- /dev/null +++ b/app/home/layout.tsx @@ -0,0 +1,24 @@ +import { requireUserInServerComponent } from '../../lib/server/require-user-in-server-component'; +import ConsentDialog from './(user)/_components/consent-dialog'; +import { loadUserAccount } from './(user)/_lib/server/load-user-account'; + +export default async function HomeLayout({ + children, +}: { + children: React.ReactNode; +}) { + const user = await requireUserInServerComponent(); + const account = user?.identities?.[0]?.id + ? await loadUserAccount(user?.identities?.[0]?.id) + : null; + + if (account && account?.has_consent_anonymized_company_statistics === null) { + return ( +
+ +
+ ); + } + + return <>{children}; +} diff --git a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx index e995447..08cd4a5 100644 --- a/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx +++ b/packages/features/accounts/src/components/personal-account-settings/account-settings-container.tsx @@ -16,6 +16,7 @@ import { Trans } from '@kit/ui/trans'; import { usePersonalAccountData } from '../../hooks/use-personal-account-data'; import { AccountDangerZone } from './account-danger-zone'; +import ConsentToggle from './consent/consent-toggle'; import { UpdateEmailFormContainer } from './email/update-email-form-container'; import { MultiFactorAuthFactorsList } from './mfa/multi-factor-auth-list'; import { UpdatePasswordFormContainer } from './password/update-password-container'; @@ -150,6 +151,32 @@ export function PersonalAccountSettingsContainer( + + +
+
+

+ +

+ + + + +
+ +
+
+
+ diff --git a/packages/features/accounts/src/components/personal-account-settings/consent/consent-toggle.tsx b/packages/features/accounts/src/components/personal-account-settings/consent/consent-toggle.tsx new file mode 100644 index 0000000..9c8cbd5 --- /dev/null +++ b/packages/features/accounts/src/components/personal-account-settings/consent/consent-toggle.tsx @@ -0,0 +1,46 @@ +import { useState } from 'react'; + +import { toast } from 'sonner'; + +import { Switch } from '@kit/ui/switch'; +import { Trans } from '@kit/ui/trans'; + +import { useRevalidatePersonalAccountDataQuery } from '../../../hooks/use-personal-account-data'; +import { useUpdateAccountData } from '../../../hooks/use-update-account'; + +// This is temporary. When the profile views are ready, all account values included in the form will be updated together on form submit. +export default function ConsentToggle({ + userId, + initialState, +}: { + userId: string; + initialState: boolean; +}) { + const [isConsent, setIsConsent] = useState(initialState); + const updateAccountMutation = useUpdateAccountData(userId); + const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery(); + + const updateConsent = (consent: boolean) => { + const promise = updateAccountMutation + .mutateAsync({ + has_consent_anonymized_company_statistics: consent, + }) + .then(() => { + revalidateUserDataQuery(userId); + }); + + return toast.promise(() => promise, { + success: , + error: , + loading: , + }); + }; + return ( + updateConsent(!isConsent)} + disabled={updateAccountMutation.isPending} + /> + ); +} diff --git a/packages/features/accounts/src/hooks/use-personal-account-data.ts b/packages/features/accounts/src/hooks/use-personal-account-data.ts index 6c53da9..83e16bc 100644 --- a/packages/features/accounts/src/hooks/use-personal-account-data.ts +++ b/packages/features/accounts/src/hooks/use-personal-account-data.ts @@ -22,14 +22,7 @@ export function usePersonalAccountData( const response = await client .from('accounts') - .select( - ` - id, - name, - picture_url, - last_name - `, - ) + .select() .eq('primary_owner_user_id', userId) .eq('is_personal_account', true) .single(); diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index a6343a6..11527f5 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -189,6 +189,7 @@ export type Database = { created_at: string | null created_by: string | null email: string | null + has_consent_anonymized_company_statistics: boolean | null has_consent_personal_data: boolean | null id: string is_personal_account: boolean @@ -208,6 +209,7 @@ export type Database = { created_at?: string | null created_by?: string | null email?: string | null + has_consent_anonymized_company_statistics?: boolean | null has_consent_personal_data?: boolean | null id?: string is_personal_account?: boolean @@ -227,6 +229,7 @@ export type Database = { created_at?: string | null created_by?: string | null email?: string | null + has_consent_anonymized_company_statistics?: boolean | null has_consent_personal_data?: boolean | null id?: string is_personal_account?: boolean @@ -1498,6 +1501,7 @@ export type Database = { created_at: string | null created_by: string | null email: string | null + has_consent_anonymized_company_statistics: boolean | null has_consent_personal_data: boolean | null id: string is_personal_account: boolean diff --git a/packages/ui/src/shadcn/constants.ts b/packages/ui/src/shadcn/constants.ts index 970de77..3d5ecf6 100644 --- a/packages/ui/src/shadcn/constants.ts +++ b/packages/ui/src/shadcn/constants.ts @@ -1,3 +1,7 @@ export const SIDEBAR_WIDTH = '16rem'; export const SIDEBAR_WIDTH_MOBILE = '18rem'; export const SIDEBAR_WIDTH_ICON = '4rem'; + +export const SIDEBAR_WIDTH_PROPERTY = 'w-[16rem]'; +export const SIDEBAR_WIDTH_MOBILE_PROPERTY = 'w-[18rem]'; +export const SIDEBAR_WIDTH_ICON_PROPERTY = 'w-[4rem]'; diff --git a/packages/ui/src/shadcn/dialog.tsx b/packages/ui/src/shadcn/dialog.tsx index 6655ab3..2c1b035 100644 --- a/packages/ui/src/shadcn/dialog.tsx +++ b/packages/ui/src/shadcn/dialog.tsx @@ -32,8 +32,16 @@ const DialogContent: React.FC< React.ComponentPropsWithoutRef & { customClose?: React.JSX.Element; preventAutoFocus?: boolean; + disableClose?: boolean; } -> = ({ className, children, customClose, preventAutoFocus, ...props }) => ( +> = ({ + className, + children, + customClose, + disableClose, + preventAutoFocus, + ...props +}) => ( e.preventDefault() : props.onOpenAutoFocus } + onInteractOutside={ + disableClose ? (e) => e.preventDefault() : props.onInteractOutside + } + onPointerDownOutside={ + disableClose ? (e) => e.preventDefault() : props.onPointerDownOutside + } + onEscapeKeyDown={ + disableClose ? (e) => e.preventDefault() : props.onEscapeKeyDown + } {...props} > {children} - {customClose || ( - <> - - Close - - )} + {!disableClose && + (customClose || ( + <> + + Close + + ))} @@ -114,13 +132,13 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, DialogClose, DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, }; diff --git a/public/assets/toggle.png b/public/assets/toggle.png new file mode 100644 index 0000000..995f3cc Binary files /dev/null and b/public/assets/toggle.png differ diff --git a/public/locales/en/account.json b/public/locales/en/account.json index dbaa182..4718a9b 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -115,5 +115,12 @@ "createCompanyAccount": "Create Company Account", "requestCompanyAccount": { "title": "Company details" + }, + "updateConsentSuccess": "Consent successfully updated", + "updateConsentError": "Encountered an error. Please try again", + "updateConsentLoading": "Updating consent...", + "consentToAnonymizedCompanyData": { + "label": "Consent to be included in employer statistics", + "description": "Consent to be included in anonymized company statistics" } -} +} \ No newline at end of file diff --git a/public/locales/et/account.json b/public/locales/et/account.json index 99a5167..f943293 100644 --- a/public/locales/et/account.json +++ b/public/locales/et/account.json @@ -130,5 +130,18 @@ "successTitle": "Tere, {{firstName}} {{lastName}}", "successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!", "successButton": "Jätka" + }, + "consentModal": { + "title": "Enne toimetama hakkamist", + "description": "Kas annad nõusoleku, et sinu terviseandmeid kasutatakse anonüümselt tööandja statistikas? Andmed jäävad isikustamata ja aitavad ettevõttel töötajate tervist paremini toetada.", + "reject": "Ei anna nõusolekut", + "accept": "Annan nõusoleku" + }, + "updateConsentSuccess": "Nõusolekud uuendatud", + "updateConsentError": "Midagi läks valesti. Palun proovi uuesti", + "updateConsentLoading": "Nõusolekuid uuendatakse...", + "consentToAnonymizedCompanyData": { + "label": "Nõustun osalema tööandja statistikas", + "description": "Nõustun anonümiseeritud kujul terviseandmete kasutamisega tööandja statistikas" } -} +} \ No newline at end of file diff --git a/public/locales/ru/account.json b/public/locales/ru/account.json index 1b0924e..cee69b4 100644 --- a/public/locales/ru/account.json +++ b/public/locales/ru/account.json @@ -112,5 +112,12 @@ "noTeamsYet": "You don't have any teams yet.", "createTeam": "Create a team to get started.", "createTeamButtonLabel": "Create a Team", - "createCompanyAccount": "Create Company Account" -} + "createCompanyAccount": "Create Company Account", + "updateConsentSuccess": "Consent successfully updated", + "updateConsentError": "Encountered an error. Please try again", + "updateConsentLoading": "Updating consent...", + "consentToAnonymizedCompanyData": { + "label": "Consent to be included in employer statistics", + "description": "Consent to be included in anonymized company statistics" + } +} \ No newline at end of file diff --git a/supabase/migrations/20250630163951_alter_accounts_personal_code.sql b/supabase/migrations/20250630163951_alter_accounts_personal_code.sql index 2a6fde1..e70412f 100644 --- a/supabase/migrations/20250630163951_alter_accounts_personal_code.sql +++ b/supabase/migrations/20250630163951_alter_accounts_personal_code.sql @@ -1,3 +1,5 @@ +ALTER TABLE "public"."invitations" ADD COLUMN IF NOT EXISTS personal_code text; + DO $$ BEGIN IF NOT EXISTS ( diff --git a/supabase/migrations/20250702095302_add_anonymized_company_statistics_consent_field.sql b/supabase/migrations/20250702095302_add_anonymized_company_statistics_consent_field.sql new file mode 100644 index 0000000..40ed6db --- /dev/null +++ b/supabase/migrations/20250702095302_add_anonymized_company_statistics_consent_field.sql @@ -0,0 +1,23 @@ +alter table "public"."accounts" add column "has_consent_anonymized_company_statistics" boolean; + +alter table "audit"."log_entries" alter column "record_key" set data type text using "record_key"::text; + +create policy "insert_own" +on "audit"."log_entries" +as permissive +for insert +to authenticated +with check ((( SELECT auth.uid() AS uid) = changed_by)); + +drop policy "service_role_all" on "audit"."sync_entries"; + +create policy "service_role_all" +on "audit"."sync_entries" +as permissive +for all +to service_role +using (true); + +CREATE TRIGGER log_account_change AFTER DELETE OR UPDATE ON public.accounts FOR EACH ROW EXECUTE FUNCTION audit.log_audit_changes(); +GRANT USAGE ON SCHEMA audit TO authenticated; +grant insert on table audit.log_entries to authenticated; \ No newline at end of file