Merge branch 'main' into B2B-34

This commit is contained in:
Danel Kungla
2025-06-26 16:11:11 +03:00
13 changed files with 1521 additions and 271 deletions

View File

@@ -9,6 +9,8 @@ MEDIPOST_USER=your-medipost-user
MEDIPOST_PASSWORD=your-medipost-password
MEDIPOST_RECIPIENT=your-medipost-recipient
CONNECTED_ONLINE_URL=your-connected-online-url
EMAIL_SENDER=
EMAIL_USER= # refer to your email provider's documentation
EMAIL_PASSWORD= # refer to your email provider's documentation

View File

@@ -25,26 +25,32 @@ async function syncData() {
config({ path: `.env.${process.env.NODE_ENV}` });
}
const baseUrl = process.env.MEDIPOST_URL!;
const user = process.env.MEDIPOST_USER!;
const password = process.env.MEDIPOST_PASSWORD!;
const sender = process.env.MEDIPOST_MESSAGE_SENDER!;
const baseUrl = process.env.MEDIPOST_URL;
const user = process.env.MEDIPOST_USER;
const password = process.env.MEDIPOST_PASSWORD;
const sender = process.env.MEDIPOST_MESSAGE_SENDER;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceRoleKey =
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY;
if (!baseUrl || !user || !password || !sender) {
if (
!baseUrl ||
!supabaseUrl ||
!supabaseServiceRoleKey ||
!user ||
!password ||
!sender
) {
throw new Error('Could not access all necessary environment variables');
}
const supabase = createCustomClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
);
});
try {
// GET LATEST PUBLIC MESSAGE ID

View File

@@ -0,0 +1,150 @@
import { createClient as createCustomClient } from '@supabase/supabase-js';
import axios from 'axios';
import { config } from 'dotenv';
async function syncData() {
if (process.env.NODE_ENV === 'local') {
config({ path: `.env.${process.env.NODE_ENV}` });
}
const isProd = process.env.NODE_ENV === 'production';
const baseUrl = process.env.CONNECTED_ONLINE_URL;
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceRoleKey =
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY;
if (!baseUrl || !supabaseUrl || !supabaseServiceRoleKey) {
throw new Error('Could not access all necessary environment variables');
}
const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, {
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
});
try {
const response = await axios.post(`${baseUrl}/Search_Load`, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: "{'Value':'|et|-1'}", // get all available services in Estonian
});
const responseData: {
Value: any;
Data: any;
ErrorCode: number;
ErrorMessage: string;
} = JSON.parse(response.data.d);
if (responseData?.ErrorCode !== 0) {
throw new Error('Failed to get Connected Online data');
}
if (
!responseData.Data.T_Lic?.length ||
!responseData.Data.T_Service?.length
) {
return supabase.schema('audit').from('sync_entries').insert({
operation: 'CONNECTED_ONLINE_SYNC',
comment: 'No clinic or service data received',
status: 'FAIL',
changed_by_role: 'service_role',
});
}
let clinics;
let services;
// Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment
if (isProd) {
clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID !== 2);
services = responseData.Data.T_Service.filter(
(service) => service.ClinicID !== 2,
);
} else {
clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID === 2);
services = responseData.Data.T_Service.filter(
(service) => service.ClinicID === 2,
);
}
const mappedClinics = clinics.map((clinic) => {
return {
id: clinic.ID,
can_select_worker: !!clinic.OnlineCanSelectWorker,
email: clinic.Email || null,
name: clinic.Name,
personal_code_required: !!clinic.PersonalCodeRequired,
phone_number: clinic.Phone || null,
};
});
const mappedServices = services.map((service) => {
return {
id: service.ID,
clinic_id: service.ClinicID,
code: service.Code,
description: service.Description || null,
display: service.Display,
duration: service.Duration,
has_free_codes: !!service.HasFreeCodes,
name: service.Name,
neto_duration: service.NetoDuration,
online_hide_duration: service.OnlineHideDuration,
online_hide_price: service.OnlineHidePrice,
price: service.Price,
price_periods: service.PricePeriods || null,
requires_payment: !!service.RequiresPayment,
sync_id: service.SyncID,
};
});
const { error: providersError } = await supabase
.from('connected_online_providers')
.upsert(mappedClinics);
const { error: servicesError } = await supabase
.from('connected_online_services')
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });
if (providersError || servicesError) {
return supabase
.schema('audit')
.from('sync_entries')
.insert({
operation: 'CONNECTED_ONLINE_SYNC',
comment: providersError
? 'Error saving providers: ' + JSON.stringify(providersError)
: 'Error saving services: ' + JSON.stringify(servicesError),
status: 'FAIL',
changed_by_role: 'service_role',
});
}
await supabase.schema('audit').from('sync_entries').insert({
operation: 'CONNECTED_ONLINE_SYNC',
status: 'SUCCESS',
changed_by_role: 'service_role',
});
} catch (e) {
await supabase
.schema('audit')
.from('sync_entries')
.insert({
operation: 'CONNECTED_ONLINE_SYNC',
status: 'FAIL',
comment: JSON.stringify(e),
changed_by_role: 'service_role',
});
throw new Error(
`Failed to sync Connected Online data, error: ${JSON.stringify(e)}`,
);
}
}
syncData();

View File

@@ -0,0 +1,48 @@
'use server'
import { createClient } from '@supabase/supabase-js';
import { RequestStatus } from '@/lib/types/audit';
import { ConnectedOnlineMethodName } from '@/lib/types/connected-online';
import { ExternalApi } from '@/lib/types/external';
import { MedipostAction } from '@/lib/types/medipost';
export default async function logRequestResult(
/* personalCode: string, */ requestApi: keyof typeof ExternalApi,
requestApiMethod: `${ConnectedOnlineMethodName}` | `${MedipostAction}`,
status: RequestStatus,
comment?: string,
startTime?: string,
serviceId?: number,
serviceProviderId?: number,
) {
const supabaseServiceUser = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
},
);
const { error } = await supabaseServiceUser
.schema('audit')
.from('request_entries')
.insert({
/* personal_code: personalCode, */
request_api: requestApi,
request_api_method: requestApiMethod,
requested_start_date: startTime,
status,
service_id: serviceId,
service_provider_id: serviceProviderId,
comment,
});
if (error) {
throw new Error('Failed to insert log entry, error: ' + error.message);
}
}

View File

@@ -0,0 +1,268 @@
'use server'
import logRequestResult from '@/lib/services/audit.service';
import { RequestStatus } from '@/lib/types/audit';
import {
AvailableAppointmentsResponse,
BookTimeResponse,
ConfirmedLoadResponse,
ConnectedOnlineMethodName,
} from '@/lib/types/connected-online';
import { ExternalApi } from '@/lib/types/external';
import { Tables } from '@/supabase/database.types';
import { createClient } from '@/utils/supabase/server';
import axios from 'axios';
export async function getAvailableAppointmentsForService(
serviceId: number,
startTime?: Date,
) {
try {
const showTimesFrom = startTime ? { StartTime: startTime } : {};
const response = await axios.post(
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.GetAvailabilities}`,
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: JSON.stringify({
ServiceID: serviceId,
Key: '7T624nlu',
Lang: 'et',
...showTimesFrom,
}),
},
);
const responseData: AvailableAppointmentsResponse = JSON.parse(
response.data.d,
);
if (
responseData?.ErrorCode !== 0 ||
!responseData.Data.T_Service?.length ||
!responseData.Data.T_Booking?.length
) {
let comment = `Response returned error code ${responseData.ErrorCode}, message: ${responseData.ErrorMessage}`;
if (responseData?.ErrorCode === 0) {
comment = responseData.Data.T_Service?.length
? `No service present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`
: `No booking times present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`;
}
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.GetAvailabilities,
RequestStatus.Fail,
comment,
);
return null;
}
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.GetAvailabilities,
RequestStatus.Success,
JSON.stringify(responseData),
);
return responseData.Data;
} catch (error) {
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.GetAvailabilities,
RequestStatus.Fail,
JSON.stringify(error),
);
return null;
}
}
export async function bookAppointment(
serviceSyncId: number,
clinicId: number,
appointmentUserId: number,
syncUserID: number,
startTime: string,
locationId = 0,
comments = '',
isEarlierTimeRequested = false,
earlierTimeRequestComment = '',
) {
const supabase = await createClient();
try {
const {
data: { user },
} = await supabase.auth.getUser();
if (!user?.id) {
throw new Error('User not authenticated');
}
const [
{ data: dbClinic, error: clinicError },
{ data: dbService, error: serviceError },
] = await Promise.all([
supabase
.from('connected_online_providers')
.select('*')
.eq('id', clinicId)
.limit(1),
supabase
.from('connected_online_services')
.select('*')
.eq('sync_id', serviceSyncId)
.eq('clinic_id', clinicId)
.limit(1),
]);
if (!dbClinic?.length || !dbService?.length) {
return logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Fail,
dbClinic?.length
? `Could not find clinic with id ${clinicId}, error: ${JSON.stringify(clinicError)}`
: `Could not find service with sync id ${serviceSyncId} and clinic id ${clinicId}, error: ${JSON.stringify(serviceError)}`,
startTime,
serviceSyncId,
clinicId,
);
}
const clinic: Tables<'connected_online_providers'> = dbClinic![0];
const service: Tables<'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(
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.BookTime}`,
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: JSON.stringify({
EarlierTime: isEarlierTimeRequested, // once we have the e-shop functionality we can let the user select if she would like to be offered earlier time slots if they become available
EarlierTimeComment: earlierTimeRequestComment,
ClinicID: clinic.id,
ServiceID: service.id,
ClinicServiceID: service.sync_id,
UserID: appointmentUserId,
SyncUserID: syncUserID,
StartTime: startTime,
FirstName: 'Test',
LastName: 'User',
PersonalCode: '4',
Email: user.email,
Phone: 'phone',
Comments: comments,
Location: locationId,
FreeCode: '',
AddToBasket: false,
Key: '7T624nlu',
Lang: 'et', // update when integrated into app, if needed
}),
},
);
const responseData: BookTimeResponse = JSON.parse(response.data.d);
if (responseData?.ErrorCode !== 0 || !responseData.Value) {
return logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Fail,
JSON.stringify(responseData),
startTime,
service.id,
clinicId,
);
}
const responseParts = responseData.Value.split(',');
const { error } = await supabase
.from('connected_online_reservation')
.insert({
booking_code: responseParts[1],
clinic_id: clinic.id,
comments,
lang: 'et', // change later, if needed
service_id: service.id,
service_user_id: appointmentUserId,
start_time: startTime,
sync_user_id: syncUserID,
requires_payment: !!responseParts[0],
user_id: user.id,
});
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Success,
JSON.stringify(responseData),
startTime,
service.id,
clinicId,
);
if (error) {
throw new Error(error.message);
}
return responseData.Value;
} catch (error) {
return logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.BookTime,
RequestStatus.Fail,
JSON.stringify(error),
startTime,
serviceSyncId,
clinicId,
);
}
}
export async function getConfirmedService(reservationCode: string) {
try {
const response = await axios.post(
`${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.ConfirmedLoad}`,
{
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
param: JSON.stringify({ Value: `${reservationCode}|7T624nlu|et` }),
},
);
const responseData: ConfirmedLoadResponse = JSON.parse(response.data.d);
if (responseData?.ErrorCode !== 0) {
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.ConfirmedLoad,
RequestStatus.Fail,
JSON.stringify(responseData),
);
return null;
}
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.ConfirmedLoad,
RequestStatus.Success,
JSON.stringify(responseData),
);
return responseData.Data;
} catch (error) {
await logRequestResult(
ExternalApi.ConnectedOnline,
ConnectedOnlineMethodName.ConfirmedLoad,
RequestStatus.Fail,
JSON.stringify(error),
);
return null;
}
}

View File

@@ -1,3 +1,5 @@
'use server';
import {
SupabaseClient,
createClient as createCustomClient,

View File

@@ -2,3 +2,8 @@ export enum SyncStatus {
Success = "SUCCESS",
Fail = "FAIL",
}
export enum RequestStatus {
Success = "SUCCESS",
Fail = "FAIL",
}

View File

@@ -0,0 +1,226 @@
import * as z from 'zod';
export const BookTimeResponseSchema = z.object({
Value: z.string(),
Data: z.null(),
ErrorCode: z.number(),
ErrorMessage: z.null(),
});
export type BookTimeResponse = z.infer<typeof BookTimeResponseSchema>;
export enum ConnectedOnlineMethodName {
SearchLoad = 'Search_Load',
GetAvailabilities = 'GetAvailabilities',
BookTime = 'BookTime',
ConfirmedLoad = 'Confirmed_Load',
}
export const AvailableAppointmentTBookingSchema = z.object({
ClinicID: z.string(),
LocationID: z.number(),
UserID: z.number(),
SyncUserID: z.number(),
ServiceID: z.number(),
HKServiceID: z.number(),
StartTime: z.coerce.date(),
EndTime: z.coerce.date(),
PayorCode: z.string(),
});
export type AvailableAppointment = z.infer<
typeof AvailableAppointmentTBookingSchema
>;
export const TDocsASchema = z.object({
List: z.string(),
});
export type TDocsA = z.infer<typeof TDocsASchema>;
export const TServiceSchema = z.object({
Code: z.string(),
Name: z.string(),
Price: z.number(),
VATType: z.number(),
HKServiceID: z.union([z.number(), z.null()]),
Duration: z.union([z.number(), z.null()]),
Description: z.union([z.string(), z.null()]),
OnlinePaymentRequired: z.number(),
TehikServiceCode: z.union([z.string(), z.null()]),
OnlineHideDuration: z.number(),
OnlineHidePrice: z.number(),
PricePeriods: z.string(),
});
export type TService = z.infer<typeof TServiceSchema>;
export const AvailableAppointmentsDataSchema = z.object({
T_ScheduleType: z.array(z.any()),
T_Service: z.array(TServiceSchema),
T_Booking: z.array(AvailableAppointmentTBookingSchema),
T_DocsAvailable: z.array(TDocsASchema),
T_DocsAll: z.array(TDocsASchema),
});
export type AvailableAppointmentsData = z.infer<
typeof AvailableAppointmentsDataSchema
>;
export const AvailableAppointmentsResponseSchema = z.object({
Value: z.null(),
Data: AvailableAppointmentsDataSchema,
ErrorCode: z.number(),
ErrorMessage: z.union([z.string(), z.null()]),
});
export type AvailableAppointmentsResponse = z.infer<
typeof AvailableAppointmentsResponseSchema
>;
export const APaymentRequestSchema = z.object({
ID: z.number(),
Ref: z.string(),
AmountTotal: z.number(),
AmountVat: z.number(),
PaidAmount: z.union([z.number(), z.null()]),
Paid: z.union([z.number(), z.null()]),
Failed: z.union([z.number(), z.null()]),
PaidAmount1: z.union([z.number(), z.null()]),
ScheduleID: z.number(),
PaymentID: z.union([z.number(), z.null()]),
Created: z.coerce.date(),
Nonce: z.string(),
DiscountPercent: z.number(),
DiscountCode: z.string(),
});
export type APaymentRequest = z.infer<typeof APaymentRequestSchema>;
export const PClinicSchema = z.object({
ID: z.number(),
LicenseID: z.number(),
Name: z.string(),
Email: z.string(),
Address: z.string(),
Address2: z.string(),
Phone: z.string(),
VatNo: z.string(),
RegistryCode: z.string(),
OID: z.string(),
PersonalCodeRequired: z.number(),
OnlineCommentRequired: z.number(),
OnlineLoginRequired: z.number(),
OnlineSenderEmail: z.string(),
OnlineReplyToEmail: z.string(),
OnlineCCEmail: z.string(),
OnlineRedirectkUrl: z.string(), // the typo is on their side
OnlineAllowQueue: z.number(),
});
export type PClinic = z.infer<typeof PClinicSchema>;
export const PJobTitleTranslationSchema = z.object({
ID: z.number(),
SyncID: z.number(),
TextEN: z.string(),
TextET: z.string(),
TextFI: z.string(),
TextRU: z.string(),
TextLT: z.string(),
ClinicID: z.number(),
Deleted: z.number(),
});
export type PJobTitleTranslation = z.infer<typeof PJobTitleTranslationSchema>;
export const PServiceSchema = z.object({
ID: z.number(),
NameET: z.string(),
NameEN: z.string(),
NameRU: z.string(),
NameFI: z.string(),
DescriptionET: z.string(),
DescriptionEN: z.string(),
DescriptionRU: z.string(),
DescriptionFI: z.string(),
ExtraEmailTextET: z.string(),
ExtraEmailTextEN: z.string(),
ExtraEmailTextRU: z.string(),
ExtraEmailTextFI: z.string(),
});
export type PService = z.infer<typeof PServiceSchema>;
export const ParamSchema = z.object({
PersonalCodeRequired: z.number(),
OnlineCommentRequired: z.number(),
OnlineLoginRequired: z.number(),
ClinicName: z.string(),
ClinicID: z.number(),
OnlineRedirectkUrl: z.string(),
OnlineAllowQueue: z.number(),
Key: z.string(),
});
export type Param = z.infer<typeof ParamSchema>;
export const TBookingSchema = z.object({
ID: z.number(),
ClinicID: z.number(),
FirstName: z.string(),
LastName: z.string(),
Email: z.string(),
Phone: z.string(),
PersonalCode: z.string(),
Comments: z.string(),
ServiceName: z.string(),
DoctorName: z.string(),
StartTime: z.coerce.date(),
EndTime: z.coerce.date(),
Status: z.number(),
PaymentRequestID: z.number(),
MailResponse: z.null(), // was not present in test data, might need to be specified in the future
BookingCode: z.string(),
LocationName: z.union([z.string(), z.null()]),
isDR: z.number(),
LocationID: z.number(),
ScheduleID: z.number(),
ServiceID: z.number(),
ServiceSyncID: z.number(),
ServiceCode: z.string(),
ServiceIsRemote: z.number(),
IsRegistered: z.number(),
OfferEarlierTime: z.number(),
EarlierTimeComment: z.string(),
PartnerCode: z.union([z.number(), z.null()]),
PartnerCallBackUrl: z.union([z.string(), z.null()]),
LocationOfficialName: z.union([z.string(), z.null()]),
LocationAddress1: z.union([z.string(), z.null()]),
LocationAddress2: z.union([z.string(), z.null()]),
LocationPhone: z.union([z.string(), z.null()]),
});
export type TBooking = z.infer<typeof TBookingSchema>;
export const TDoctorSchema = z.object({
ID: z.number(),
Name: z.string(),
Prefix: z.string(),
Photo: z.string(),
SpokenLanguages: z.string(),
JobTitleID: z.union([z.number(), z.null()]),
ClinicID: z.number(),
Deleted: z.number(),
});
export type TDoctor = z.infer<typeof TDoctorSchema>;
export const ConfirmedLoadDataSchema = z.object({
T_Booking: z.array(TBookingSchema),
P_Location: z.array(z.any()),
P_Clinic: z.array(PClinicSchema),
A_PaymentRequest: z.array(APaymentRequestSchema),
P_Service: z.array(PServiceSchema),
T_Service: z.array(z.any()),
T_ServiceHK: z.array(z.any()),
P_JobTitleTranslations: z.array(PJobTitleTranslationSchema),
T_Doctor: z.array(TDoctorSchema),
Params: z.array(ParamSchema),
});
export type ConfirmedLoadData = z.infer<typeof ConfirmedLoadDataSchema>;
export const ConfirmedLoadResponseSchema = z.object({
Value: z.null(),
Data: ConfirmedLoadDataSchema,
ErrorCode: z.number(),
ErrorMessage: z.union([z.string(), z.null()]),
});
export type ConfirmedLoadResponse = z.infer<typeof ConfirmedLoadResponseSchema>;

4
lib/types/external.ts Normal file
View File

@@ -0,0 +1,4 @@
export enum ExternalApi {
Medipost = 'Medipost',
ConnectedOnline = 'ConnectedOnline',
}

View File

@@ -19,19 +19,20 @@
"supabase": "supabase",
"supabase:start": "supabase status || supabase start",
"supabase:stop": "supabase stop",
"supabase:reset": "supabase db reset",
"supabase:status": "supabase status",
"supabase:test": "supabase db test",
"supabase:db:reset": "supabase db reset",
"supabase:db:lint": "supabase db lint",
"supabase:db:diff": "supabase db diff",
"supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push",
"supabase:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts",
"supabase:db:dump:local": "supabase db dump --local --data-only",
"sync-data:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts"
"sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts",
"sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts"
},
"dependencies": {
"@edge-csrf/nextjs": "2.5.3-cloudflare-rc1",
"@hookform/resolvers": "^5.0.1",
"@hookform/resolvers": "^5.1.1",
"@kit/accounts": "workspace:*",
"@kit/admin": "workspace:*",
"@kit/analytics": "workspace:*",
@@ -59,7 +60,7 @@
"@supabase/supabase-js": "2.49.4",
"@tanstack/react-query": "5.76.1",
"@tanstack/react-table": "^8.21.3",
"axios": "^1.9.0",
"axios": "^1.10.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.5",
@@ -70,13 +71,13 @@
"next-themes": "0.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"react-i18next": "^15.5.1",
"react-hook-form": "^7.58.0",
"react-i18next": "^15.5.3",
"recharts": "2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
"sonner": "^2.0.5",
"tailwind-merge": "^3.3.1",
"ts-node": "^10.9.2",
"zod": "^3.24.4"
"zod": "^3.25.67"
},
"devDependencies": {
"@hookform/resolvers": "^5.0.1",
@@ -84,9 +85,9 @@
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@next/bundle-analyzer": "15.3.2",
"@tailwindcss/postcss": "^4.1.7",
"@tailwindcss/postcss": "^4.1.10",
"@types/lodash": "^4.17.17",
"@types/node": "^22.15.18",
"@types/node": "^22.15.32",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"babel-plugin-react-compiler": "19.1.0-rc.2",
@@ -95,7 +96,7 @@
"pino-pretty": "^13.0.0",
"prettier": "^3.5.3",
"react-hook-form": "^7.57.0",
"supabase": "^2.22.12",
"supabase": "^2.26.9",
"tailwindcss": "4.1.7",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.8.3",

574
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,227 @@
create table "public"."connected_online_providers" (
"id" bigint not null,
"name" text not null,
"email" text,
"phone_number" text,
"can_select_worker" boolean not null,
"personal_code_required" boolean not null,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp without time zone default now()
);
alter table "public"."connected_online_providers" enable row level security;
create table "public"."connected_online_services" (
"id" bigint not null,
"clinic_id" bigint not null,
"sync_id" bigint not null,
"name" text not null,
"description" text,
"price" double precision not null,
"requires_payment" boolean not null,
"duration" bigint not null,
"neto_duration" bigint,
"display" text,
"price_periods" text,
"online_hide_duration" bigint,
"online_hide_price" bigint,
"code" text not null,
"has_free_codes" boolean not null,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone default now()
);
alter table "public"."connected_online_services" enable row level security;
CREATE UNIQUE INDEX connected_online_providers_id_key ON public.connected_online_providers USING btree (id);
CREATE UNIQUE INDEX connected_online_providers_pkey ON public.connected_online_providers USING btree (id);
CREATE UNIQUE INDEX connected_online_services_id_key ON public.connected_online_services USING btree (id);
CREATE UNIQUE INDEX connected_online_services_pkey ON public.connected_online_services USING btree (id);
alter table "public"."connected_online_providers" add constraint "connected_online_providers_pkey" PRIMARY KEY using index "connected_online_providers_pkey";
alter table "public"."connected_online_services" add constraint "connected_online_services_pkey" PRIMARY KEY using index "connected_online_services_pkey";
alter table "public"."connected_online_providers" add constraint "connected_online_providers_id_key" UNIQUE using index "connected_online_providers_id_key";
alter table "public"."connected_online_services" add constraint "connected_online_services_clinic_id_fkey" FOREIGN KEY (clinic_id) REFERENCES connected_online_providers(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."connected_online_services" validate constraint "connected_online_services_clinic_id_fkey";
alter table "public"."connected_online_services" add constraint "connected_online_services_id_key" UNIQUE using index "connected_online_services_id_key";
grant delete on table "public"."connected_online_providers" to "service_role";
grant insert on table "public"."connected_online_providers" to "service_role";
grant references on table "public"."connected_online_providers" to "service_role";
grant select on table "public"."connected_online_providers" to "service_role";
grant trigger on table "public"."connected_online_providers" to "service_role";
grant truncate on table "public"."connected_online_providers" to "service_role";
grant update on table "public"."connected_online_providers" to "service_role";
grant select on table "public"."connected_online_providers" to "authenticated";
grant delete on table "public"."connected_online_services" to "service_role";
grant insert on table "public"."connected_online_services" to "service_role";
grant references on table "public"."connected_online_services" to "service_role";
grant select on table "public"."connected_online_services" to "service_role";
grant trigger on table "public"."connected_online_services" to "service_role";
grant truncate on table "public"."connected_online_services" to "service_role";
grant update on table "public"."connected_online_services" to "service_role";
grant select on table "public"."connected_online_services" to "authenticated";
create type "audit"."request_status" as enum ('SUCCESS', 'FAIL');
create table "audit"."request_entries" (
"id" bigint generated by default as identity not null,
"personal_code" bigint,
"request_api" text not null,
"request_api_method" text not null,
"status" audit.request_status not null,
"comment" text,
"service_provider_id" bigint,
"service_id" bigint,
"requested_start_date" timestamp with time zone,
"requested_end_date" timestamp with time zone,
"created_at" timestamp with time zone not null default now()
);
alter table "audit"."request_entries" enable row level security;
CREATE UNIQUE INDEX request_entries_pkey ON audit.request_entries USING btree (id);
alter table "audit"."request_entries" add constraint "request_entries_pkey" PRIMARY KEY using index "request_entries_pkey";
grant delete on table "audit"."request_entries" to "service_role";
grant insert on table "audit"."request_entries" to "service_role";
grant references on table "audit"."request_entries" to "service_role";
grant select on table "audit"."request_entries" to "service_role";
grant trigger on table "audit"."request_entries" to "service_role";
grant truncate on table "audit"."request_entries" to "service_role";
grant update on table "audit"."request_entries" to "service_role";
create policy "service_role_all"
on "audit"."request_entries"
as permissive
for all
to service_role
using (true);
create table "public"."connected_online_reservation" (
"id" bigint generated by default as identity not null,
"user_id" uuid not null,
"booking_code" text not null,
"service_id" bigint not null,
"clinic_id" bigint not null,
"service_user_id" bigint,
"sync_user_id" bigint not null,
"requires_payment" boolean not null,
"comments" text,
"start_time" timestamp with time zone not null,
"lang" text not null,
"discount_code" text,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone default now()
);
alter table "public"."connected_online_reservation" enable row level security;
CREATE UNIQUE INDEX connected_online_reservation_booking_code_key ON public.connected_online_reservation USING btree (booking_code);
CREATE UNIQUE INDEX connected_online_reservation_pkey ON public.connected_online_reservation USING btree (id);
alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_pkey" PRIMARY KEY using index "connected_online_reservation_pkey";
alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_booking_code_key" UNIQUE using index "connected_online_reservation_booking_code_key";
alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."connected_online_reservation" validate constraint "connected_online_reservation_user_id_fkey";
grant delete on table "public"."connected_online_reservation" to "service_role";
grant insert on table "public"."connected_online_reservation" to "service_role";
grant references on table "public"."connected_online_reservation" to "service_role";
grant select on table "public"."connected_online_reservation" to "service_role";
grant trigger on table "public"."connected_online_reservation" to "service_role";
grant truncate on table "public"."connected_online_reservation" to "service_role";
grant update on table "public"."connected_online_reservation" to "service_role";
create policy "service_role_all"
on "public"."connected_online_reservation"
as permissive
for all
to service_role
using (true);
CREATE TRIGGER connected_online_providers_change_record_timestamps AFTER INSERT OR UPDATE ON public.connected_online_providers FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamps();
CREATE TRIGGER connected_online_services_change_record_timestamps AFTER INSERT OR UPDATE ON public.connected_online_services FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamps();
create policy "service_role_all"
on "public"."connected_online_providers"
as permissive
for all
to service_role
using (true);
create policy "service_role_all"
on "public"."connected_online_services"
as permissive
for all
to service_role
using (true);
create policy "authenticated_select"
on "public"."connected_online_providers"
as permissive
for select
to authenticated
using (true);
create policy "authenticated_select"
on "public"."connected_online_services"
as permissive
for select
to authenticated
using (true);
create policy "own_all"
on "public"."connected_online_reservation"
as permissive
for all
to authenticated
using ((( SELECT auth.uid() AS uid) = user_id));

View File

@@ -0,0 +1,225 @@
create table "public"."medreport_product_groups" (
"id" bigint generated by default as identity not null,
"name" text not null,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone
);
create table "public"."medreport_products" (
"id" bigint generated by default as identity not null,
"name" text not null,
"product_group_id" bigint,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone default now()
);
alter table "public"."medreport_products" enable row level security;
create table "public"."medreport_products_analyses_relations" (
"product_id" bigint not null,
"analysis_element_id" bigint,
"analysis_id" bigint
);
alter table "public"."medreport_product_groups" enable row level security;
alter table "public"."medreport_products_analyses_relations" enable row level security;
CREATE UNIQUE INDEX medreport_product_groups_name_key ON public.medreport_product_groups USING btree (name);
CREATE UNIQUE INDEX medreport_product_groups_pkey ON public.medreport_product_groups USING btree (id);
alter table "public"."medreport_product_groups" add constraint "medreport_product_groups_pkey" PRIMARY KEY using index "medreport_product_groups_pkey";
alter table "public"."medreport_product_groups" add constraint "medreport_product_groups_name_key" UNIQUE using index "medreport_product_groups_name_key";
alter table "public"."medreport_products" add constraint "medreport_products_product_groups_id_fkey" FOREIGN KEY (product_group_id) REFERENCES medreport_product_groups(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."medreport_products" validate constraint "medreport_products_product_groups_id_fkey";
grant select on table "public"."medreport_product_groups" to "anon";
grant select on table "public"."medreport_product_groups" to "authenticated";
grant delete on table "public"."medreport_product_groups" to "service_role";
grant insert on table "public"."medreport_product_groups" to "service_role";
grant references on table "public"."medreport_product_groups" to "service_role";
grant select on table "public"."medreport_product_groups" to "service_role";
grant trigger on table "public"."medreport_product_groups" to "service_role";
grant truncate on table "public"."medreport_product_groups" to "service_role";
grant update on table "public"."medreport_product_groups" to "service_role";
CREATE UNIQUE INDEX medreport_products_analyses_analysis_element_id_key ON public.medreport_products_analyses_relations USING btree (analysis_element_id);
CREATE UNIQUE INDEX medreport_products_analyses_analysis_id_key ON public.medreport_products_analyses_relations USING btree (analysis_id);
CREATE UNIQUE INDEX medreport_products_analyses_pkey ON public.medreport_products_analyses_relations USING btree (product_id);
CREATE UNIQUE INDEX medreport_products_name_key ON public.medreport_products USING btree (name);
CREATE UNIQUE INDEX medreport_products_pkey ON public.medreport_products USING btree (id);
alter table "public"."medreport_products" add constraint "medreport_products_pkey" PRIMARY KEY using index "medreport_products_pkey";
alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_pkey" PRIMARY KEY using index "medreport_products_analyses_pkey";
alter table "public"."medreport_products" add constraint "medreport_products_name_key" UNIQUE using index "medreport_products_name_key";
alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_element_id_fkey" FOREIGN KEY (analysis_element_id) REFERENCES analysis_elements(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."medreport_products_analyses_relations" validate constraint "medreport_products_analyses_analysis_element_id_fkey";
alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_element_id_key" UNIQUE using index "medreport_products_analyses_analysis_element_id_key";
alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_id_fkey" FOREIGN KEY (analysis_id) REFERENCES analyses(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."medreport_products_analyses_relations" validate constraint "medreport_products_analyses_analysis_id_fkey";
alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_id_key" UNIQUE using index "medreport_products_analyses_analysis_id_key";
alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_product_id_fkey" FOREIGN KEY (product_id) REFERENCES medreport_products(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."medreport_products_analyses_relations" validate constraint "medreport_products_analyses_product_id_fkey";
alter table "public"."medreport_products_analyses_relations" add constraint "product_can_be_tied_to_only_one_external_item" CHECK (((analysis_id IS NULL) OR (analysis_element_id IS NULL))) not valid;
alter table "public"."medreport_products_analyses_relations" validate constraint "product_can_be_tied_to_only_one_external_item";
grant select on table "public"."medreport_products" to "anon";
grant select on table "public"."medreport_products" to "authenticated";
grant delete on table "public"."medreport_products" to "service_role";
grant insert on table "public"."medreport_products" to "service_role";
grant references on table "public"."medreport_products" to "service_role";
grant select on table "public"."medreport_products" to "service_role";
grant trigger on table "public"."medreport_products" to "service_role";
grant truncate on table "public"."medreport_products" to "service_role";
grant update on table "public"."medreport_products" to "service_role";
grant select on table "public"."medreport_products_analyses_relations" to "anon";
grant select on table "public"."medreport_products_analyses_relations" to "authenticated";
grant delete on table "public"."medreport_products_analyses_relations" to "service_role";
grant insert on table "public"."medreport_products_analyses_relations" to "service_role";
grant references on table "public"."medreport_products_analyses_relations" to "service_role";
grant select on table "public"."medreport_products_analyses_relations" to "service_role";
grant trigger on table "public"."medreport_products_analyses_relations" to "service_role";
grant truncate on table "public"."medreport_products_analyses_relations" to "service_role";
grant update on table "public"."medreport_products_analyses_relations" to "service_role";
create policy "Enable read access for all users"
on "public"."medreport_products_analyses_relations"
as permissive
for select
to public
using (true);
ALTER TABLE medreport_products_analyses_relations
ADD CONSTRAINT product_can_be_tied_to_only_one_analysis_item
CHECK (analysis_id IS NULL OR analysis_element_id IS NULL);
create table "public"."medreport_products_external_services_relations" (
"product_id" bigint not null,
"connected_online_service_id" bigint not null
);
alter table "public"."medreport_products_external_services_relations" enable row level security;
CREATE UNIQUE INDEX medreport_products_connected_online_services_id_key ON public.medreport_products_external_services_relations USING btree (connected_online_service_id);
CREATE UNIQUE INDEX medreport_products_connected_online_services_pkey ON public.medreport_products_external_services_relations USING btree (connected_online_service_id);
alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_pkey" PRIMARY KEY using index "medreport_products_connected_online_services_pkey";
alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_id_fkey" FOREIGN KEY (connected_online_service_id) REFERENCES connected_online_services(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."medreport_products_external_services_relations" validate constraint "medreport_products_connected_online_services_id_fkey";
alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_id_key" UNIQUE using index "medreport_products_connected_online_services_id_key";
alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_product_id_fkey" FOREIGN KEY (product_id) REFERENCES medreport_products(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."medreport_products_external_services_relations" validate constraint "medreport_products_connected_online_services_product_id_fkey";
grant select on table "public"."medreport_products_external_services_relations" to "anon";
grant select on table "public"."medreport_products_external_services_relations" to "authenticated";
grant delete on table "public"."medreport_products_external_services_relations" to "service_role";
grant insert on table "public"."medreport_products_external_services_relations" to "service_role";
grant references on table "public"."medreport_products_external_services_relations" to "service_role";
grant select on table "public"."medreport_products_external_services_relations" to "service_role";
grant trigger on table "public"."medreport_products_external_services_relations" to "service_role";
grant truncate on table "public"."medreport_products_external_services_relations" to "service_role";
grant update on table "public"."medreport_products_external_services_relations" to "service_role";
CREATE OR REPLACE FUNCTION check_tied_to_connected_online()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM medreport_products_external_services_relations
WHERE product_id = NEW.product_id
) THEN
RAISE EXCEPTION 'Value "%" already exists in medreport_products_external_services_relations', NEW.product_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION check_tied_to_analysis_item()
RETURNS TRIGGER AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM medreport_products_analyses_relations
WHERE product_id = NEW.product_id
) THEN
RAISE EXCEPTION 'Value "%" already exists in medreport_products_analyses_relations', NEW.product_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER check_not_already_tied_to_connected_online BEFORE INSERT OR UPDATE ON public.medreport_products_analyses_relations FOR EACH ROW EXECUTE FUNCTION check_tied_to_connected_online();
CREATE TRIGGER check_not_already_tied_to_analysis BEFORE INSERT OR UPDATE ON public.medreport_products_external_services_relations FOR EACH ROW EXECUTE FUNCTION check_tied_to_analysis_item();
create policy "read_all"
on "public"."medreport_product_groups"
as permissive
for select
to public
using (true);