diff --git a/prisma/migrations/20240122084806_change_custom_field_access_table/migration.sql b/prisma/migrations/20240122084806_change_custom_field_access_table/migration.sql deleted file mode 100644 index cba2703..0000000 --- a/prisma/migrations/20240122084806_change_custom_field_access_table/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- DropIndex -DROP INDEX "CustomFieldAccess_customFieldId_key"; - --- AlterTable -ALTER TABLE "CustomFieldAccess" ADD COLUMN "companyId" UUID; diff --git a/prisma/migrations/20240122105731_change_custom_field_access_table/migration.sql b/prisma/migrations/20240122105731_change_custom_field_access_table/migration.sql deleted file mode 100644 index bab086d..0000000 --- a/prisma/migrations/20240122105731_change_custom_field_access_table/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `permissions` on the `CustomFieldAccess` table. All the data in the column will be lost. - - Added the required column `permission` to the `CustomFieldAccess` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "Permission" AS ENUM ('VIEW', 'EDIT'); - --- AlterTable -ALTER TABLE "CustomFieldAccess" DROP COLUMN "permissions", -ADD COLUMN "permission" "Permission" NOT NULL; diff --git a/prisma/migrations/20240122135813_change_custom_field_access_table/migration.sql b/prisma/migrations/20240122135813_change_custom_field_access_table/migration.sql deleted file mode 100644 index 10b5e0a..0000000 --- a/prisma/migrations/20240122135813_change_custom_field_access_table/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* - Warnings: - - - Made the column `customFieldId` on table `CustomFieldAccess` required. This step will fail if there are existing NULL values in that column. - - Made the column `companyId` on table `CustomFieldAccess` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "CustomFieldAccess" ALTER COLUMN "customFieldId" SET NOT NULL, -ALTER COLUMN "companyId" SET NOT NULL; diff --git a/prisma/migrations/20240115152243_create_custom_field_access_table/migration.sql b/prisma/migrations/20240127105731_create_custom_field_access_table/migration.sql similarity index 61% rename from prisma/migrations/20240115152243_create_custom_field_access_table/migration.sql rename to prisma/migrations/20240127105731_create_custom_field_access_table/migration.sql index ab12c92..19f3f71 100644 --- a/prisma/migrations/20240115152243_create_custom_field_access_table/migration.sql +++ b/prisma/migrations/20240127105731_create_custom_field_access_table/migration.sql @@ -1,13 +1,14 @@ +-- CreateEnum +CREATE TYPE "Permission" AS ENUM ('VIEW', 'EDIT'); + -- CreateTable CREATE TABLE "CustomFieldAccess" ( "id" UUID NOT NULL DEFAULT gen_random_uuid(), - "customFieldId" UUID, - "permissions" JSONB, + "customFieldId" UUID NOT NULL, + "portalId" TEXT NOT NULL, + "permissions" "Permission"[], "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMPTZ NOT NULL, CONSTRAINT "CustomFieldAccess_pkey" PRIMARY KEY ("id") ); - --- CreateIndex -CREATE UNIQUE INDEX "CustomFieldAccess_customFieldId_key" ON "CustomFieldAccess"("customFieldId"); diff --git a/prisma/migrations/20240127105945_create_client_profile_updates_table/migration.sql b/prisma/migrations/20240127105945_create_client_profile_updates_table/migration.sql new file mode 100644 index 0000000..fe66380 --- /dev/null +++ b/prisma/migrations/20240127105945_create_client_profile_updates_table/migration.sql @@ -0,0 +1,12 @@ +-- CreateTable +CREATE TABLE "ClientProfileUpdates" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "clientId" UUID NOT NULL, + "companyId" UUID NOT NULL, + "customFields" JSONB NOT NULL, + "changedFields" JSONB NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "ClientProfileUpdates_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20240127115634_create_setting_table/migration.sql b/prisma/migrations/20240127115634_create_setting_table/migration.sql new file mode 100644 index 0000000..cd04429 --- /dev/null +++ b/prisma/migrations/20240127115634_create_setting_table/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "Setting" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "portalId" TEXT NOT NULL, + "profileLinks" JSONB NOT NULL, + "createdAt" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "Setting_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20240129075218_add_portal_id_to_client_profile_updates_table/migration.sql b/prisma/migrations/20240129075218_add_portal_id_to_client_profile_updates_table/migration.sql new file mode 100644 index 0000000..c7cddee --- /dev/null +++ b/prisma/migrations/20240129075218_add_portal_id_to_client_profile_updates_table/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `portalId` to the `ClientProfileUpdates` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "ClientProfileUpdates" ADD COLUMN "portalId" TEXT NOT NULL; diff --git a/prisma/migrations/20240129090605_rename_column_in_setting_table/migration.sql b/prisma/migrations/20240129090605_rename_column_in_setting_table/migration.sql new file mode 100644 index 0000000..f987ce5 --- /dev/null +++ b/prisma/migrations/20240129090605_rename_column_in_setting_table/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `profileLinks` on the `Setting` table. All the data in the column will be lost. + - Added the required column `data` to the `Setting` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Setting" DROP COLUMN "profileLinks", +ADD COLUMN "data" JSONB NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d202484..45c5ccb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,18 +8,38 @@ generator client { datasource db { provider = "postgresql" url = env("POSTGRES_PRISMA_URL") + directUrl = env("POSTGRES_URL_NON_POOLING") } enum Permission { - VIEW - EDIT + VIEW + EDIT } model CustomFieldAccess { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + customFieldId String @db.Uuid + portalId String + permissions Permission[] + createdAt DateTime @default(now()) @db.Timestamptz() + updatedAt DateTime @updatedAt @ignore @db.Timestamptz() +} + +model ClientProfileUpdates { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - customFieldId String @db.Uuid - companyId String @db.Uuid - permission Permission + clientId String @db.Uuid + companyId String @db.Uuid + portalId String + customFields Json @db.JsonB + changedFields Json @db.JsonB createdAt DateTime @default(now()) @db.Timestamptz() updatedAt DateTime @updatedAt @ignore @db.Timestamptz() } + +model Setting { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + portalId String + data Json @db.JsonB + createdAt DateTime @default(now()) @db.Timestamptz() + updatedAt DateTime @updatedAt @ignore @db.Timestamptz() +} diff --git a/src/app/api/client-profile-updates/route.ts b/src/app/api/client-profile-updates/route.ts new file mode 100644 index 0000000..a1f6b58 --- /dev/null +++ b/src/app/api/client-profile-updates/route.ts @@ -0,0 +1,116 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ClientProfileUpdatesRequestSchema, ParsedClientProfileUpdatesResponse } from '@/types/clientProfileUpdates'; +import { CopilotAPI } from '@/utils/copilotApiUtils'; +import { handleError, respondError } from '@/utils/common'; +import { ClientProfileUpdatesService } from '@/app/api/client-profile-updates/services/clientProfileUpdates.service'; +import { ClientResponse, CompanyResponse } from '@/types/common'; +import { z } from 'zod'; +import { createLookup, getObjectDifference, getSelectedOptions } from '@/lib/helper'; + +export async function POST(request: NextRequest) { + const data = await request.json(); + const clientProfileUpdateRequest = ClientProfileUpdatesRequestSchema.safeParse(data); + if (!clientProfileUpdateRequest.success) { + return NextResponse.json(clientProfileUpdateRequest.error.issues, { status: 422 }); + } + try { + //todo: check access + const copilotClient = new CopilotAPI(clientProfileUpdateRequest.data.token); + const client: ClientResponse = await copilotClient.getClient(clientProfileUpdateRequest.data.clientId); + const clientUpdateResponse = await copilotClient.updateClient(clientProfileUpdateRequest.data.clientId, { + customFields: clientProfileUpdateRequest.data.form, + }); + const changedFields = getObjectDifference(clientUpdateResponse.customFields ?? {}, client.customFields ?? {}); + if (Object.keys(changedFields).length === 0) { + return NextResponse.json({}); + } + + const service = new ClientProfileUpdatesService(); + await service.save({ + clientId: clientProfileUpdateRequest.data.clientId, + companyId: clientProfileUpdateRequest.data.companyId, + portalId: clientProfileUpdateRequest.data.portalId, + customFields: clientUpdateResponse.customFields ?? {}, + changedFields, + }); + + return NextResponse.json({}); + } catch (error) { + return handleError(error); + } +} + +export async function GET(request: NextRequest) { + const token = request.nextUrl.searchParams.get('token'); + const portalId = request.nextUrl.searchParams.get('portalId'); + + if (!token) { + return respondError('Missing token', 422); + } + if (!portalId) { + return respondError('Missing portalId', 422); + } + + try { + const copilotClient = new CopilotAPI(z.string().parse(token)); + + const [currentUser, clients, companies, portalCustomFields] = await Promise.all([ + copilotClient.me(), + copilotClient.getClients(), + copilotClient.getCompanies(), + copilotClient.getCustomFields(), + ]); + //todo:: filter companyIds based on currentUser restrictions + const clientProfileUpdates = await new ClientProfileUpdatesService().findMany(portalId, []); + + const clientLookup = createLookup(clients.data, 'id'); + const companyLookup = createLookup(companies.data, 'id'); + + const parsedClientProfileUpdates: ParsedClientProfileUpdatesResponse[] = clientProfileUpdates.map((update) => { + const client = clientLookup[update.clientId]; + const company = companyLookup[update.companyId]; + + const customFields = portalCustomFields.data?.map((portalCustomField) => { + const value = update.customFields[portalCustomField.key] ?? null; + const options = getSelectedOptions(portalCustomField, value); + + return { + name: portalCustomField.name, + type: portalCustomField.type, + key: portalCustomField.key, + value: options.length > 0 ? options : value, + isChanged: !!update.changedFields[portalCustomField.key], + }; + }); + + return { + id: update.id, + client: getClientDetails(client), + company: getCompanyDetails(company), + lastUpdated: update.createdAt, + customFields, + }; + }); + + return NextResponse.json(parsedClientProfileUpdates); + } catch (error) { + return handleError(error); + } +} + +function getClientDetails(client: ClientResponse) { + return { + id: client.id, + name: `${client.givenName} ${client.familyName}`, + email: client.email, + avatarImageUrl: client.avatarImageUrl, + }; +} + +function getCompanyDetails(company: CompanyResponse) { + return { + id: company.id, + name: company.name, + iconImageUrl: company.iconImageUrl, + }; +} diff --git a/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts b/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts new file mode 100644 index 0000000..70d9888 --- /dev/null +++ b/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts @@ -0,0 +1,58 @@ +import { PrismaClient } from '@prisma/client'; +import DBClient from '@/lib/db'; +import { + ClientProfileUpdates, + ClientProfileUpdatesResponse, + ClientProfileUpdatesResponseSchema, + UpdateHistory, +} from '@/types/clientProfileUpdates'; + +export class ClientProfileUpdatesService { + private prismaClient: PrismaClient = DBClient.getInstance(); + + async save(requestData: ClientProfileUpdates): Promise { + await this.prismaClient.clientProfileUpdates.create({ + data: { + clientId: requestData.clientId, + companyId: requestData.companyId, + portalId: requestData.portalId, + customFields: requestData.customFields, + changedFields: requestData.changedFields, + }, + }); + } + + // Company Filter is not applied if companyIds is empty + async findMany(portalId: string, companyIds: Array): Promise { + let clientProfileUpdates = []; + if (companyIds.length > 0) { + clientProfileUpdates = await this.prismaClient.clientProfileUpdates.findMany({ + where: { + portalId: portalId, + companyId: { + in: companyIds, + }, + }, + }); + } else { + clientProfileUpdates = await this.prismaClient.clientProfileUpdates.findMany({ + where: { + portalId: portalId, + }, + }); + } + + return ClientProfileUpdatesResponseSchema.parse(clientProfileUpdates); + } + + async getUpdateHistory(customFieldKey: string, clientId: string, lastUpdated: Date): Promise { + return this.prismaClient.$queryRaw` + SELECT "changedFields" + FROM "ClientProfileUpdates" + WHERE "clientId" = ${clientId}::uuid + AND "createdAt" <= ${lastUpdated} + AND "changedFields" ->> ${customFieldKey} IS NOT NULL + ORDER BY "createdAt" DESC; + `; + } +} diff --git a/src/app/api/custom-field-access/route.ts b/src/app/api/custom-field-access/route.ts index f40e8b2..fb0ddc8 100644 --- a/src/app/api/custom-field-access/route.ts +++ b/src/app/api/custom-field-access/route.ts @@ -1,31 +1,31 @@ import { NextRequest, NextResponse } from 'next/server'; import { CustomFieldAccessRequestSchema } from '@/types/customFieldAccess'; import { CustomFieldAccessService } from '@/app/api/custom-field-access/services/customFieldAccess.service'; -import { errorHandler } from '@/utils/common'; +import { respondError } from '@/utils/common'; import { CopilotAPI } from '@/utils/copilotApiUtils'; import { z } from 'zod'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const token = searchParams.get('token'); - const companyId = searchParams.get('companyId'); + const portalId = searchParams.get('portalId'); if (!token) { - return errorHandler('Missing token', 422); + return respondError('Missing token', 422); } - if (!companyId) { - return errorHandler('Missing companyId', 422); + if (!portalId) { + return respondError('Missing portalId', 422); } const copilotClient = new CopilotAPI(z.string().parse(token)); const customFields = (await copilotClient.getCustomFields()).data; const customFieldAccessService = new CustomFieldAccessService(); - const customFieldAccesses = await customFieldAccessService.findAll(z.string().uuid().parse(companyId)); + const customFieldAccesses = await customFieldAccessService.findAll(z.string().parse(portalId)); const customFieldsWithAccess = customFields?.map((customField) => { const customFieldAccess = customFieldAccesses.find((access) => access.customFieldId === customField.id); return { ...customField, - permission: customFieldAccess ? customFieldAccess.permission : null, + permission: customFieldAccess ? customFieldAccess.permissions : [], }; }); @@ -36,7 +36,7 @@ export async function PUT(request: NextRequest) { const data = await request.json(); const customFieldAccess = CustomFieldAccessRequestSchema.safeParse(data); if (!customFieldAccess.success) { - return NextResponse.json(customFieldAccess.error.issues); + return NextResponse.json(customFieldAccess.error.issues, { status: 422 }); } const customFieldAccessService = new CustomFieldAccessService(); await customFieldAccessService.save(customFieldAccess.data); diff --git a/src/app/api/custom-field-access/services/customFieldAccess.service.ts b/src/app/api/custom-field-access/services/customFieldAccess.service.ts index d6c16b1..accfce7 100644 --- a/src/app/api/custom-field-access/services/customFieldAccess.service.ts +++ b/src/app/api/custom-field-access/services/customFieldAccess.service.ts @@ -17,14 +17,14 @@ export class CustomFieldAccessService { customFieldId: { in: customFieldIds, }, - companyId: requestData.companyId, + portalId: requestData.portalId, }, }, }); const accesses = requestData.accesses.map((access) => { return { ...access, - companyId: requestData.companyId, + portalId: requestData.portalId, }; }); const createCustomFields = this.prismaClient.customFieldAccess.createMany({ @@ -34,10 +34,10 @@ export class CustomFieldAccessService { await this.prismaClient.$transaction([deleteCustomFields, createCustomFields]); } - async findAll(companyId: string): Promise { + async findAll(portalId: string): Promise { const customFieldAccesses = await this.prismaClient.customFieldAccess.findMany({ where: { - companyId: companyId, + portalId: portalId, }, }); diff --git a/src/app/api/custom-fields/route.ts b/src/app/api/custom-fields/route.ts index 282d96e..de58523 100644 --- a/src/app/api/custom-fields/route.ts +++ b/src/app/api/custom-fields/route.ts @@ -1,4 +1,4 @@ -import { errorHandler } from '@/utils/common'; +import { respondError } from '@/utils/common'; import { CopilotAPI } from '@/utils/copilotApiUtils'; import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; @@ -7,7 +7,7 @@ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const token = searchParams.get('token'); if (!token) { - errorHandler('Missing token', 422); + return respondError('Missing token', 422); } const copilotClient = new CopilotAPI(z.string().parse(token)); const { data } = await copilotClient.getCustomFields(); diff --git a/src/app/api/profile-update-history/route.ts b/src/app/api/profile-update-history/route.ts new file mode 100644 index 0000000..4c78eab --- /dev/null +++ b/src/app/api/profile-update-history/route.ts @@ -0,0 +1,53 @@ +import { handleError, respondError } from '@/utils/common'; +import { NextRequest, NextResponse } from 'next/server'; +import { ClientProfileUpdatesService } from '@/app/api/client-profile-updates/services/clientProfileUpdates.service'; +import { z } from 'zod'; +import { CopilotAPI } from '@/utils/copilotApiUtils'; +import { createLookup, getSelectedOptions } from '@/lib/helper'; + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const token = searchParams.get('token'); + const clientId = searchParams.get('clientId'); + const customFieldKey = searchParams.get('key'); + const lastUpdated = searchParams.get('lastUpdated'); + if (!token) { + return respondError('Missing token', 422); + } + if (!clientId) { + return respondError('Missing clientId', 422); + } + if (!customFieldKey) { + return respondError('Missing customFieldKey', 422); + } + if (!lastUpdated) { + return respondError('Missing lastUpdated', 422); + } + try { + const copilotClient = new CopilotAPI(z.string().parse(token)); + const portalCustomFields = await copilotClient.getCustomFields(); + const selectedCustomField = portalCustomFields.data?.find( + (portalCustomField) => portalCustomField.key === customFieldKey, + ); + if (!selectedCustomField) { + return respondError('Invalid customFieldKey.', 400); + } + const updateHistory = await new ClientProfileUpdatesService().getUpdateHistory( + z.string().parse(customFieldKey), + z.string().parse(clientId), + new Date(lastUpdated), + ); + const parsedUpdateHistory = updateHistory.map((update) => { + const value = update.changedFields[customFieldKey]; + const options = getSelectedOptions(selectedCustomField, value); + return { + type: selectedCustomField.type, + value: options.length > 0 ? options : value, + }; + }); + + return NextResponse.json(parsedUpdateHistory); + } catch (error) { + return handleError(error); + } +} diff --git a/src/app/api/settings/route.ts b/src/app/api/settings/route.ts new file mode 100644 index 0000000..35f7142 --- /dev/null +++ b/src/app/api/settings/route.ts @@ -0,0 +1,30 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { SettingRequestSchema } from '@/types/settings'; +import { SettingService } from '@/app/api/settings/services/setting.service'; +import { respondError } from '@/utils/common'; + +export async function PUT(request: NextRequest) { + const requestData = await request.json(); + const setting = SettingRequestSchema.safeParse(requestData); + if (!setting.success) { + return NextResponse.json(setting.error.issues, { status: 422 }); + } + await new SettingService().save(setting.data); + + return NextResponse.json({}); +} + +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const token = searchParams.get('token'); + const portalId = searchParams.get('portalId'); + if (!token) { + return respondError('Missing token', 422); + } + if (!portalId) { + return respondError('Missing portalId', 422); + } + const setting = await new SettingService().findByPortalId(portalId); + + return NextResponse.json({ data: setting?.data }); +} diff --git a/src/app/api/settings/services/setting.service.ts b/src/app/api/settings/services/setting.service.ts new file mode 100644 index 0000000..6317f24 --- /dev/null +++ b/src/app/api/settings/services/setting.service.ts @@ -0,0 +1,51 @@ +import DBClient from '@/lib/db'; +import { PrismaClient } from '@prisma/client'; +import { SettingRequest, SettingResponse, SettingResponseSchema } from '@/types/settings'; + +export class SettingService { + private prismaClient: PrismaClient = DBClient.getInstance(); + + async findByPortalId(portalId: string): Promise { + const setting = await this.prismaClient.setting.findFirst({ + where: { + portalId: portalId, + }, + }); + if (!setting) { + return null; + } + + return SettingResponseSchema.parse(setting); + } + + async save(requestData: SettingRequest): Promise { + const settingByPortal = await this.prismaClient.setting.findFirst({ + where: { + portalId: requestData.portalId, + }, + }); + if (!settingByPortal) { + await this.prismaClient.setting.create({ + data: { + portalId: requestData.portalId, + data: { + profileLinks: requestData.profileLinks, + }, + }, + }); + + return; + } + + await this.prismaClient.setting.update({ + where: { + id: settingByPortal.id, + }, + data: { + data: { + profileLinks: requestData.profileLinks, + }, + }, + }); + } +} diff --git a/src/lib/helper.ts b/src/lib/helper.ts new file mode 100644 index 0000000..f3df2cb --- /dev/null +++ b/src/lib/helper.ts @@ -0,0 +1,40 @@ +import { CustomField } from '@/types/common'; + +export function getObjectDifference(obj1: Record, obj2: Record) { + let diff: Record = {}; + for (let key in obj1) { + let value1 = obj1[key]; + let value2 = obj2[key]; + value1 = Array.isArray(value1) ? value1.sort() : value1; + value2 = Array.isArray(value2) ? value2.sort() : value2; + if (JSON.stringify(value1) !== JSON.stringify(value2)) { + diff[key] = value1; + } + } + return diff; +} + +// todo:: Need refactor, possibly create generics +export function createLookup(array: any[] | undefined | null, key: string): Record { + const lookup: Record = {}; + + array?.forEach((item) => { + lookup[item[key]] = item; + }); + + return lookup; +} + +export function getSelectedOptions(portalCustomField: CustomField, value: string | string[]) { + const options: unknown[] = []; + + if (portalCustomField.type === 'multiSelect' && value && Array.isArray(value) && portalCustomField.options) { + portalCustomField.options.forEach((option) => { + if (value.includes(option.key)) { + options.push(option); + } + }); + } + + return options; +} diff --git a/src/types/clientProfileUpdates.ts b/src/types/clientProfileUpdates.ts new file mode 100644 index 0000000..eadc137 --- /dev/null +++ b/src/types/clientProfileUpdates.ts @@ -0,0 +1,57 @@ +import { z } from 'zod'; + +export const CustomFieldUpdatesSchema = z.record(z.union([z.string(), z.array(z.string())])); +export type CustomFieldUpdates = z.infer; + +export const ClientProfileUpdatesRequestSchema = z.object({ + token: z.string(), + clientId: z.string().uuid(), + companyId: z.string().uuid(), + portalId: z.string(), + form: CustomFieldUpdatesSchema, +}); +export type ClientProfileUpdatesRequest = z.infer; + +export const ClientProfileUpdatesSchema = z.object({ + clientId: z.string().uuid(), + companyId: z.string().uuid(), + portalId: z.string(), + customFields: CustomFieldUpdatesSchema, + changedFields: CustomFieldUpdatesSchema, +}); +export type ClientProfileUpdates = z.infer; + +export const ClientProfileUpdatesResponseSchema = z.array( + z.object({ + id: z.string().uuid(), + clientId: z.string().uuid(), + companyId: z.string().uuid(), + customFields: CustomFieldUpdatesSchema, + changedFields: CustomFieldUpdatesSchema, + createdAt: z.date(), + }), +); +export type ClientProfileUpdatesResponse = z.infer; + +export const ParsedClientProfileUpdatesResponseSchema = z.object({ + id: z.string().uuid(), + client: z.object({ + id: z.string().uuid(), + name: z.string(), + email: z.string(), + avatarImageUrl: z.string().nullable(), + }), + company: z.object({ + id: z.string().uuid(), + name: z.string(), + iconImageUrl: z.string().nullable(), + }), + lastUpdated: z.date(), + customFields: z.unknown(), +}); +export type ParsedClientProfileUpdatesResponse = z.infer; + +export const UpdateHistorySchema = z.object({ + changedFields: CustomFieldUpdatesSchema, +}); +export type UpdateHistory = z.infer; diff --git a/src/types/common.ts b/src/types/common.ts index 39d164c..99d812d 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -15,6 +15,8 @@ export const ClientResponseSchema = z.object({ familyName: z.string(), email: z.string(), companyId: z.string(), + status: z.string(), + avatarImageUrl: z.string().nullable(), customFields: z.record(z.string(), z.union([z.string(), z.array(z.string())])).nullable(), }); export type ClientResponse = z.infer; @@ -27,10 +29,15 @@ export type ClientsResponse = z.infer; export const CompanyResponseSchema = z.object({ id: z.string(), name: z.string(), - iconImageUrl: z.string(), + iconImageUrl: z.string().nullable(), }); export type CompanyResponse = z.infer; +export const CompaniesResponseSchema = z.object({ + data: z.array(CompanyResponseSchema).nullable(), +}); +export type CompaniesResponse = z.infer; + export const CustomFieldSchema = z.object({ id: z.string(), key: z.string(), @@ -49,7 +56,16 @@ export const CustomFieldSchema = z.object({ ) .optional(), }); +export type CustomField = z.infer; export const CustomFieldResponseSchema = z.object({ data: z.array(CustomFieldSchema).nullable(), }); export type CustomFieldResponse = z.infer; + +export const ClientRequestSchema = z.object({ + givenName: z.string().optional(), + familyName: z.string().optional(), + companyId: z.string().uuid().optional(), + customFields: z.record(z.union([z.string(), z.array(z.string())])).optional(), +}); +export type ClientRequest = z.infer; diff --git a/src/types/customFieldAccess.ts b/src/types/customFieldAccess.ts index b028af3..62aaae2 100644 --- a/src/types/customFieldAccess.ts +++ b/src/types/customFieldAccess.ts @@ -3,11 +3,11 @@ import { Permission } from '@prisma/client'; export const CustomFieldAccessRequestSchema = z.object({ token: z.string(), - companyId: z.string().uuid(), + portalId: z.string(), accesses: z.array( z.object({ customFieldId: z.string().uuid(), - permission: z.nativeEnum(Permission), + permissions: z.array(z.nativeEnum(Permission)), }), ), }); @@ -16,8 +16,8 @@ export type CustomFieldAccessRequest = z.infer; diff --git a/src/types/settings.ts b/src/types/settings.ts new file mode 100644 index 0000000..15ba827 --- /dev/null +++ b/src/types/settings.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +enum ProfileLinks { + ProfileSetting = 'profile_settings', + PaymentMethod = 'payment_method', +} + +export const SettingRequestSchema = z.object({ + token: z.string(), + portalId: z.string(), + profileLinks: z.array(z.nativeEnum(ProfileLinks)), +}); +export type SettingRequest = z.infer; + +export const SettingResponseSchema = z.object({ + id: z.string().uuid(), + data: z.object({ + profileLinks: z.array(z.nativeEnum(ProfileLinks)), + }), +}); +export type SettingResponse = z.infer; diff --git a/src/utils/common.ts b/src/utils/common.ts index 2324192..7023261 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,6 +1,7 @@ import { NextResponse } from 'next/server'; import { CopilotAPI } from './copilotApiUtils'; import { MeResponse } from '@/types/common'; +import { ApiError } from 'copilot-node-sdk/codegen/api'; export async function getCurrentUser(apiToken: string): Promise { const copilotClient = new CopilotAPI(apiToken); @@ -8,7 +9,7 @@ export async function getCurrentUser(apiToken: string): Promise { return await copilotClient.me(); } -export function errorHandler(message: string, status: number = 200) { +export function respondError(message: string, status: number = 500) { return NextResponse.json( { message }, { @@ -16,3 +17,18 @@ export function errorHandler(message: string, status: number = 200) { }, ); } + +export function handleError(error: unknown) { + console.error(error); + let apiError = { + message: 'Something went wrong', + status: 500, + }; + if (error instanceof ApiError) { + apiError = { + message: error.body.message, + status: error.status, + }; + } + return respondError(apiError.message, apiError.status); +} diff --git a/src/utils/copilotApiUtils.ts b/src/utils/copilotApiUtils.ts index b4e5754..618108c 100644 --- a/src/utils/copilotApiUtils.ts +++ b/src/utils/copilotApiUtils.ts @@ -6,10 +6,13 @@ import { ClientsResponseSchema, CompanyResponse, CompanyResponseSchema, + ClientRequest, CustomFieldResponse, CustomFieldResponseSchema, MeResponse, MeResponseSchema, + CompaniesResponse, + CompaniesResponseSchema, } from '@/types/common'; import { copilotAPIKey } from '@/config'; @@ -35,10 +38,19 @@ export class CopilotAPI { return ClientsResponseSchema.parse(await this.copilot.listClients({})); } + async updateClient(clientId: string, requestBody: ClientRequest): Promise { + // @ts-ignore + return ClientResponseSchema.parse(await this.copilot.updateAClient({ id: clientId, requestBody })); + } + async getCompany(companyId: string): Promise { return CompanyResponseSchema.parse(await this.copilot.retrieveACompany({ id: companyId })); } + async getCompanies(): Promise { + return CompaniesResponseSchema.parse(await this.copilot.listCompanies({})); + } + async getCustomFields(): Promise { return CustomFieldResponseSchema.parse(await this.copilot.listCustomFields()); }