diff --git a/.env.example b/.env.example index 91203dd..30d4c00 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,14 @@ -COPILOT_API_KEY="" -POSTGRES_PRISMA_URL="" -POSTGRES_URL_NON_POOLING="" +COPILOT_API_KEY="b4fa0b79be594f91a2c72c63216d45ec.6463d9dc556351b6" + +# Vercel Database +POSTGRES_PRISMA_URL="postgres://default:2tCAVY7Npmyc@ep-morning-mountain-74331247-pooler.us-east-1.postgres.vercel-storage.com/verceldb?pgbouncer=true&connect_timeout=15" +POSTGRES_URL_NON_POOLING="postgres://default:2tCAVY7Npmyc@ep-morning-mountain-74331247.us-east-1.postgres.vercel-storage.com/verceldb" + +# Local Database +; POSTGRES_PRISMA_URL="postgresql://db:db@127.0.0.1:59004/db?schema=public" +; POSTGRES_URL_NON_POOLING="postgresql://db:db@127.0.0.1:59004/db?schema=public" + +COPILOT_API_URL="https://api-beta.copilot.com" COPILOT_ENV="local" -COPILOT_API_URL="" +VERCEL_URL="localhost:3000" +VERCEL_ENV="development" diff --git a/src/app/api/client-profile-updates/route.ts b/src/app/api/client-profile-updates/route.ts index a1f6b58..f4faecf 100644 --- a/src/app/api/client-profile-updates/route.ts +++ b/src/app/api/client-profile-updates/route.ts @@ -70,11 +70,19 @@ export async function GET(request: NextRequest) { const client = clientLookup[update.clientId]; const company = companyLookup[update.companyId]; - const customFields = portalCustomFields.data?.map((portalCustomField) => { + let parsedClientProfileUpdate: ParsedClientProfileUpdatesResponse = { + id: update.id, + client: getClientDetails(client), + company: getCompanyDetails(company), + lastUpdated: update.createdAt, + }; + + portalCustomFields.data?.forEach((portalCustomField) => { const value = update.customFields[portalCustomField.key] ?? null; const options = getSelectedOptions(portalCustomField, value); - return { + // @ts-ignore + parsedClientProfileUpdate[portalCustomField.name] = { name: portalCustomField.name, type: portalCustomField.type, key: portalCustomField.key, @@ -83,13 +91,7 @@ export async function GET(request: NextRequest) { }; }); - return { - id: update.id, - client: getClientDetails(client), - company: getCompanyDetails(company), - lastUpdated: update.createdAt, - customFields, - }; + return parsedClientProfileUpdate; }); return NextResponse.json(parsedClientProfileUpdates); diff --git a/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts b/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts index 70d9888..b1b7526 100644 --- a/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts +++ b/src/app/api/client-profile-updates/services/clientProfileUpdates.service.ts @@ -11,7 +11,7 @@ export class ClientProfileUpdatesService { private prismaClient: PrismaClient = DBClient.getInstance(); async save(requestData: ClientProfileUpdates): Promise { - await this.prismaClient.clientProfileUpdates.create({ + await this.prismaClient.clientProtileUpdates.create({ data: { clientId: requestData.clientId, companyId: requestData.companyId, @@ -52,7 +52,8 @@ export class ClientProfileUpdatesService { WHERE "clientId" = ${clientId}::uuid AND "createdAt" <= ${lastUpdated} AND "changedFields" ->> ${customFieldKey} IS NOT NULL - ORDER BY "createdAt" DESC; + ORDER BY "createdAt" DESC + LIMIT 5; `; } } diff --git a/src/app/page.tsx b/src/app/page.tsx index a178995..f2dd945 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,12 +1,38 @@ -import { Box, Stack, Typography } from '@mui/material'; +import { Stack } from '@mui/material'; import { Sidebar } from './views/Sidebar'; import MainSection from './views/MainSection'; import { SidebarDecider } from '@/hoc/SidebarDecider'; +import { apiUrl } from '@/config'; +import { ParsedClientProfileUpdatesResponse } from '@/types/clientProfileUpdates'; + +export const revalidate = 0; + +async function getClientProfileUpdates({ + token, + portalId, +}: { + token: string; + portalId: string; +}): Promise { + const res = await fetch(`${apiUrl}/api/client-profile-updates?token=${token}&portalId=${portalId}`); + + if (!res.ok) { + throw new Error('Something went wrong while in getClientProfileUpdates'); + } + + const data = await res.json(); + + return data; +} + +export default async function Home({ searchParams }: { searchParams: { token: string; portalId: string } }) { + const { token, portalId } = searchParams; + + const clientProfileUpdates = await getClientProfileUpdates({ token, portalId }); -export default function Home() { return ( - + diff --git a/src/app/views/MainSection.tsx b/src/app/views/MainSection.tsx index dd0e7f7..71fd535 100644 --- a/src/app/views/MainSection.tsx +++ b/src/app/views/MainSection.tsx @@ -6,11 +6,21 @@ import { Header } from '@/layouts/Header'; import { Box } from '@mui/material'; import { Sidebar } from './Sidebar'; import { TableCore } from '@/components/table/Table'; +import { ParsedClientProfileUpdatesResponse } from '@/types/clientProfileUpdates'; +import { useEffect } from 'react'; -const MainSection = () => { +interface IMainSection { + clientProfileUpdates: ParsedClientProfileUpdatesResponse[]; +} + +const MainSection = ({ clientProfileUpdates }: IMainSection) => { const appState = useAppState(); const windowWidth = useWindowWidth(); + useEffect(() => { + appState?.setAppState((prev) => ({ ...prev, clientProfileUpdates })); + }, [clientProfileUpdates]); + return ( { const appState = useAppState(); // Row Data: The data to be displayed. - const [rowData, setRowData] = useState([ - { - Client: { image: 'https://robohash.org/stefan-one', name: 'Tesla', email: 'tesla@gmail.com', id: 1 }, - company: { image: 'https://robohash.org/generate', name: 'Chase' }, - 'Last updated': '1m', - 'Phone number': { value: '1928366444', showDot: true }, - Address: 'Texas', - Hobby: 'Music', - }, - { - Client: { image: 'https://robohash.org/stefan-eleven', name: 'Wongchi', email: 'wongchi@gmail.com', id: 2 }, - company: { image: 'https://robohash.org/generate', name: 'Amazon' }, - 'Last updated': '1d', - 'Phone number': { value: '9283774466', showDot: false }, - Address: '', - Hobby: 'Dance', - }, - { - Client: { image: 'https://robohash.org/stefan-hundred', name: 'Holyland', email: 'holyland@gmail.com', id: 3 }, - company: { image: 'https://robohash.org/generate', name: 'Walt Disney Co.' }, - 'Last updated': '', - 'Phone number': { value: '2883743322', showDot: true }, - Address: 'New York', - Hobby: 'Flying', - }, - { - Client: { image: 'https://robohash.org/stefan-five', name: 'Nokia', email: 'nokia@gmail.com', id: 4 }, - company: { image: 'https://robohash.org/generate', name: 'Scrappy' }, - 'Last updated': '2d', - 'Phone number': { value: '9182663344', showDot: true }, - Address: 'Hongkong', - Hobby: '', - }, - { - Client: { image: 'https://robohash.org/stefan-four', name: 'Prolink', email: 'prolink@gmail.com', id: 5 }, - company: { image: 'https://robohash.org/generate', name: 'Facebook' }, - 'Last updated': '1 month', - 'Phone number': { value: '', showDot: false }, - Address: 'Manila', - Hobby: 'Diving', - }, - { - Client: { image: 'https://robohash.org/stefan-three', name: 'Apple', email: 'apple@gmail.com', id: 6 }, - company: { image: 'https://robohash.org/generate', name: 'Catch' }, - 'Last updated': '12 hours', - 'Phone number': { value: '3847228833', showDot: false }, - Address: 'Kathmandu', - Hobby: 'Music', - chobby: 'Music', - }, - ]); + const [rowData, setRowData] = useState([]); + // + // Column Definitions: Defines & controls grid columns. + const [colDefs, setColDefs] = useState([]); + + useEffect(() => { + setRowData(appState?.clientProfileUpdates); - const comparator = (valueA: any, valueB: any) => { - if (valueA < valueB) { + let colDefs: any = []; + if (appState?.clientProfileUpdates.length && appState?.clientProfileUpdates.length) { + const col = appState?.clientProfileUpdates[0]; + delete col.id; + const keys = Object.keys(col); + keys.map((el) => { + if (el === 'client') { + colDefs = [ + ...colDefs, + { + field: 'client', + cellRenderer: ClientCellRenderer, + flex: 1, + comparator: comparatorTypeI, + minWidth: 250, + valueGetter: (params: any) => { + const client = params.data[el]; + return { + avatarImageUrl: client.avatarImageUrl, + name: client.name, + email: client.email, + }; + }, + }, + ]; + return; + } + if (el === 'company') { + colDefs = [ + ...colDefs, + { + field: 'company', + cellRenderer: CompanyCellRenderer, + flex: 1, + comparator: comparatorTypeI, + valueGetter: (params: any) => { + const company = params.data[el]; + return { + iconImageUrl: company.iconImageUrl, + name: company.name, + }; + }, + }, + ]; + return; + } + if (el === 'lastUpdated') { + colDefs = [ + ...colDefs, + { + field: 'lastUpdated', + flex: 1, + valueGetter: (params: any) => { + const lastUpdated = params.data[el]; + return `${getTimeAgo(lastUpdated)} ago`; + }, + }, + ]; + return; + } + + colDefs = [ + ...colDefs, + { + field: el, + flex: 1, + cellRenderer: HistoryCellRenderer, + }, + ]; + }); + setColDefs(colDefs); + } + }, [appState?.clientProfileUpdates]); + + const comparatorTypeI = (valueA: any, valueB: any) => { + if (valueA.name < valueB.name) { return -1; } - if (valueA > valueB) { + if (valueA.name > valueB.name) { return 1; } return 0; // names are equal }; - // Column Definitions: Defines & controls grid columns. - const [colDefs, setColDefs] = useState([ - { - field: 'Client', - cellRenderer: ClientCellRenderer, - flex: 1, - comparator, - valueGetter: (params: any) => { - const client = params.data.Client; - return `${client.image} ${client.name} ${client.email}`; - }, - }, - { - field: 'company', - cellRenderer: CompanyCellRenderer, - flex: 1, - comparator, - valueGetter: (params: any) => { - const company = params.data.company; - return `${company.image} ${company.name}`; - }, - }, - { field: 'Last updated', flex: 1 }, - { - field: 'Phone number', - flex: 1, - cellRenderer: HistoryCellRenderer, - valueGetter: (params: any) => { - const phoneProps = params.data['Phone number']; - return `${phoneProps.value} ${phoneProps.showDot}`; - }, - }, - { field: 'Address', flex: 1 }, - { field: 'Hobby', flex: 1 }, - ]); - const defaultColDef = useMemo(() => { return { resizable: false, diff --git a/src/components/table/cellRenderers/ClientCellRenderer.tsx b/src/components/table/cellRenderers/ClientCellRenderer.tsx index e97c4ec..d395a03 100644 --- a/src/components/table/cellRenderers/ClientCellRenderer.tsx +++ b/src/components/table/cellRenderers/ClientCellRenderer.tsx @@ -1,16 +1,16 @@ import { Box, Stack, Typography } from '@mui/material'; -export const ClientCellRenderer = ({ value }: { value: string }) => { - const c = value.split(' '); +export const ClientCellRenderer = ({ value }: { value: { avatarImageUrl: string; email: string; name: string } }) => { + const { avatarImageUrl, email, name } = value; return ( - + - {c[1]} + {name} - {c[2]} + {email} diff --git a/src/components/table/cellRenderers/CompanyCellRenderer.tsx b/src/components/table/cellRenderers/CompanyCellRenderer.tsx index 2b315db..a0036cd 100644 --- a/src/components/table/cellRenderers/CompanyCellRenderer.tsx +++ b/src/components/table/cellRenderers/CompanyCellRenderer.tsx @@ -1,12 +1,12 @@ import { Box, Stack, Typography } from '@mui/material'; -export const CompanyCellRenderer = ({ value }: { value: string }) => { - const c = value.split(' '); +export const CompanyCellRenderer = ({ value }: { value: { iconImageUrl: string; name: string } }) => { + const { iconImageUrl, name } = value; return ( - + - {c[1]} + {name} ); diff --git a/src/components/table/cellRenderers/HistoryCellRenderer.tsx b/src/components/table/cellRenderers/HistoryCellRenderer.tsx index 6bc0d8d..a4ae98e 100644 --- a/src/components/table/cellRenderers/HistoryCellRenderer.tsx +++ b/src/components/table/cellRenderers/HistoryCellRenderer.tsx @@ -1,10 +1,12 @@ import { Box, Popper, Stack, Typography } from '@mui/material'; import React from 'react'; -export const HistoryCellRenderer = ({ value }: { value: string }) => { - const h = value.split(' '); - const val = h[0]; - const showDot = h[1] === 'true'; +export const HistoryCellRenderer = ({ + value, +}: { + value: { name: string; type: string; key: string; value: any; isChanged: boolean }; +}) => { + const showDot = value.isChanged; const [anchorEl, setAnchorEl] = React.useState(null); const handleMouseEnter = (event: React.MouseEvent) => { @@ -22,6 +24,64 @@ export const HistoryCellRenderer = ({ value }: { value: string }) => { const open = Boolean(anchorEl); const id = open ? 'simple-popper' : undefined; + if (value.value === null) { + return null; + } + + if (value.type === 'multiSelect') { + return ( + + {showDot && ( + + • + + )} + + + + + + {value.value ? value.value[0].label : ''} + + + + +{value.value.length - 1} + + + ); + } + return ( {showDot && ( @@ -40,7 +100,7 @@ export const HistoryCellRenderer = ({ value }: { value: string }) => { • )} - {val} + {value.value} diff --git a/src/config/index.ts b/src/config/index.ts index abd9912..d716e57 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,2 +1,3 @@ export const copilotAPIUrl = process.env.COPILOT_API_URL || ''; export const copilotAPIKey = process.env.COPILOT_API_KEY || ''; +export const apiUrl = `${process.env.VERCEL_ENV === 'development' ? 'http://' : 'https://'}${process.env.VERCEL_URL}`; diff --git a/src/context/index.tsx b/src/context/index.tsx index 458329a..67dfd8b 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -5,11 +5,13 @@ import { FC, ReactNode, useState, createContext, Dispatch, SetStateAction } from export interface IAppState { showSidebar: boolean; searchKeyword: string; + clientProfileUpdates: any[]; } export interface IAppContext { showSidebar: boolean; searchKeyword: string; + clientProfileUpdates: any[]; setAppState: Dispatch>; } @@ -23,6 +25,7 @@ export const AppContextProvider: FC = ({ children }) => { const [state, setState] = useState({ showSidebar: false, searchKeyword: '', + clientProfileUpdates: [], }); return ( @@ -30,6 +33,7 @@ export const AppContextProvider: FC = ({ children }) => { value={{ showSidebar: state.showSidebar, searchKeyword: state.searchKeyword, + clientProfileUpdates: state.clientProfileUpdates, setAppState: setState, }} > diff --git a/src/types/clientProfileUpdates.ts b/src/types/clientProfileUpdates.ts index eadc137..1045269 100644 --- a/src/types/clientProfileUpdates.ts +++ b/src/types/clientProfileUpdates.ts @@ -47,7 +47,6 @@ export const ParsedClientProfileUpdatesResponseSchema = z.object({ iconImageUrl: z.string().nullable(), }), lastUpdated: z.date(), - customFields: z.unknown(), }); export type ParsedClientProfileUpdatesResponse = z.infer; diff --git a/src/utils/getTimeAgo.ts b/src/utils/getTimeAgo.ts new file mode 100644 index 0000000..a34d141 --- /dev/null +++ b/src/utils/getTimeAgo.ts @@ -0,0 +1,25 @@ +export function getTimeAgo(dateString: string): string { + const currentDate = new Date(); + const inputDate = new Date(dateString); + + const timeDifferenceInSeconds = Math.floor((currentDate.getTime() - inputDate.getTime()) / 1000); + + if (timeDifferenceInSeconds < 60) { + return 'just now'; + } else if (timeDifferenceInSeconds < 3600) { + const minutes = Math.floor(timeDifferenceInSeconds / 60); + return `${minutes}m`; + } else if (timeDifferenceInSeconds < 86400) { + const hours = Math.floor(timeDifferenceInSeconds / 3600); + return `${hours}h`; + } else if (timeDifferenceInSeconds < 2592000) { + const days = Math.floor(timeDifferenceInSeconds / 86400); + return `${days}d`; + } else if (timeDifferenceInSeconds < 31536000) { + const months = Math.floor(timeDifferenceInSeconds / 2592000); + return `${months}mo`; + } else { + const years = Math.floor(timeDifferenceInSeconds / 31536000); + return `${years}y`; + } +} diff --git a/yarn.lock b/yarn.lock index dcfcc2b..fe559a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3740,7 +3740,7 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== -next@^14.0.2, next@latest: +next@14.1.0, next@^14.0.2: version "14.1.0" resolved "https://registry.yarnpkg.com/next/-/next-14.1.0.tgz#b31c0261ff9caa6b4a17c5af019ed77387174b69" integrity sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==