MED-63: change api to medreport schema

MED-63
* membership confirmation flow
* update schema public -> medreport
This commit is contained in:
danelkungla
2025-07-09 13:40:28 +03:00
committed by GitHub
86 changed files with 2195 additions and 10702 deletions

View File

@@ -78,3 +78,12 @@ npm run supabase:typegen:app
To get medusa store working you need to update the env's to your running medusa app and migrate the tables from medusa project to your supabase project
You can get `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` from your medusa app settings
## Super admin
To access admin pages follow these steps:
- Register new user
- Go to Profile and add Multi-Factor Authentication
- Sign out and Sign in
- Authenticate with mfa (at current time profile page prompts it again)

View File

@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation';
import { MedReportLogo } from '@/components/med-report-logo';
import { SubmitButton } from '@/components/ui/submit-button';
import { withI18n } from '@/lib/i18n/with-i18n';
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
import { CompanySubmitData } from '@/lib/types/company';
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
@@ -18,7 +19,7 @@ import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { Trans } from '@kit/ui/trans';
export default function CompanyOffer() {
function CompanyOffer() {
const router = useRouter();
const {
register,
@@ -100,3 +101,5 @@ export default function CompanyOffer() {
</div>
);
}
export default withI18n(CompanyOffer);

View File

@@ -34,6 +34,7 @@ async function accountLoader(id: string) {
const client = getSupabaseServerClient();
const { data, error } = await client
.schema('medreport')
.from('accounts')
.select('*, memberships: accounts_memberships (*)')
.eq('id', id)

View File

@@ -1,39 +1,34 @@
'use client';
import { redirect } from 'next/navigation';
import React from 'react';
import pathsConfig from '@/config/paths.config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from '@kit/notifications/components';
import { SuccessNotification } from './success-notification';
export const UpdateAccountSuccessNotification = ({
userId,
}: {
userId?: string;
}) => {
const { t } = useTranslation('account');
if (!userId) {
redirect(pathsConfig.app.home);
}
const MembershipConfirmationNotification: React.FC<{
userId: string;
}> = ({ userId }) => {
const { t } = useTranslation();
const { data: accountData } = usePersonalAccountData(userId);
return (
<SuccessNotification
showLogo={false}
title={t('account:updateAccount:successTitle', {
title={t('account:membershipConfirmation:successTitle', {
firstName: accountData?.name,
lastName: accountData?.last_name,
})}
descriptionKey="account:updateAccount:successDescription"
descriptionKey="account:membershipConfirmation:successDescription"
buttonProps={{
buttonTitleKey: 'account:updateAccount:successButton',
buttonTitleKey: 'account:membershipConfirmation:successButton',
href: pathsConfig.app.selectPackage,
}}
/>
);
};
export default MembershipConfirmationNotification;

View File

@@ -0,0 +1,11 @@
import { withI18n } from '~/lib/i18n/with-i18n';
async function SiteLayout(props: React.PropsWithChildren) {
return (
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
{props.children}
</div>
);
}
export default SiteLayout;

View File

@@ -0,0 +1,25 @@
import { redirect } from 'next/navigation';
import pathsConfig from '@/config/paths.config';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { withI18n } from '~/lib/i18n/with-i18n';
import MembershipConfirmationNotification from './_components/membership-confirmation-notification';
async function UpdateAccountSuccess() {
const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
if (!user?.id) {
redirect(pathsConfig.app.home);
}
return <MembershipConfirmationNotification userId={user.id} />;
}
export default withI18n(UpdateAccountSuccess);

View File

@@ -1,17 +0,0 @@
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { UpdateAccountSuccessNotification } from '@kit/notifications/components';
import { withI18n } from '~/lib/i18n/with-i18n';
async function UpdateAccountSuccess() {
const client = getSupabaseServerClient();
const {
data: { user },
} = await client.auth.getUser();
return <UpdateAccountSuccessNotification userId={user?.id} />;
}
export default withI18n(UpdateAccountSuccess);

View File

@@ -26,7 +26,7 @@ async function getSupabaseHealthCheck() {
try {
const client = getSupabaseServerAdminClient();
const { error } = await client.rpc('is_set', {
const { error } = await client.schema('medreport').rpc('is_set', {
field_name: 'billing_provider',
});

View File

@@ -1,3 +1,6 @@
import { use } from 'react';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
@@ -6,9 +9,7 @@ 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();

View File

@@ -35,13 +35,13 @@ async function workspaceLoader() {
accountsPromise(),
workspacePromise,
requireUserInServerComponent(),
tempAccountsPromise()
tempAccountsPromise(),
]);
return {
accounts,
workspace,
user,
tempVisibleAccounts
tempVisibleAccounts,
};
}

View File

@@ -46,7 +46,9 @@ async function loadAccountMembers(
client: SupabaseClient<Database>,
account: string,
) {
const { data, error } = await client.rpc('get_account_members', {
const { data, error } = await client
.schema('medreport')
.rpc('get_account_members', {
account_slug: account,
});
@@ -67,7 +69,9 @@ async function loadInvitations(
client: SupabaseClient<Database>,
account: string,
) {
const { data, error } = await client.rpc('get_account_invitations', {
const { data, error } = await client
.schema('medreport')
.rpc('get_account_invitations', {
account_slug: account,
});

View File

@@ -79,12 +79,11 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
// we need to verify the user isn't already in the account
// we do so by checking if the user can read the account
// if the user can read the account, then they are already in the account
const { data: isAlreadyTeamMember } = await client.rpc(
'is_account_team_member',
{
const { data: isAlreadyTeamMember } = await client
.schema('medreport')
.rpc('is_account_team_member', {
target_account_id: invitation.account.id,
},
);
});
// if the user is already in the account redirect to the home page
if (isAlreadyTeamMember) {

View File

@@ -10,6 +10,7 @@ const PathsSchema = z.object({
passwordUpdate: z.string().min(1),
updateAccount: z.string().min(1),
updateAccountSuccess: z.string().min(1),
membershipConfirmation: z.string().min(1),
}),
app: z.object({
home: z.string().min(1),
@@ -42,6 +43,7 @@ const pathsConfig = PathsSchema.parse({
passwordUpdate: '/update-password',
updateAccount: '/auth/update-account',
updateAccountSuccess: '/auth/update-account/success',
membershipConfirmation: '/auth/membership-confirmation',
},
app: {
home: '/home',

View File

@@ -137,6 +137,7 @@ async function syncData() {
for (const analysisGroup of analysisGroups) {
// SAVE ANALYSIS GROUP
const { data: insertedAnalysisGroup, error } = await supabase
.schema('medreport')
.from('analysis_groups')
.upsert(
{
@@ -174,6 +175,7 @@ async function syncData() {
const analysisElement = item.UuringuElement;
const { data: insertedAnalysisElement, error } = await supabase
.schema('medreport')
.from('analysis_elements')
.upsert(
{
@@ -217,6 +219,7 @@ async function syncData() {
if (analyses?.length) {
for (const analysis of analyses) {
const { data: insertedAnalysis, error } = await supabase
.schema('medreport')
.from('analyses')
.upsert(
{
@@ -259,7 +262,7 @@ async function syncData() {
}
}
await supabase.from('codes').upsert(codes);
await supabase.schema('medreport').from('codes').upsert(codes);
await supabase.schema('audit').from('sync_entries').insert({
operation: 'ANALYSES_SYNC',

View File

@@ -105,10 +105,12 @@ async function syncData() {
});
const { error: providersError } = await supabase
.schema('medreport')
.from('connected_online_providers')
.upsert(mappedClinics);
const { error: servicesError } = await supabase
.schema('medreport')
.from('connected_online_services')
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });

View File

@@ -1,4 +1,4 @@
'use server'
'use server';
import logRequestResult from '@/lib/services/audit.service';
import { RequestStatus } from '@/lib/types/audit';
@@ -9,7 +9,7 @@ import {
ConnectedOnlineMethodName,
} from '@/lib/types/connected-online';
import { ExternalApi } from '@/lib/types/external';
import { Tables } from '@/supabase/database.types';
import { Tables } from '@/packages/supabase/src/database.types';
import { createClient } from '@/utils/supabase/server';
import axios from 'axios';
@@ -106,11 +106,13 @@ export async function bookAppointment(
{ data: dbService, error: serviceError },
] = await Promise.all([
supabase
.schema('medreport')
.from('connected_online_providers')
.select('*')
.eq('id', clinicId)
.limit(1),
supabase
.schema('medreport')
.from('connected_online_services')
.select('*')
.eq('sync_id', serviceSyncId)
@@ -132,8 +134,14 @@ export async function bookAppointment(
);
}
const clinic: Tables<'connected_online_providers'> = dbClinic![0];
const service: Tables<'connected_online_services'> = dbService![0];
const clinic: Tables<
{ schema: 'medreport' },
'connected_online_providers'
> = dbClinic![0];
const service: Tables<
{ schema: 'medreport' },
'connected_online_services'
> = dbService![0];
// TODO the dummy data needs to be replaced with real values once they're present on the user/account
const response = await axios.post(
@@ -183,6 +191,7 @@ export async function bookAppointment(
const responseParts = responseData.Value.split(',');
const { error } = await supabase
.schema('medreport')
.from('connected_online_reservation')
.insert({
booking_code: responseParts[1],

View File

@@ -32,6 +32,7 @@ import { toArray } from '@/lib/utils';
import axios from 'axios';
import { XMLParser } from 'fast-xml-parser';
import { uniqBy } from 'lodash';
import { Tables } from '@kit/supabase/database';
const BASE_URL = process.env.MEDIPOST_URL!;
@@ -196,6 +197,7 @@ async function saveAnalysisGroup(
supabase: SupabaseClient,
) {
const { data: insertedAnalysisGroup, error } = await supabase
.schema('medreport')
.from('analysis_groups')
.upsert(
{
@@ -215,7 +217,8 @@ async function saveAnalysisGroup(
const analysisGroupId = insertedAnalysisGroup[0].id;
const analysisGroupCodes = toArray(analysisGroup.Kood);
const codes: Partial<Tables<'codes'>>[] = analysisGroupCodes.map((kood) => ({
const codes: Partial<Tables<{ schema: 'medreport' }, 'codes'>>[] =
analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
@@ -229,6 +232,7 @@ async function saveAnalysisGroup(
const analysisElement = item.UuringuElement;
const { data: insertedAnalysisElement, error } = await supabase
.schema('medreport')
.from('analysis_elements')
.upsert(
{
@@ -270,6 +274,7 @@ async function saveAnalysisGroup(
if (analyses?.length) {
for (const analysis of analyses) {
const { data: insertedAnalysis, error } = await supabase
.schema('medreport')
.from('analyses')
.upsert(
{
@@ -310,6 +315,7 @@ async function saveAnalysisGroup(
}
const { error: codesError } = await supabase
.schema('medreport')
.from('codes')
.upsert(codes, { ignoreDuplicates: false });
@@ -404,27 +410,34 @@ export async function composeOrderXML(
};
const { data: analysisElements } = (await supabase
.schema('medreport')
.from('analysis_elements')
.select(`*, analysis_groups(*)`)
.in('id', orderedElements)) as {
data: ({
analysis_groups: Tables<'analysis_groups'>;
} & Tables<'analysis_elements'>)[];
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
} & Tables<{ schema: 'medreport' }, 'analysis_elements'>)[];
};
const { data: analyses } = (await supabase
.schema('medreport')
.from('analyses')
.select(`*, analysis_elements(*, analysis_groups(*))`)
.in('id', orderedAnalyses)) as {
data: ({
analysis_elements: Tables<'analysis_elements'> & {
analysis_groups: Tables<'analysis_groups'>;
analysis_elements: Tables<
{ schema: 'medreport' },
'analysis_elements'
> & {
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
};
} & Tables<'analyses'>)[];
} & Tables<{ schema: 'medreport' }, 'analyses'>)[];
};
const analysisGroups: Tables<'analysis_groups'>[] = uniqBy(
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
uniqBy(
(
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? []
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ??
[]
).concat(
analyses?.flatMap(
({ analysis_elements }) => analysis_elements.analysis_groups,
@@ -545,6 +558,7 @@ export async function syncPrivateMessage(
const status = response.TellimuseOlek;
const { data: analysisOrder, error: analysisOrderError } = await supabase
.schema('medreport')
.from('analysis_orders')
.select('user_id')
.eq('id', response.ValisTellimuseId);
@@ -556,6 +570,7 @@ export async function syncPrivateMessage(
}
const { data: analysisResponse, error } = await supabase
.schema('medreport')
.from('analysis_responses')
.upsert(
{
@@ -576,7 +591,7 @@ export async function syncPrivateMessage(
const analysisGroups = toArray(response.UuringuGrupp);
const responses: Omit<
Tables<'analysis_response_elements'>,
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
'id' | 'created_at' | 'updated_at'
>[] = [];
for (const analysisGroup of analysisGroups) {
@@ -608,6 +623,7 @@ export async function syncPrivateMessage(
}
const { error: deleteError } = await supabase
.schema('medreport')
.from('analysis_response_elements')
.delete()
.eq('analysis_response_id', analysisResponse[0].id);
@@ -619,6 +635,7 @@ export async function syncPrivateMessage(
}
const { error: elementInsertError } = await supabase
.schema('medreport')
.from('analysis_response_elements')
.insert(responses);

View File

@@ -1,5 +1,5 @@
import { DATE_TIME_FORMAT } from '@/lib/constants';
import { Tables } from '@/supabase/database.types';
import { Tables } from '@/packages/supabase/src/database.types';
import { format } from 'date-fns';
const isProd = process.env.NODE_ENV === 'production';
@@ -160,7 +160,7 @@ export const getAnalysisGroup = (
analysisGroupOriginalId: string,
analysisGroupName: string,
specimenOrderNr: number,
analysisElement: Tables<'analysis_elements'>,
analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>,
) =>
`<UuringuGrupp>
<UuringuGruppId>${analysisGroupOriginalId}</UuringuGruppId>

View File

@@ -139,7 +139,7 @@ function getPatterns() {
handler: adminMiddleware,
},
{
pattern: new URLPattern({ pathname: '/auth/update-account' }),
pattern: new URLPattern({ pathname: '/auth/*?' }),
handler: async (req: NextRequest, res: NextResponse) => {
const {
data: { user },

View File

@@ -102,7 +102,7 @@
"pino-pretty": "^13.0.0",
"prettier": "^3.5.3",
"react-hook-form": "^7.57.0",
"supabase": "^2.26.9",
"supabase": "^2.30.4",
"tailwindcss": "4.1.7",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.3",

View File

@@ -1,7 +1,7 @@
import { Database } from '@kit/supabase/database';
export type UpsertSubscriptionParams =
Database['public']['Functions']['upsert_subscription']['Args'] & {
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
line_items: Array<LineItem>;
};
@@ -19,4 +19,4 @@ interface LineItem {
}
export type UpsertOrderParams =
Database['public']['Functions']['upsert_order']['Args'];
Database['medreport']['Functions']['upsert_order']['Args'];

View File

@@ -32,8 +32,7 @@
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "19.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1"
"react-hook-form": "^7.56.3"
},
"typesVersions": {
"*": {

View File

@@ -14,8 +14,8 @@ import { Trans } from '@kit/ui/trans';
import { CurrentPlanBadge } from './current-plan-badge';
import { LineItemDetails } from './line-item-details';
type Order = Tables<'orders'>;
type LineItem = Tables<'order_items'>;
type Order = Tables<{ schema: 'medreport' }, 'orders'>;
type LineItem = Tables<{ schema: 'medreport' }, 'order_items'>;
interface Props {
order: Order & {

View File

@@ -18,8 +18,8 @@ import { CurrentPlanAlert } from './current-plan-alert';
import { CurrentPlanBadge } from './current-plan-badge';
import { LineItemDetails } from './line-item-details';
type Subscription = Tables<'subscriptions'>;
type LineItem = Tables<'subscription_items'>;
type Subscription = Tables<{ schema: 'medreport' }, 'subscriptions'>;
type LineItem = Tables<{ schema: 'medreport' }, 'subscription_items'>;
interface Props {
subscription: Subscription & {

View File

@@ -86,6 +86,7 @@ class BillingEventHandlerService {
logger.info(ctx, 'Processing subscription deleted event...');
const { error } = await client
.schema('medreport')
.from('subscriptions')
.delete()
.match({ id: subscriptionId });
@@ -109,7 +110,7 @@ class BillingEventHandlerService {
logger.info(ctx, 'Successfully deleted subscription');
},
onSubscriptionUpdated: async (subscription) => {
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
const ctx = {
@@ -147,7 +148,7 @@ class BillingEventHandlerService {
onCheckoutSessionCompleted: async (payload) => {
// Handle the checkout session completed event
// here we add the subscription to the database
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
// Check if the payload contains an order_id
@@ -212,7 +213,7 @@ class BillingEventHandlerService {
}
},
onPaymentSucceeded: async (sessionId: string) => {
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
const ctx = {
@@ -244,7 +245,7 @@ class BillingEventHandlerService {
logger.info(ctx, 'Successfully updated payment status');
},
onPaymentFailed: async (sessionId: string) => {
const client = this.clientProvider();
const client = this.clientProvider().schema('medreport');
const logger = await getLogger();
const ctx = {

View File

@@ -21,6 +21,7 @@ export async function getBillingGatewayProvider(
async function getBillingProvider(client: SupabaseClient<Database>) {
const { data, error } = await client
.schema('medreport')
.from('config')
.select('billing_provider')
.single();

View File

@@ -4,7 +4,7 @@ import { Tables } from '@kit/supabase/database';
import { createBillingGatewayService } from '../billing-gateway/billing-gateway.service';
type Subscription = Tables<'subscriptions'>;
type Subscription = Tables<{ schema: 'medreport' }, 'subscriptions'>;
export function createBillingWebhooksService() {
return new BillingWebhooksService();

View File

@@ -17,14 +17,14 @@ import { createLemonSqueezySubscriptionPayloadBuilderService } from './lemon-squ
import { createHmac } from './verify-hmac';
type UpsertSubscriptionParams =
Database['public']['Functions']['upsert_subscription']['Args'] & {
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
line_items: Array<LineItem>;
};
type UpsertOrderParams =
Database['public']['Functions']['upsert_order']['Args'];
Database['medreport']['Functions']['upsert_order']['Args'];
type BillingProvider = Enums<'billing_provider'>;
type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>;
interface LineItem {
id: string;

View File

@@ -9,7 +9,7 @@ import { createStripeClient } from './stripe-sdk';
import { createStripeSubscriptionPayloadBuilderService } from './stripe-subscription-payload-builder.service';
type UpsertSubscriptionParams =
Database['public']['Functions']['upsert_subscription']['Args'] & {
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
line_items: Array<LineItem>;
};
@@ -27,9 +27,9 @@ interface LineItem {
}
type UpsertOrderParams =
Database['public']['Functions']['upsert_order']['Args'];
Database['medreport']['Functions']['upsert_order']['Args'];
type BillingProvider = Enums<'billing_provider'>;
type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>;
export class StripeWebhookHandlerService
implements BillingWebhookHandlerService

View File

@@ -1,6 +1,6 @@
import { Database } from '@kit/supabase/database';
export type Tables = Database['public']['Tables'];
export type Tables = Database['medreport']['Tables'];
export type TableChangeType = 'INSERT' | 'UPDATE' | 'DELETE';

View File

@@ -44,7 +44,6 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",

View File

@@ -19,7 +19,8 @@ import { Trans } from '@kit/ui/trans';
import { useUpdateAccountData } from '../../hooks/use-update-account';
import { AccountDetailsSchema } from '../../schema/account-details.schema';
type UpdateUserDataParams = Database['public']['Tables']['accounts']['Update'];
type UpdateUserDataParams =
Database['medreport']['Tables']['accounts']['Update'];
export function UpdateAccountDetailsForm({
displayName,

View File

@@ -72,6 +72,7 @@ function UploadProfileAvatarForm(props: {
uploadUserProfilePhoto(client, file, props.userId)
.then((pictureUrl) => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: pictureUrl,
@@ -90,6 +91,7 @@ function UploadProfileAvatarForm(props: {
removeExistingStorageFile()
.then(() => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: null,

View File

@@ -17,7 +17,9 @@ interface UserWorkspace {
id: string | null;
name: string | null;
picture_url: string | null;
subscription_status: Tables<'subscriptions'>['status'] | null;
subscription_status:
| Tables<{ schema: 'medreport' }, 'subscriptions'>['status']
| null;
};
user: User;

View File

@@ -21,6 +21,7 @@ export function usePersonalAccountData(
}
const response = await client
.schema('medreport')
.from('accounts')
.select()
.eq('primary_owner_user_id', userId)

View File

@@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
import { Database } from '@kit/supabase/database';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
type UpdateData = Database['public']['Tables']['accounts']['Update'];
type UpdateData = Database['medreport']['Tables']['accounts']['Update'];
export function useUpdateAccountData(accountId: string) {
const client = useSupabase();
@@ -11,7 +11,11 @@ export function useUpdateAccountData(accountId: string) {
const mutationKey = ['account:data', accountId];
const mutationFn = async (data: UpdateData) => {
const response = await client.from('accounts').update(data).match({
const response = await client
.schema('medreport')
.from('accounts')
.update(data)
.match({
id: accountId,
});

View File

@@ -17,6 +17,7 @@ class AccountsApi {
*/
async getAccount(id: string) {
const { data, error } = await this.client
.schema('medreport')
.from('accounts')
.select('*')
.eq('id', id)
@@ -35,6 +36,7 @@ class AccountsApi {
*/
async getAccountWorkspace() {
const { data, error } = await this.client
.schema('medreport')
.from('user_account_workspace')
.select(`*`)
.single();
@@ -53,44 +55,46 @@ class AccountsApi {
async loadUserAccounts() {
const authUser = await this.client.auth.getUser();
const {
data,
error: userError,
} = authUser
const { data, error: userError } = authUser;
if (userError) {
console.error('Failed to get user', userError);
throw userError;
}
const { user } = data;
const { data: accounts, error } = await this.client
.schema('medreport')
.from('accounts_memberships')
.select(`
.select(
`
account_id,
user_accounts (
accounts (
name,
slug,
picture_url
)
`)
`,
)
.eq('user_id', user.id)
.eq('account_role', 'owner');
if (error) {
console.error('error', error);
throw error;
}
return accounts.map(({ user_accounts }) => ({
label: user_accounts.name,
value: user_accounts.slug,
image: user_accounts.picture_url,
return accounts.map(({ accounts }) => ({
label: accounts.name,
value: accounts.slug,
image: accounts.picture_url,
}));
}
async loadTempUserAccounts() {
const { data: accounts, error } = await this.client
.schema('medreport')
.from('user_accounts')
.select(`name, slug`);
@@ -131,6 +135,7 @@ class AccountsApi {
*/
async getOrder(accountId: string) {
const response = await this.client
.schema('medreport')
.from('orders')
.select('*, items: order_items !inner (*)')
.eq('account_id', accountId)
@@ -151,6 +156,7 @@ class AccountsApi {
*/
async getCustomerId(accountId: string) {
const response = await this.client
.schema('medreport')
.from('billing_customers')
.select('customer_id')
.eq('account_id', accountId)

View File

@@ -3,6 +3,11 @@ import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import {
AccountInvitationsTable,
AccountMembersTable,
InviteMembersDialogContainer,
} from '@kit/team-accounts/components';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';
@@ -28,14 +33,8 @@ 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'>;
type Account = Tables<{ schema: 'medreport' }, 'accounts'>;
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
export function AdminAccountPage(props: {
account: Account & { memberships: Membership[] };
@@ -231,6 +230,7 @@ async function SubscriptionsTable(props: { accountId: string }) {
const client = getSupabaseServerClient();
const { data: subscription, error } = await client
.schema('medreport')
.from('subscriptions')
.select('*, subscription_items !inner (*)')
.eq('account_id', props.accountId)
@@ -372,6 +372,7 @@ async function getMemberships(userId: string) {
const client = getSupabaseServerClient();
const memberships = await client
.schema('medreport')
.from('accounts_memberships')
.select<
string,
@@ -394,7 +395,7 @@ async function getMemberships(userId: string) {
async function getMembers(accountSlug: string) {
const client = getSupabaseServerClient();
const members = await client.rpc('get_account_members', {
const members = await client.schema('medreport').rpc('get_account_members', {
account_slug: accountSlug,
});

View File

@@ -38,7 +38,7 @@ import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
import { AdminResetPasswordDialog } from './admin-reset-password-dialog';
type Account = Database['public']['Tables']['accounts']['Row'];
type Account = Database['medreport']['Tables']['accounts']['Row'];
const FiltersSchema = z.object({
type: z.enum(['all', 'team', 'personal']),

View File

@@ -9,7 +9,7 @@ import { DataTable } from '@kit/ui/enhanced-data-table';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
type Memberships =
Database['public']['Functions']['get_account_members']['Returns'][number];
Database['medreport']['Functions']['get_account_members']['Returns'][number];
export function AdminMembersTable(props: { members: Memberships[] }) {
return <DataTable data={props.members} columns={getColumns()} />;

View File

@@ -7,7 +7,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { Tables } from '@kit/supabase/database';
import { DataTable } from '@kit/ui/enhanced-data-table';
type Membership = Tables<'accounts_memberships'> & {
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'> & {
account: {
id: string;
name: string;

View File

@@ -15,13 +15,13 @@ import {
ImpersonateUserSchema,
ReactivateUserSchema,
} from './schema/admin-actions.schema';
import { CreateCompanySchema } from './schema/create-company.schema';
import { CreateUserSchema } from './schema/create-user.schema';
import { ResetPasswordSchema } from './schema/reset-password.schema';
import { createAdminAccountsService } from './services/admin-accounts.service';
import { createAdminAuthUserService } from './services/admin-auth-user.service';
import { adminAction } from './utils/admin-action';
import { CreateCompanySchema } from './schema/create-company.schema';
import { createCreateCompanyAccountService } from './services/admin-create-company-account.service';
import { adminAction } from './utils/admin-action';
/**
* @name banUserAction
@@ -183,12 +183,16 @@ export const createUserAction = adminAction(
);
const { error: accountError } = await adminClient
.schema('medreport')
.from('accounts')
.update({ personal_code: personalCode })
.eq('id', data.user.id);
if (accountError) {
logger.error({ accountError }, 'Error inserting personal code to accounts');
logger.error(
{ accountError },
'Error inserting personal code to accounts',
);
throw new Error(`Error saving personal code: ${accountError.message}`);
}

View File

@@ -13,6 +13,7 @@ class AdminAccountsService {
async deleteAccount(accountId: string) {
const { error } = await this.adminClient
.schema('medreport')
.from('accounts')
.delete()
.eq('id', accountId);

View File

@@ -30,6 +30,7 @@ export class AdminDashboardService {
};
const subscriptionsPromise = this.client
.schema('medreport')
.from('subscriptions')
.select('*', selectParams)
.eq('status', 'active')
@@ -47,6 +48,7 @@ export class AdminDashboardService {
});
const trialsPromise = this.client
.schema('medreport')
.from('subscriptions')
.select('*', selectParams)
.eq('status', 'trialing')
@@ -64,6 +66,7 @@ export class AdminDashboardService {
});
const accountsPromise = this.client
.schema('medreport')
.from('accounts')
.select('*', selectParams)
.eq('is_personal_account', true)
@@ -81,6 +84,7 @@ export class AdminDashboardService {
});
const teamAccountsPromise = this.client
.schema('medreport')
.from('accounts')
.select('*', selectParams)
.eq('is_personal_account', false)

View File

@@ -16,7 +16,8 @@
"./mfa": "./src/mfa.ts",
"./captcha/client": "./src/captcha/client/index.ts",
"./captcha/server": "./src/captcha/server/index.ts",
"./resend-email-link": "./src/components/resend-auth-link-form.tsx"
"./resend-email-link": "./src/components/resend-auth-link-form.tsx",
"./lib/utils/*": "./src/lib/utils/*.ts"
},
"devDependencies": {
"@hookform/resolvers": "^5.0.1",
@@ -34,7 +35,6 @@
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",

View File

@@ -47,7 +47,7 @@ export function MultiFactorChallengeContainer({
const verifyMFAChallenge = useVerifyMFAChallenge({
onSuccess: () => {
router.replace(paths.redirectPath);
router.replace('/');
},
});

View File

@@ -55,14 +55,13 @@ export function SignInMethodsContainer(props: {
}
try {
const { data: hasPersonalCode } = await client.rpc(
'has_personal_code',
{
const { data: hasConsentPersonalData } = await client
.schema('medreport')
.rpc('has_consent_personal_data', {
account_id: userId,
},
);
});
if (hasPersonalCode) {
if (hasConsentPersonalData) {
router.replace(props.paths.returnPath);
} else {
router.replace(props.paths.updateAccount);

View File

@@ -23,7 +23,7 @@ export interface AccountSubmitData {
}
export const onUpdateAccount = enhanceAction(
async (params) => {
async (params: AccountSubmitData) => {
const client = getSupabaseServerClient();
const api = createAuthApi(client);
@@ -36,7 +36,14 @@ export const onUpdateAccount = enhanceAction(
}
console.warn('On update account error: ', err);
}
redirect(pathsConfig.auth.updateAccountSuccess);
const hasUnseenMembershipConfirmation =
await api.hasUnseenMembershipConfirmation();
if (hasUnseenMembershipConfirmation) {
redirect(pathsConfig.auth.membershipConfirmation);
} else {
redirect(pathsConfig.app.selectPackage);
}
},
{
schema: UpdateAccountSchema,

View File

@@ -13,13 +13,22 @@ class AuthApi {
constructor(private readonly client: SupabaseClient<Database>) {}
/**
* @name hasPersonalCode
* @description Check if given account ID has added personal code.
* @param id
* @name hasUnseenMembershipConfirmation
* @description Check if given user ID has any unseen membership confirmation.
*/
async hasPersonalCode(id: string) {
const { data, error } = await this.client.rpc('has_personal_code', {
account_id: id,
async hasUnseenMembershipConfirmation() {
const {
data: { user },
} = await this.client.auth.getUser();
if (!user) {
throw new Error('User not authenticated');
}
const { data, error } = await this.client
.schema('medreport')
.rpc('has_unseen_membership_confirmation', {
p_user_id: user.id,
});
if (error) {
@@ -43,7 +52,9 @@ class AuthApi {
throw new Error('User not authenticated');
}
const { error } = await this.client.rpc('update_account', {
const { error } = await this.client
.schema('medreport')
.rpc('update_account', {
p_name: data.firstName,
p_last_name: data.lastName,
p_personal_code: data.personalCode,
@@ -75,8 +86,11 @@ class AuthApi {
if (!user) {
throw new Error('User not authenticated');
}
console.log('test', user, data);
const response = await this.client.from('account_params').insert({
const response = await this.client
.schema('medreport')
.from('account_params')
.insert({
account_id: user.id,
height: data.height,
weight: data.weight,

View File

@@ -1,29 +1,28 @@
import { HttpTypes } from "@medusajs/types"
import { NextRequest, NextResponse } from "next/server"
import { HttpTypes } from "@medusajs/types";
import { NextRequest, NextResponse } from "next/server";
const BACKEND_URL = process.env.MEDUSA_BACKEND_URL
const PUBLISHABLE_API_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY
const DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "us"
const BACKEND_URL = process.env.MEDUSA_BACKEND_URL;
const PUBLISHABLE_API_KEY = process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY;
const DEFAULT_REGION = process.env.NEXT_PUBLIC_DEFAULT_REGION || "us";
const regionMapCache = {
regionMap: new Map<string, HttpTypes.StoreRegion>(),
regionMapUpdated: Date.now(),
}
};
async function getRegionMap(cacheId: string) {
const { regionMap, regionMapUpdated } = regionMapCache
const { regionMap, regionMapUpdated } = regionMapCache;
if (!BACKEND_URL) {
throw new Error(
"Middleware.ts: Error fetching regions. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
)
);
}
if (
!regionMap.keys().next().value ||
regionMapUpdated < Date.now() - 3600 * 1000
) {
console.log("PUBLISHABLE_API_KEY", PUBLISHABLE_API_KEY)
// Fetch regions from Medusa. We can't use the JS client here because middleware is running on Edge and the client needs a Node environment.
const { regions } = await fetch(`${BACKEND_URL}/store/regions`, {
headers: {
@@ -35,32 +34,32 @@ async function getRegionMap(cacheId: string) {
},
cache: "force-cache",
}).then(async (response) => {
const json = await response.json()
const json = await response.json();
if (!response.ok) {
throw new Error(json.message)
throw new Error(json.message);
}
return json
})
return json;
});
if (!regions?.length) {
throw new Error(
"No regions found. Please set up regions in your Medusa Admin."
)
);
}
// Create a map of country codes to regions.
regions.forEach((region: HttpTypes.StoreRegion) => {
region.countries?.forEach((c) => {
regionMapCache.regionMap.set(c.iso_2 ?? "", region)
})
})
regionMapCache.regionMap.set(c.iso_2 ?? "", region);
});
});
regionMapCache.regionMapUpdated = Date.now()
regionMapCache.regionMapUpdated = Date.now();
}
return regionMapCache.regionMap
return regionMapCache.regionMap;
}
/**
@@ -73,30 +72,32 @@ async function getCountryCode(
regionMap: Map<string, HttpTypes.StoreRegion | number>
) {
try {
let countryCode
let countryCode;
const vercelCountryCode = request.headers
.get("x-vercel-ip-country")
?.toLowerCase()
?.toLowerCase();
const urlCountryCode = request.nextUrl.pathname.split("/")[1]?.toLowerCase()
const urlCountryCode = request.nextUrl.pathname
.split("/")[1]
?.toLowerCase();
if (urlCountryCode && regionMap.has(urlCountryCode)) {
countryCode = urlCountryCode
countryCode = urlCountryCode;
} else if (vercelCountryCode && regionMap.has(vercelCountryCode)) {
countryCode = vercelCountryCode
countryCode = vercelCountryCode;
} else if (regionMap.has(DEFAULT_REGION)) {
countryCode = DEFAULT_REGION
countryCode = DEFAULT_REGION;
} else if (regionMap.keys().next().value) {
countryCode = regionMap.keys().next().value
countryCode = regionMap.keys().next().value;
}
return countryCode
return countryCode;
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error(
"Middleware.ts: Error getting the country code. Did you set up regions in your Medusa Admin and define a MEDUSA_BACKEND_URL environment variable? Note that the variable is no longer named NEXT_PUBLIC_MEDUSA_BACKEND_URL."
)
);
}
}
}
@@ -105,56 +106,56 @@ async function getCountryCode(
* Middleware to handle region selection and onboarding status.
*/
export async function middleware(request: NextRequest) {
let redirectUrl = request.nextUrl.href
let redirectUrl = request.nextUrl.href;
let response = NextResponse.redirect(redirectUrl, 307)
let response = NextResponse.redirect(redirectUrl, 307);
let cacheIdCookie = request.cookies.get("_medusa_cache_id")
let cacheIdCookie = request.cookies.get("_medusa_cache_id");
let cacheId = cacheIdCookie?.value || crypto.randomUUID()
let cacheId = cacheIdCookie?.value || crypto.randomUUID();
const regionMap = await getRegionMap(cacheId)
const regionMap = await getRegionMap(cacheId);
const countryCode = regionMap && (await getCountryCode(request, regionMap))
const countryCode = regionMap && (await getCountryCode(request, regionMap));
const urlHasCountryCode =
countryCode && request.nextUrl.pathname.split("/")[1].includes(countryCode)
countryCode && request.nextUrl.pathname.split("/")[1].includes(countryCode);
// if one of the country codes is in the url and the cache id is set, return next
if (urlHasCountryCode && cacheIdCookie) {
return NextResponse.next()
return NextResponse.next();
}
// if one of the country codes is in the url and the cache id is not set, set the cache id and redirect
if (urlHasCountryCode && !cacheIdCookie) {
response.cookies.set("_medusa_cache_id", cacheId, {
maxAge: 60 * 60 * 24,
})
});
return response
return response;
}
// check if the url is a static asset
if (request.nextUrl.pathname.includes(".")) {
return NextResponse.next()
return NextResponse.next();
}
const redirectPath =
request.nextUrl.pathname === "/" ? "" : request.nextUrl.pathname
request.nextUrl.pathname === "/" ? "" : request.nextUrl.pathname;
const queryString = request.nextUrl.search ? request.nextUrl.search : ""
const queryString = request.nextUrl.search ? request.nextUrl.search : "";
// If no country code is set, we redirect to the relevant region.
if (!urlHasCountryCode && countryCode) {
redirectUrl = `${request.nextUrl.origin}/${countryCode}${redirectPath}${queryString}`
response = NextResponse.redirect(`${redirectUrl}`, 307)
redirectUrl = `${request.nextUrl.origin}/${countryCode}${redirectPath}${queryString}`;
response = NextResponse.redirect(`${redirectUrl}`, 307);
}
return response
return response;
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|images|assets|png|svg|jpg|jpeg|gif|webp).*)",
],
}
};

View File

@@ -24,8 +24,7 @@
"@types/react": "19.1.4",
"lucide-react": "^0.510.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^15.5.1"
"react-dom": "19.1.0"
},
"prettier": "@kit/prettier-config",
"typesVersions": {

View File

@@ -1,3 +1,2 @@
export * from './notifications-popover';
export * from './success-notification';
export * from './update-account-success-notification';

View File

@@ -14,7 +14,7 @@ import { cn } from '@kit/ui/utils';
import { useDismissNotification, useFetchNotifications } from '../hooks';
type Notification = Database['public']['Tables']['notifications']['Row'];
type Notification = Database['medreport']['Tables']['notifications']['Row'];
type PartialNotification = Pick<
Notification,
@@ -121,7 +121,10 @@ export function NotificationsPopover(params: {
return (
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button className={'relative px-4 py-2 h-10 border-1 mr-0'} variant="ghost">
<Button
className={'relative mr-0 h-10 border-1 px-4 py-2'}
variant="ghost"
>
<Bell className={'size-4'} />
<span

View File

@@ -11,12 +11,17 @@ export const SuccessNotification = ({
title,
titleKey,
descriptionKey,
walletProps,
buttonProps,
}: {
showLogo?: boolean;
title?: string;
titleKey?: string;
descriptionKey?: string;
walletProps?: {
amount: string;
expiredAt: string;
};
buttonProps?: {
buttonTitleKey: string;
href: string;
@@ -38,6 +43,16 @@ export const SuccessNotification = ({
<Trans i18nKey={descriptionKey} />
</p>
</div>
{walletProps && (
<div>
<Trans i18nKey="common.wallet.balance" />
<p></p>
<Trans
i18nKey="common.wallet.expiredAt"
values={{ expiredAt: '12.12.2025' }}
/>
</div>
)}
{buttonProps && (
<Button className="mt-8 w-full">
<Link href={buttonProps.href}>

View File

@@ -8,6 +8,7 @@ export function useDismissNotification() {
return useCallback(
async (notification: number) => {
const { error } = await client
.schema('medreport')
.from('notifications')
.update({ dismissed: true })
.eq('id', notification);

View File

@@ -49,6 +49,7 @@ function useFetchInitialNotifications(props: { accountIds: string[] }) {
queryKey: ['notifications', ...props.accountIds],
queryFn: async () => {
const { data } = await client
.schema('medreport')
.from('notifications')
.select(
`id,

View File

@@ -22,7 +22,7 @@ import { Database } from '@kit/supabase/database';
import { createNotificationsService } from './notifications.service';
type Notification = Database['public']['Tables']['notifications'];
type Notification = Database['medreport']['Tables']['notifications'];
/**
* @name createNotificationsApi

View File

@@ -4,7 +4,7 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
type Notification = Database['public']['Tables']['notifications'];
type Notification = Database['medreport']['Tables']['notifications'];
export function createNotificationsService(client: SupabaseClient<Database>) {
return new NotificationsService(client);
@@ -14,7 +14,10 @@ class NotificationsService {
constructor(private readonly client: SupabaseClient<Database>) {}
async createNotification(params: Notification['Insert']) {
const { error } = await this.client.from('notifications').insert(params);
const { error } = await this.client
.schema('medreport')
.from('notifications')
.insert(params);
if (error) {
throw error;

View File

@@ -45,7 +45,6 @@
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",

View File

@@ -27,7 +27,7 @@ import { RenewInvitationDialog } from './renew-invitation-dialog';
import { UpdateInvitationDialog } from './update-invitation-dialog';
type Invitations =
Database['public']['Functions']['get_account_invitations']['Returns'];
Database['medreport']['Functions']['get_account_invitations']['Returns'];
type AccountInvitationsTableProps = {
invitations: Invitations;

View File

@@ -27,7 +27,7 @@ import { TransferOwnershipDialog } from './transfer-ownership-dialog';
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
type Members =
Database['public']['Functions']['get_account_members']['Returns'];
Database['medreport']['Functions']['get_account_members']['Returns'];
interface Permissions {
canUpdateRole: (roleHierarchy: number) => boolean;

View File

@@ -21,7 +21,7 @@ export function RolesDataProvider(props: {
}
function useFetchRoles(props: { maxRoleHierarchy: number }) {
const supabase = useSupabase();
const supabase = useSupabase().schema('medreport');
return useQuery({
queryKey: ['roles', props.maxRoleHierarchy],

View File

@@ -53,6 +53,7 @@ export function UpdateTeamAccountImage(props: {
uploadUserProfilePhoto(client, file, props.account.id).then(
(pictureUrl) => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: pictureUrl,
@@ -68,6 +69,7 @@ export function UpdateTeamAccountImage(props: {
const promise = () =>
removeExistingStorageFile().then(() => {
return client
.schema('medreport')
.from('accounts')
.update({
picture_url: null,

View File

@@ -7,8 +7,8 @@ import { User } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
interface AccountWorkspace {
accounts: Database['public']['Views']['user_accounts']['Row'][];
account: Database['public']['Functions']['team_account_workspace']['Returns'][0];
accounts: Database['medreport']['Views']['user_accounts']['Row'][];
account: Database['medreport']['Functions']['team_account_workspace']['Returns'][0];
user: User;
}

View File

@@ -25,7 +25,6 @@ export const TeamNameSchema = z
.max(50)
.refine(
(name) => {
console.log(name);
return !SPECIAL_CHARACTERS_REGEX.test(name);
},
{

View File

@@ -22,6 +22,7 @@ export const updateTeamAccountName = enhanceAction(
logger.info(ctx, `Updating team name...`);
const { error, data } = await client
.schema('medreport')
.from('accounts')
.update({
name,

View File

@@ -6,6 +6,8 @@ import { redirect } from 'next/navigation';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { createNotificationsApi } from '@kit/notifications/api';
import { getLogger } from '@kit/shared/logger';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
@@ -16,8 +18,6 @@ import { RenewInvitationSchema } from '../../schema/renew-invitation.schema';
import { UpdateInvitationSchema } from '../../schema/update-invitation.schema';
import { createAccountInvitationsService } from '../services/account-invitations.service';
import { createAccountPerSeatBillingService } from '../services/account-per-seat-billing.service';
import { createNotificationsApi } from '@kit/notifications/api';
import { getLogger } from '@kit/shared/logger';
/**
* @name createInvitationsAction
@@ -32,48 +32,54 @@ export const createInvitationsAction = enhanceAction(
const service = createAccountInvitationsService(client);
const api = createNotificationsApi(serviceClient);
//TODO: This does not send email. It only creates invitations in the database.
await service.sendInvitations(params);
const { invitations: invitationParams, accountSlug } = params;
const personalCodes = invitationParams.map(({ personal_code }) => personal_code);
const personalCodes = invitationParams.map(
({ personal_code }) => personal_code,
);
const { data: company, error: companyError } = await client
.schema('medreport')
.from('accounts')
.select('id')
.eq('slug', accountSlug);
logger.debug({ company, companyError, personalCodes })
logger.debug({ company, companyError, personalCodes });
if (companyError || !company?.length || !company[0]) {
throw new Error(`Failed to fetch company id: ${companyError?.message || 'not found'}`);
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',
{
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 })
logger.debug({ invitations, invitationError });
if (invitationError) {
throw new Error(`Failed to fetch invitations with accounts: ${invitationError.message}`);
throw new Error(
`Failed to fetch invitations with accounts: ${invitationError.message}`,
);
}
const notificationPromises = invitations
.map(({ invite_token, account_id }) =>
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')
logger.info('All invitation notifications are sent');
revalidateMemberPage();
return {

View File

@@ -79,7 +79,9 @@ export const transferOwnershipAction = enhanceAction(
logger.info(ctx, 'Processing team ownership transfer request...');
// assert that the user is the owner of the account
const { data: isOwner, error } = await client.rpc('is_account_owner', {
const { data: isOwner, error } = await client
.schema('medreport')
.rpc('is_account_owner', {
account_id: data.accountId,
});

View File

@@ -17,6 +17,7 @@ export class TeamAccountsApi {
*/
async getTeamAccount(slug: string) {
const { data, error } = await this.client
.schema('medreport')
.from('accounts')
.select('*')
.eq('slug', slug)
@@ -36,6 +37,7 @@ export class TeamAccountsApi {
*/
async getTeamAccountById(accountId: string) {
const { data, error } = await this.client
.schema('medreport')
.from('accounts')
.select('*')
.eq('id', accountId)
@@ -55,6 +57,7 @@ export class TeamAccountsApi {
*/
async getSubscription(accountId: string) {
const { data, error } = await this.client
.schema('medreport')
.from('subscriptions')
.select('*, items: subscription_items !inner (*)')
.eq('account_id', accountId)
@@ -73,6 +76,7 @@ export class TeamAccountsApi {
*/
async getOrder(accountId: string) {
const response = await this.client
.schema('medreport')
.from('orders')
.select('*, items: order_items !inner (*)')
.eq('account_id', accountId)
@@ -91,13 +95,17 @@ export class TeamAccountsApi {
* @param slug
*/
async getAccountWorkspace(slug: string, userId: string) {
const accountPromise = this.client.rpc('team_account_workspace', {
const accountPromise = this.client
.schema('medreport')
.rpc('team_account_workspace', {
account_slug: slug,
});
const accountsPromise = this.client
.schema('medreport')
.from('accounts_memberships')
.select(`
.select(
`
account_id,
user_accounts (
id,
@@ -106,7 +114,8 @@ export class TeamAccountsApi {
slug,
picture_url
)
`)
`,
)
.eq('user_id', userId)
.eq('account_role', 'owner');
@@ -154,7 +163,7 @@ export class TeamAccountsApi {
async hasPermission(params: {
accountId: string;
userId: string;
permission: Database['public']['Enums']['app_permissions'];
permission: Database['medreport']['Enums']['app_permissions'];
}) {
const { data, error } = await this.client.rpc('has_permission', {
account_id: params.accountId,
@@ -176,6 +185,7 @@ export class TeamAccountsApi {
*/
async getMembersCount(accountId: string) {
const { count, error } = await this.client
.schema('medreport')
.from('accounts_memberships')
.select('*', {
head: true,
@@ -197,6 +207,7 @@ export class TeamAccountsApi {
*/
async getCustomerId(accountId: string) {
const { data, error } = await this.client
.schema('medreport')
.from('billing_customers')
.select('customer_id')
.eq('account_id', accountId)
@@ -217,6 +228,7 @@ export class TeamAccountsApi {
*/
async getInvitation(adminClient: SupabaseClient<Database>, token: string) {
const { data: invitation, error } = await adminClient
.schema('medreport')
.from('invitations')
.select<
string,

View File

@@ -43,6 +43,7 @@ class AccountInvitationsService {
logger.info(ctx, 'Removing invitation...');
const { data, error } = await this.client
.schema('medreport')
.from('invitations')
.delete()
.match({
@@ -76,6 +77,7 @@ class AccountInvitationsService {
logger.info(ctx, 'Updating invitation...');
const { data, error } = await this.client
.schema('medreport')
.from('invitations')
.update({
role: params.role,
@@ -105,19 +107,21 @@ class AccountInvitationsService {
invitation: z.infer<typeof InviteMembersSchema>['invitations'][number],
accountSlug: string,
) {
const { data: members, error } = await this.client.rpc(
'get_account_members',
{
const { data: members, error } = await this.client
.schema('medreport')
.rpc('get_account_members', {
account_slug: accountSlug,
},
);
});
if (error) {
throw error;
}
const isUserAlreadyMember = members.find((member) => {
return member.email === invitation.email || member.personal_code === invitation.personal_code;
return (
member.email === invitation.email ||
member.personal_code === invitation.personal_code
);
});
if (isUserAlreadyMember) {
@@ -166,6 +170,7 @@ class AccountInvitationsService {
}
const accountResponse = await this.client
.schema('medreport')
.from('accounts')
.select('name')
.eq('slug', accountSlug)
@@ -180,7 +185,9 @@ class AccountInvitationsService {
throw new Error('Account not found');
}
const response = await this.client.rpc('add_invitations_to_account', {
const response = await this.client
.schema('medreport')
.rpc('add_invitations_to_account', {
invitations,
account_slug: accountSlug,
});
@@ -229,7 +236,9 @@ class AccountInvitationsService {
logger.info(ctx, 'Accepting invitation to team');
const { error, data } = await adminClient.rpc('accept_invitation', {
const { error, data } = await adminClient
.schema('medreport')
.rpc('accept_invitation', {
token: params.inviteToken,
user_id: params.userId,
});
@@ -269,6 +278,7 @@ class AccountInvitationsService {
const sevenDaysFromNow = formatISO(addDays(new Date(), 7));
const { data, error } = await this.client
.schema('medreport')
.from('invitations')
.update({
expires_at: sevenDaysFromNow,

View File

@@ -37,6 +37,7 @@ class AccountMembersService {
logger.info(ctx, `Removing member from account...`);
const { data, error } = await this.client
.schema('medreport')
.from('accounts_memberships')
.delete()
.match({
@@ -88,7 +89,7 @@ class AccountMembersService {
logger.info(ctx, `Validating permissions to update member role...`);
const { data: canActionAccountMember, error: accountError } =
await this.client.rpc('can_action_account_member', {
await this.client.schema('medreport').rpc('can_action_account_member', {
target_user_id: params.userId,
target_team_account_id: params.accountId,
});
@@ -112,6 +113,7 @@ class AccountMembersService {
// for updating accounts_memberships. Instead, we use the can_action_account_member
// RPC to validate permissions to update the role
const { data, error } = await adminClient
.schema('medreport')
.from('accounts_memberships')
.update({
account_role: params.role,
@@ -157,13 +159,12 @@ class AccountMembersService {
logger.info(ctx, `Transferring ownership of account...`);
const { data, error } = await adminClient.rpc(
'transfer_team_account_ownership',
{
const { data, error } = await adminClient
.schema('medreport')
.rpc('transfer_team_account_ownership', {
target_account_id: params.accountId,
new_owner_id: params.userId,
},
);
});
if (error) {
logger.error(

View File

@@ -36,6 +36,7 @@ class AccountPerSeatBillingService {
);
const { data, error } = await this.client
.schema('medreport')
.from('subscriptions')
.select(
`

View File

@@ -22,7 +22,9 @@ class CreateTeamAccountService {
logger.info(ctx, `Creating new team account...`);
const { error, data } = await this.client.rpc('create_team_account', {
const { error, data } = await this.client
.schema('medreport')
.rpc('create_team_account', {
account_name: params.name,
});

View File

@@ -40,6 +40,7 @@ class DeleteTeamAccountService {
// we can use the admin client to delete the account.
const { error } = await adminClient
.schema('medreport')
.from('accounts')
.delete()
.eq('id', params.accountId);

View File

@@ -45,6 +45,7 @@ class LeaveTeamAccountService {
const { accountId, userId } = Schema.parse(params);
const { error } = await this.adminClient
.schema('medreport')
.from('accounts_memberships')
.delete()
.match({

View File

@@ -5,7 +5,7 @@ import { z } from 'zod';
import { getLogger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
type Invitation = Database['public']['Tables']['invitations']['Row'];
type Invitation = Database['medreport']['Tables']['invitations']['Row'];
const invitePath = '/join';
@@ -72,6 +72,7 @@ class AccountInvitationsWebhookService {
);
const inviter = await this.adminClient
.schema('medreport')
.from('accounts')
.select('email, name')
.eq('id', invitation.invited_by)
@@ -90,6 +91,7 @@ class AccountInvitationsWebhookService {
}
const team = await this.adminClient
.schema('medreport')
.from('accounts')
.select('name')
.eq('id', invitation.account_id)

View File

@@ -3,7 +3,7 @@ import { z } from 'zod';
import { getLogger } from '@kit/shared/logger';
import { Database } from '@kit/supabase/database';
type Account = Database['public']['Tables']['accounts']['Row'];
type Account = Database['medreport']['Tables']['accounts']['Row'];
export function createAccountWebhooksService() {
return new AccountWebhooksService();

View File

@@ -24,7 +24,7 @@
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^15.5.1"
"react-i18next": "^15.5.3"
},
"dependencies": {
"i18next": "25.1.3",

View File

@@ -74,7 +74,7 @@ class OtpService {
logger.info(ctx, 'Creating one-time token');
try {
const result = await this.client.rpc('create_nonce', {
const result = await this.client.schema('medreport').rpc('create_nonce', {
p_user_id: userId,
p_purpose: purpose,
p_expires_in_seconds: expiresInSeconds,
@@ -135,7 +135,7 @@ class OtpService {
logger.info(ctx, 'Verifying one-time token');
try {
const result = await this.client.rpc('verify_nonce', {
const result = await this.client.schema('medreport').rpc('verify_nonce', {
p_token: token,
p_user_id: params.userId,
p_purpose: purpose,
@@ -184,7 +184,9 @@ class OtpService {
logger.info(ctx, 'Revoking one-time token');
try {
const { data, error } = await this.client.rpc('revoke_nonce', {
const { data, error } = await this.client
.schema('medreport')
.rpc('revoke_nonce', {
p_id: id,
p_reason: reason,
});
@@ -225,7 +227,9 @@ class OtpService {
logger.info(ctx, 'Getting one-time token status');
try {
const result = await this.client.rpc('get_nonce_status', {
const result = await this.client
.schema('medreport')
.rpc('get_nonce_status', {
p_id: id,
});

View File

@@ -17,7 +17,7 @@ export type Database = {
changed_data: Json | null
id: number
operation: string
record_key: number | null
record_key: string | null
row_data: Json | null
schema_name: string
table_name: string
@@ -29,7 +29,7 @@ export type Database = {
changed_data?: Json | null
id?: number
operation: string
record_key?: number | null
record_key?: string | null
row_data?: Json | null
schema_name: string
table_name: string
@@ -41,7 +41,7 @@ export type Database = {
changed_data?: Json | null
id?: number
operation?: string
record_key?: number | null
record_key?: string | null
row_data?: Json | null
schema_name?: string
table_name?: string
@@ -157,7 +157,7 @@ export type Database = {
[_ in never]: never
}
}
public: {
medreport: {
Tables: {
account_params: {
Row: {
@@ -189,7 +189,6 @@ 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
@@ -209,7 +208,6 @@ 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
@@ -229,7 +227,6 @@ 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
@@ -252,6 +249,7 @@ export type Database = {
account_role: string
created_at: string
created_by: string | null
has_seen_confirmation: boolean
updated_at: string
updated_by: string | null
user_id: string
@@ -261,6 +259,7 @@ export type Database = {
account_role: string
created_at?: string
created_by?: string | null
has_seen_confirmation?: boolean
updated_at?: string
updated_by?: string | null
user_id: string
@@ -270,6 +269,7 @@ export type Database = {
account_role?: string
created_at?: string
created_by?: string | null
has_seen_confirmation?: boolean
updated_at?: string
updated_by?: string | null
user_id?: string
@@ -435,7 +435,7 @@ export type Database = {
analysis_ids: number[] | null
created_at: string
id: number
status: Database["public"]["Enums"]["analysis_order_status"]
status: Database["medreport"]["Enums"]["analysis_order_status"]
user_id: string
}
Insert: {
@@ -443,7 +443,7 @@ export type Database = {
analysis_ids?: number[] | null
created_at?: string
id?: number
status: Database["public"]["Enums"]["analysis_order_status"]
status: Database["medreport"]["Enums"]["analysis_order_status"]
user_id: string
}
Update: {
@@ -451,7 +451,7 @@ export type Database = {
analysis_ids?: number[] | null
created_at?: string
id?: number
status?: Database["public"]["Enums"]["analysis_order_status"]
status?: Database["medreport"]["Enums"]["analysis_order_status"]
user_id?: string
}
Relationships: []
@@ -521,7 +521,7 @@ export type Database = {
created_at: string
id: number
order_number: string
order_status: Database["public"]["Enums"]["analysis_order_status"]
order_status: Database["medreport"]["Enums"]["analysis_order_status"]
updated_at: string | null
user_id: string
}
@@ -530,7 +530,7 @@ export type Database = {
created_at?: string
id?: number
order_number: string
order_status: Database["public"]["Enums"]["analysis_order_status"]
order_status: Database["medreport"]["Enums"]["analysis_order_status"]
updated_at?: string | null
user_id: string
}
@@ -539,7 +539,7 @@ export type Database = {
created_at?: string
id?: number
order_number?: string
order_status?: Database["public"]["Enums"]["analysis_order_status"]
order_status?: Database["medreport"]["Enums"]["analysis_order_status"]
updated_at?: string | null
user_id?: string
}
@@ -559,21 +559,21 @@ export type Database = {
customer_id: string
email: string | null
id: number
provider: Database["public"]["Enums"]["billing_provider"]
provider: Database["medreport"]["Enums"]["billing_provider"]
}
Insert: {
account_id: string
customer_id: string
email?: string | null
id?: number
provider: Database["public"]["Enums"]["billing_provider"]
provider: Database["medreport"]["Enums"]["billing_provider"]
}
Update: {
account_id?: string
customer_id?: string
email?: string | null
id?: number
provider?: Database["public"]["Enums"]["billing_provider"]
provider?: Database["medreport"]["Enums"]["billing_provider"]
}
Relationships: [
{
@@ -662,19 +662,19 @@ export type Database = {
}
config: {
Row: {
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
enable_account_billing: boolean
enable_team_account_billing: boolean
enable_team_accounts: boolean
}
Insert: {
billing_provider?: Database["public"]["Enums"]["billing_provider"]
billing_provider?: Database["medreport"]["Enums"]["billing_provider"]
enable_account_billing?: boolean
enable_team_account_billing?: boolean
enable_team_accounts?: boolean
}
Update: {
billing_provider?: Database["public"]["Enums"]["billing_provider"]
billing_provider?: Database["medreport"]["Enums"]["billing_provider"]
enable_account_billing?: boolean
enable_team_account_billing?: boolean
enable_team_accounts?: boolean
@@ -842,7 +842,6 @@ export type Database = {
id: number
invite_token: string
invited_by: string
personal_code: string | null
role: string
updated_at: string
}
@@ -854,7 +853,6 @@ export type Database = {
id?: number
invite_token: string
invited_by: string
personal_code?: string | null
role: string
updated_at?: string
}
@@ -866,7 +864,6 @@ export type Database = {
id?: number
invite_token?: string
invited_by?: string
personal_code?: string | null
role?: string
updated_at?: string
}
@@ -1085,35 +1082,35 @@ export type Database = {
Row: {
account_id: string
body: string
channel: Database["public"]["Enums"]["notification_channel"]
channel: Database["medreport"]["Enums"]["notification_channel"]
created_at: string
dismissed: boolean
expires_at: string | null
id: number
link: string | null
type: Database["public"]["Enums"]["notification_type"]
type: Database["medreport"]["Enums"]["notification_type"]
}
Insert: {
account_id: string
body: string
channel?: Database["public"]["Enums"]["notification_channel"]
channel?: Database["medreport"]["Enums"]["notification_channel"]
created_at?: string
dismissed?: boolean
expires_at?: string | null
id?: never
link?: string | null
type?: Database["public"]["Enums"]["notification_type"]
type?: Database["medreport"]["Enums"]["notification_type"]
}
Update: {
account_id?: string
body?: string
channel?: Database["public"]["Enums"]["notification_channel"]
channel?: Database["medreport"]["Enums"]["notification_channel"]
created_at?: string
dismissed?: boolean
expires_at?: string | null
id?: never
link?: string | null
type?: Database["public"]["Enums"]["notification_type"]
type?: Database["medreport"]["Enums"]["notification_type"]
}
Relationships: [
{
@@ -1184,33 +1181,33 @@ export type Database = {
Row: {
account_id: string
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
created_at: string
currency: string
id: string
status: Database["public"]["Enums"]["payment_status"]
status: Database["medreport"]["Enums"]["payment_status"]
total_amount: number
updated_at: string
}
Insert: {
account_id: string
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
created_at?: string
currency: string
id: string
status: Database["public"]["Enums"]["payment_status"]
status: Database["medreport"]["Enums"]["payment_status"]
total_amount: number
updated_at?: string
}
Update: {
account_id?: string
billing_customer_id?: number
billing_provider?: Database["public"]["Enums"]["billing_provider"]
billing_provider?: Database["medreport"]["Enums"]["billing_provider"]
created_at?: string
currency?: string
id?: string
status?: Database["public"]["Enums"]["payment_status"]
status?: Database["medreport"]["Enums"]["payment_status"]
total_amount?: number
updated_at?: string
}
@@ -1248,17 +1245,17 @@ export type Database = {
role_permissions: {
Row: {
id: number
permission: Database["public"]["Enums"]["app_permissions"]
permission: Database["medreport"]["Enums"]["app_permissions"]
role: string
}
Insert: {
id?: number
permission: Database["public"]["Enums"]["app_permissions"]
permission: Database["medreport"]["Enums"]["app_permissions"]
role: string
}
Update: {
id?: number
permission?: Database["public"]["Enums"]["app_permissions"]
permission?: Database["medreport"]["Enums"]["app_permissions"]
role?: string
}
Relationships: [
@@ -1296,7 +1293,7 @@ export type Database = {
product_id: string
quantity: number
subscription_id: string
type: Database["public"]["Enums"]["subscription_item_type"]
type: Database["medreport"]["Enums"]["subscription_item_type"]
updated_at: string
variant_id: string
}
@@ -1309,7 +1306,7 @@ export type Database = {
product_id: string
quantity?: number
subscription_id: string
type: Database["public"]["Enums"]["subscription_item_type"]
type: Database["medreport"]["Enums"]["subscription_item_type"]
updated_at?: string
variant_id: string
}
@@ -1322,7 +1319,7 @@ export type Database = {
product_id?: string
quantity?: number
subscription_id?: string
type?: Database["public"]["Enums"]["subscription_item_type"]
type?: Database["medreport"]["Enums"]["subscription_item_type"]
updated_at?: string
variant_id?: string
}
@@ -1341,14 +1338,14 @@ export type Database = {
account_id: string
active: boolean
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
created_at: string
currency: string
id: string
period_ends_at: string
period_starts_at: string
status: Database["public"]["Enums"]["subscription_status"]
status: Database["medreport"]["Enums"]["subscription_status"]
trial_ends_at: string | null
trial_starts_at: string | null
updated_at: string
@@ -1357,14 +1354,14 @@ export type Database = {
account_id: string
active: boolean
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
created_at?: string
currency: string
id: string
period_ends_at: string
period_starts_at: string
status: Database["public"]["Enums"]["subscription_status"]
status: Database["medreport"]["Enums"]["subscription_status"]
trial_ends_at?: string | null
trial_starts_at?: string | null
updated_at?: string
@@ -1373,14 +1370,14 @@ export type Database = {
account_id?: string
active?: boolean
billing_customer_id?: number
billing_provider?: Database["public"]["Enums"]["billing_provider"]
billing_provider?: Database["medreport"]["Enums"]["billing_provider"]
cancel_at_period_end?: boolean
created_at?: string
currency?: string
id?: string
period_ends_at?: string
period_starts_at?: string
status?: Database["public"]["Enums"]["subscription_status"]
status?: Database["medreport"]["Enums"]["subscription_status"]
trial_ends_at?: string | null
trial_starts_at?: string | null
updated_at?: string
@@ -1424,7 +1421,7 @@ export type Database = {
name: string | null
picture_url: string | null
subscription_status:
| Database["public"]["Enums"]["subscription_status"]
| Database["medreport"]["Enums"]["subscription_status"]
| null
}
Relationships: []
@@ -1458,16 +1455,12 @@ export type Database = {
account_slug: string
invitations: Database["public"]["CompositeTypes"]["invitation"][]
}
Returns: Database["public"]["Tables"]["invitations"]["Row"][]
Returns: Database["medreport"]["Tables"]["invitations"]["Row"][]
}
can_action_account_member: {
Args: { target_team_account_id: string; target_user_id: string }
Returns: boolean
}
check_personal_code_exists: {
Args: { code: string }
Returns: boolean
}
create_invitation: {
Args: { account_id: string; email: string; role: string }
Returns: {
@@ -1478,7 +1471,6 @@ export type Database = {
id: number
invite_token: string
invited_by: string
personal_code: string | null
role: string
updated_at: string
}
@@ -1501,7 +1493,6 @@ 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
@@ -1528,7 +1519,6 @@ export type Database = {
created_at: string
updated_at: string
expires_at: string
personal_code: string
inviter_name: string
inviter_email: string
}[]
@@ -1544,7 +1534,6 @@ export type Database = {
primary_owner_user_id: string
name: string
email: string
personal_code: string
picture_url: string
created_at: string
updated_at: string
@@ -1554,14 +1543,6 @@ export type Database = {
Args: Record<PropertyKey, never>
Returns: Json
}
get_invitations_with_account_ids: {
Args: { company_id: string; personal_codes: string[] }
Returns: {
invite_token: string
personal_code: string
account_id: string
}[]
}
get_nonce_status: {
Args: { p_id: string }
Returns: Json
@@ -1574,6 +1555,10 @@ export type Database = {
Args: { target_account_id: string }
Returns: boolean
}
has_consent_personal_data: {
Args: { account_id: string }
Returns: boolean
}
has_more_elevated_role: {
Args: {
target_user_id: string
@@ -1586,14 +1571,10 @@ export type Database = {
Args: {
user_id: string
account_id: string
permission_name: Database["public"]["Enums"]["app_permissions"]
permission_name: Database["medreport"]["Enums"]["app_permissions"]
}
Returns: boolean
}
has_personal_code: {
Args: { account_id: string }
Returns: boolean
}
has_role_on_account: {
Args: { account_id: string; account_role?: string }
Returns: boolean
@@ -1606,6 +1587,10 @@ export type Database = {
}
Returns: boolean
}
has_unseen_membership_confirmation: {
Args: { p_user_id?: string }
Returns: boolean
}
is_aal2: {
Args: Record<PropertyKey, never>
Returns: boolean
@@ -1618,10 +1603,6 @@ export type Database = {
Args: { target_account_id: string }
Returns: boolean
}
is_company_admin: {
Args: { account_slug: string }
Returns: boolean
}
is_mfa_compliant: {
Args: Record<PropertyKey, never>
Returns: boolean
@@ -1652,8 +1633,8 @@ export type Database = {
role: string
role_hierarchy_level: number
primary_owner_user_id: string
subscription_status: Database["public"]["Enums"]["subscription_status"]
permissions: Database["public"]["Enums"]["app_permissions"][]
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
permissions: Database["medreport"]["Enums"]["app_permissions"][]
}[]
}
transfer_team_account_ownership: {
@@ -1677,8 +1658,8 @@ export type Database = {
target_account_id: string
target_customer_id: string
target_order_id: string
status: Database["public"]["Enums"]["payment_status"]
billing_provider: Database["public"]["Enums"]["billing_provider"]
status: Database["medreport"]["Enums"]["payment_status"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
total_amount: number
currency: string
line_items: Json
@@ -1686,11 +1667,11 @@ export type Database = {
Returns: {
account_id: string
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
created_at: string
currency: string
id: string
status: Database["public"]["Enums"]["payment_status"]
status: Database["medreport"]["Enums"]["payment_status"]
total_amount: number
updated_at: string
}
@@ -1701,8 +1682,8 @@ export type Database = {
target_customer_id: string
target_subscription_id: string
active: boolean
status: Database["public"]["Enums"]["subscription_status"]
billing_provider: Database["public"]["Enums"]["billing_provider"]
status: Database["medreport"]["Enums"]["subscription_status"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
currency: string
period_starts_at: string
@@ -1715,14 +1696,14 @@ export type Database = {
account_id: string
active: boolean
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
created_at: string
currency: string
id: string
period_ends_at: string
period_starts_at: string
status: Database["public"]["Enums"]["subscription_status"]
status: Database["medreport"]["Enums"]["subscription_status"]
trial_ends_at: string | null
trial_starts_at: string | null
updated_at: string
@@ -1770,6 +1751,51 @@ export type Database = {
| "incomplete_expired"
| "paused"
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
get_invitations_with_account_ids: {
Args: { company_id: string; personal_codes: string[] }
Returns: {
invite_token: string
personal_code: string
account_id: string
}[]
}
has_permission: {
Args: {
user_id: string
account_id: string
permission_name: Database["public"]["Enums"]["app_permissions"]
}
Returns: boolean
}
has_role_on_account: {
Args: { account_id: string; account_role?: string }
Returns: boolean
}
is_company_admin: {
Args: { account_slug: string }
Returns: boolean
}
}
Enums: {
app_permissions:
| "roles.manage"
| "billing.manage"
| "settings.manage"
| "members.manage"
| "invites.manage"
}
CompositeTypes: {
invitation: {
email: string | null
@@ -1895,7 +1921,7 @@ export const Constants = {
graphql_public: {
Enums: {},
},
public: {
medreport: {
Enums: {
analysis_order_status: [
"QUEUED",
@@ -1929,5 +1955,16 @@ export const Constants = {
],
},
},
public: {
Enums: {
app_permissions: [
"roles.manage",
"billing.manage",
"settings.manage",
"members.manage",
"invites.manage",
],
},
},
} as const

View File

@@ -55,7 +55,6 @@
"prettier": "^3.5.3",
"react-day-picker": "^8.10.1",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"sonner": "^2.0.3",
"tailwindcss": "4.1.7",
"tailwindcss-animate": "^1.0.7",

11780
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -126,10 +126,7 @@
"description": "Jätkamiseks palun sisestage enda isikuandmed",
"button": "Jätka",
"userConsentLabel": "Nõustun isikuandmete kasutamisega platvormil",
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid",
"successTitle": "Tere, {{firstName}} {{lastName}}",
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
"successButton": "Jätka"
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid"
},
"consentModal": {
"title": "Enne toimetama hakkamist",
@@ -143,5 +140,10 @@
"consentToAnonymizedCompanyData": {
"label": "Nõustun osalema tööandja statistikas",
"description": "Nõustun anonümiseeritud kujul terviseandmete kasutamisega tööandja statistikas"
},
"membershipConfirmation": {
"successTitle": "Tere, {{firstName}} {{lastName}}",
"successDescription": "Teie tervisekonto on aktiveeritud ja kasutamiseks valmis!",
"successButton": "Jätka"
}
}

View File

@@ -120,5 +120,9 @@
"city": "Linn",
"weight": "Kaal",
"height": "Pikkus"
},
"wallet": {
"balance": "Sinu MedReporti konto seis",
"expiredAt": "Kehtiv kuni {{expiredAt}}"
}
}

View File

@@ -2157,6 +2157,9 @@ AS $function$select
or has_role_on_account.account_role is null)));$function$
;
grant
execute on function medreport.has_role_on_account (uuid, varchar) to authenticated;
CREATE OR REPLACE FUNCTION medreport.has_same_role_hierarchy_level(target_user_id uuid, target_account_id uuid, role_name character varying)
RETURNS boolean
LANGUAGE plpgsql
@@ -2221,6 +2224,10 @@ begin
end;$function$
;
grant
execute on function medreport.has_same_role_hierarchy_level (uuid, uuid, varchar) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION medreport.is_aal2()
RETURNS boolean
LANGUAGE plpgsql
@@ -2236,6 +2243,8 @@ end
$function$
;
grant execute on function medreport.is_aal2() to authenticated;
CREATE OR REPLACE FUNCTION medreport.is_account_owner(account_id uuid)
RETURNS boolean
LANGUAGE sql
@@ -2251,6 +2260,10 @@ AS $function$select
and primary_owner_user_id = auth.uid());$function$
;
grant
execute on function medreport.is_account_owner (uuid) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION medreport.is_account_team_member(target_account_id uuid)
RETURNS boolean
LANGUAGE sql
@@ -2262,6 +2275,10 @@ AS $function$select exists(
);$function$
;
grant
execute on function medreport.is_account_team_member (uuid) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION medreport.is_mfa_compliant()
RETURNS boolean
LANGUAGE plpgsql
@@ -2280,6 +2297,8 @@ AS $function$begin
end$function$
;
grant execute on function medreport.is_mfa_compliant() to authenticated;
CREATE OR REPLACE FUNCTION medreport.is_set(field_name text)
RETURNS boolean
LANGUAGE plpgsql
@@ -2294,6 +2313,9 @@ begin
end;$function$
;
grant
execute on function medreport.is_set (text) to authenticated;
CREATE OR REPLACE FUNCTION medreport.is_super_admin()
RETURNS boolean
LANGUAGE plpgsql
@@ -2311,6 +2333,8 @@ begin
end$function$
;
grant execute on function medreport.is_super_admin() to authenticated;
CREATE OR REPLACE FUNCTION medreport.is_team_member(account_id uuid, user_id uuid)
RETURNS boolean
LANGUAGE sql
@@ -2328,6 +2352,10 @@ AS $function$select
and membership.account_id = is_team_member.account_id);$function$
;
grant
execute on function medreport.is_team_member (uuid, uuid) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION medreport.revoke_nonce(p_id uuid, p_reason text DEFAULT NULL::text)
RETURNS boolean
LANGUAGE plpgsql
@@ -2350,6 +2378,8 @@ BEGIN
END;$function$
;
grant execute on function medreport.revoke_nonce to service_role;
CREATE OR REPLACE FUNCTION medreport.team_account_workspace(account_slug text)
RETURNS TABLE(id uuid, name character varying, picture_url character varying, slug text, role character varying, role_hierarchy_level integer, primary_owner_user_id uuid, subscription_status medreport.subscription_status, permissions medreport.app_permissions[])
LANGUAGE plpgsql
@@ -2489,6 +2519,10 @@ AS $function$begin
end;$function$
;
grant
execute on function medreport.update_account(p_name character varying, p_last_name text, p_personal_code text, p_phone text, p_city text, p_has_consent_personal_data boolean, p_uid uuid) to authenticated,
service_role;
CREATE OR REPLACE FUNCTION medreport.upsert_order(target_account_id uuid, target_customer_id character varying, target_order_id text, status medreport.payment_status, billing_provider medreport.billing_provider, total_amount numeric, currency character varying, line_items jsonb)
RETURNS medreport.orders
LANGUAGE plpgsql
@@ -2589,6 +2623,18 @@ begin
end;$function$
;
grant
execute on function medreport.upsert_order (
uuid,
varchar,
text,
medreport.payment_status,
medreport.billing_provider,
numeric,
varchar,
jsonb
) to service_role;
CREATE OR REPLACE FUNCTION medreport.upsert_subscription(target_account_id uuid, target_customer_id character varying, target_subscription_id text, active boolean, status medreport.subscription_status, billing_provider medreport.billing_provider, cancel_at_period_end boolean, currency character varying, period_starts_at timestamp with time zone, period_ends_at timestamp with time zone, line_items jsonb, trial_starts_at timestamp with time zone DEFAULT NULL::timestamp with time zone, trial_ends_at timestamp with time zone DEFAULT NULL::timestamp with time zone)
RETURNS medreport.subscriptions
LANGUAGE plpgsql
@@ -2716,6 +2762,23 @@ begin
end;$function$
;
grant
execute on function medreport.upsert_subscription (
uuid,
varchar,
text,
bool,
medreport.subscription_status,
medreport.billing_provider,
bool,
varchar,
timestamptz,
timestamptz,
jsonb,
timestamptz,
timestamptz
) to service_role;
create or replace view "medreport"."user_account_workspace" as SELECT accounts.id,
accounts.name,
accounts.picture_url,
@@ -2727,6 +2790,10 @@ create or replace view "medreport"."user_account_workspace" as SELECT accounts.
WHERE ((accounts.primary_owner_user_id = ( SELECT auth.uid() AS uid)) AND (accounts.is_personal_account = true))
LIMIT 1;
grant
select
on medreport.user_account_workspace to authenticated,
service_role;
create or replace view "medreport"."user_accounts" as SELECT account.id,
account.name,
@@ -2739,6 +2806,10 @@ create or replace view "medreport"."user_accounts" as SELECT account.id,
FROM medreport.accounts_memberships
WHERE (accounts_memberships.user_id = ( SELECT auth.uid() AS uid)))));
grant
select
on medreport.user_accounts to authenticated,
service_role;
CREATE OR REPLACE FUNCTION medreport.verify_nonce(p_token text, p_purpose text, p_user_id uuid DEFAULT NULL::uuid, p_required_scopes text[] DEFAULT NULL::text[], p_max_verification_attempts integer DEFAULT 5, p_ip inet DEFAULT NULL::inet, p_user_agent text DEFAULT NULL::text)
RETURNS jsonb
@@ -2836,6 +2907,10 @@ BEGIN
END;$function$
;
grant
execute on function medreport.verify_nonce to authenticated,
service_role;
grant delete on table "medreport"."account_params" to "anon";
grant insert on table "medreport"."account_params" to "anon";

View File

@@ -0,0 +1,43 @@
alter table "medreport"."accounts_memberships" add column "has_seen_confirmation" boolean not null default false;
set check_function_bodies = off;
CREATE OR REPLACE FUNCTION medreport.has_unseen_membership_confirmation(p_user_id uuid DEFAULT auth.uid())
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
SET search_path TO 'medreport', 'extensions'
AS $function$
select exists (
select 1
from medreport.accounts_memberships am
where am.user_id = p_user_id
and am.has_seen_confirmation = false
);
$function$
;
grant execute on function medreport.has_unseen_membership_confirmation(uuid)
to authenticated, anon;
drop function if exists "medreport"."has_personal_code"(account_id uuid);
CREATE OR REPLACE FUNCTION medreport.has_consent_personal_data(account_id uuid)
RETURNS boolean
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $function$BEGIN
RETURN EXISTS (
SELECT 1
FROM medreport.accounts
WHERE id = account_id
AND has_consent_personal_data IS TRUE
);
END;$function$
;
grant execute on function medreport.has_consent_personal_data(uuid)
to authenticated, anon;
grant usage on schema medreport to authenticated;