diff --git a/web_app/package.json b/web_app/package.json index f80e1d6..d464d12 100644 --- a/web_app/package.json +++ b/web_app/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-hover-card": "^1.1.1", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", diff --git a/web_app/pnpm-lock.yaml b/web_app/pnpm-lock.yaml index 0af8cbd..fd4bce8 100644 --- a/web_app/pnpm-lock.yaml +++ b/web_app/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: '@radix-ui/react-popover': specifier: ^1.0.7 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) @@ -1529,6 +1532,27 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: diff --git a/web_app/src/app/(main)/agency/[agencyId]/page.tsx b/web_app/src/app/(main)/agency/[agencyId]/page.tsx index 840a926..52b7f7f 100644 --- a/web_app/src/app/(main)/agency/[agencyId]/page.tsx +++ b/web_app/src/app/(main)/agency/[agencyId]/page.tsx @@ -1,7 +1,192 @@ +import CircleProgress from "@/components/global/CircleProgress"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { Separator } from "@/components/ui/separator"; +import { db } from "@/lib/db"; +import { AreaChart } from "@tremor/react"; +import { + ClipboardIcon, + Contact2, + DollarSign, + Goal, + ShoppingCart, +} from "lucide-react"; +import Link from "next/link"; import React from "react"; -const Page = ({ params }: { params: { agencyId: string } }) => { - return
{params.agencyId}
; +const Page = async ({ + params, +}: { + params: { agencyId: string }; + searchParams: { code: string }; +}) => { + let currency = "USD"; + let sessions; + let totalClosedSessions; + let totalPendingSessions; + let net = 0; + let potentialIncome = 0; + let closingRate = 0; + const currentYear = new Date().getFullYear(); + const startDate = new Date(`${currentYear}-01-01T00:00:00Z`).getTime() / 1000; + const endDate = new Date(`${currentYear}-12-31T23:59:59Z`).getTime() / 1000; + + const agencyDetails = await db.agency.findUnique({ + where: { + id: params.agencyId, + }, + }); + + if (!agencyDetails) return; + + const subaccounts = await db.subAccount.findMany({ + where: { + agencyId: params.agencyId, + }, + }); + + + + return ( +
+ +

Dashboard

+ +
+
+ + + Income + + {net ? `${currency} ${net.toFixed(2)}` : `$0.00`} + + + For the year {currentYear} + + + + Total revenue generated as reflected in your stripe dashboard. + + + + + + Potential Income + + {potentialIncome + ? `${currency} ${potentialIncome.toFixed(2)}` + : `$0.00`} + + + For the year {currentYear} + + + + This is how much you can close. + + + + + + Active Clients + {subaccounts.length} + + + Reflects the number of sub accounts you own and manage. + + + + + + Agency Goal + +

+ Reflects the number of sub accounts you want to own and + manage. +

+
+
+ +
+
+ + Current: {subaccounts.length} + + + Goal: {agencyDetails.goal} + +
+ +
+
+ +
+
+
+ + + Transaction History + + + + + + Conversions + + + + {sessions && ( +
+ Abandoned +
+ + {/* {sessions.length || 0} */} + 0 +
+
+ )} + {totalClosedSessions && ( +
+ Won Carts +
+ + {/* {totalClosedSessions.length} */} + 0 +
+
+ )} + + } + /> +
+
+
+
+
+ ); }; export default Page; diff --git a/web_app/src/app/(main)/subaccount/[subAccountId]/page.tsx b/web_app/src/app/(main)/subaccount/[subAccountId]/page.tsx index 15447c8..f990c86 100644 --- a/web_app/src/app/(main)/subaccount/[subAccountId]/page.tsx +++ b/web_app/src/app/(main)/subaccount/[subAccountId]/page.tsx @@ -1,11 +1,238 @@ -import React from 'react' +import BlurPage from "@/components/global/BlurPage"; +import CircleProgress from "@/components/global/CircleProgress"; +import PipelineValue from "@/components/global/PipelineValue"; +import SubaccountFunnelChart from "@/components/global/SubAccountFunnelChart"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; -type Props = {} +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { db } from "@/lib/db"; +import { AreaChart, BadgeDelta } from "@tremor/react"; +import { + ClipboardIcon, + Contact2, + DollarSign, + ShoppingCart, +} from "lucide-react"; +import Link from "next/link"; +import React from "react"; + +type Props = { + params: { subAccountId: string }; + searchParams: { + code: string; + }; +}; + +const SubaccountPageId = async ({ params, searchParams }: Props) => { + let currency = "USD"; + let sessions; + let totalClosedSessions; + let totalPendingSessions; + let net = 0; + let potentialIncome = 0; + let closingRate = 0; + + const subaccountDetails = await db.subAccount.findUnique({ + where: { + id: params.subAccountId, + }, + }); + + const currentYear = new Date().getFullYear(); + const startDate = new Date(`${currentYear}-01-01T00:00:00Z`).getTime() / 1000; + const endDate = new Date(`${currentYear}-12-31T23:59:59Z`).getTime() / 1000; + + if (!subaccountDetails) return; + + const funnels = await db.funnel.findMany({ + where: { + subAccountId: params.subAccountId, + }, + include: { + FunnelPages: true, + }, + }); + + const funnelPerformanceMetrics = funnels.map((funnel) => ({ + ...funnel, + totalFunnelVisits: funnel.FunnelPages.reduce( + (total, page) => total + page.visits, + 0 + ), + })); -const page = (props: Props) => { return ( -
page
- ) -} + +
+
+
+ + + Income + + {net ? `${currency} ${net.toFixed(2)}` : `$0.00`} + + + For the year {currentYear} + + + + Total revenue generated as reflected in your stripe dashboard. + + + + + + Potential Income + + {potentialIncome + ? `${currency} ${potentialIncome.toFixed(2)}` + : `$0.00`} + + + For the year {currentYear} + + + + This is how much you can close. + + + + + + + + Conversions + + {sessions && ( +
+ Total Carts Opened +
+ + {/* {sessions.length} */}0 +
+
+ )} + {totalClosedSessions && ( +
+ Won Carts +
+ + {/* {totalClosedSessions.length} */}0 +
+
+ )} + + } + /> +
+
+
+ +
+ + + Funnel Performance + + + +
+ Total page visits across all funnels. Hover over to get more + details on funnel page performance. +
+
+ +
+ + + Checkout Activity + + + +
+
+ + + + Transition History + + +12.3% + + + + + + Email + Status + Created Date + Value + + + + {totalClosedSessions + ? totalClosedSessions.map((session) => ( + + + {session.customer_details?.email || "-"} + + + + Paid + + + + {new Date(session.created).toUTCString()} + + + + {currency}{" "} + + {session.amount_total} + + + + )) + : "No Data"} + +
+
+
+
+
+
+
+ ); +}; -export default page \ No newline at end of file +export default SubaccountPageId; diff --git a/web_app/src/components/global/CircleProgress.tsx b/web_app/src/components/global/CircleProgress.tsx new file mode 100644 index 0000000..89de490 --- /dev/null +++ b/web_app/src/components/global/CircleProgress.tsx @@ -0,0 +1,29 @@ +"use client"; +import { ProgressCircle } from "@tremor/react"; +import React from "react"; + +type Props = { + value: number; + description: React.ReactNode; +}; + +const CircleProgress = ({ description, value = 0 }: Props) => { + return ( +
+ + {value}% + +
+ Closing Rate +

{description}

+
+
+ ); +}; + +export default CircleProgress; diff --git a/web_app/src/components/global/PipelineValue.tsx b/web_app/src/components/global/PipelineValue.tsx new file mode 100644 index 0000000..0697497 --- /dev/null +++ b/web_app/src/components/global/PipelineValue.tsx @@ -0,0 +1,114 @@ +"use client"; +import { getPipelines } from "@/lib/queries"; +import { Prisma } from "@prisma/client"; +import React, { useEffect, useMemo, useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader } from "../ui/card"; +import { Progress } from "../ui/progress"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "../ui/select"; + +type Props = { + subaccountId: string; +}; + +const PipelineValue = ({ subaccountId }: Props) => { + const [pipelines, setPipelines] = useState< + Prisma.PromiseReturnType + >([]); + + const [selectedPipelineId, setselectedPipelineId] = useState(""); + const [pipelineClosedValue, setPipelineClosedValue] = useState(0); + + useEffect(() => { + const fetchData = async () => { + const res = await getPipelines(subaccountId); + setPipelines(res); + setselectedPipelineId(res[0]?.id); + }; + fetchData(); + }, [subaccountId]); + + const totalPipelineValue = useMemo(() => { + if (pipelines.length) { + return ( + pipelines + .find((pipeline) => pipeline.id === selectedPipelineId) + ?.Lane?.reduce((totalLanes, lane, currentLaneIndex, array) => { + const laneTicketsTotal = lane.Tickets.reduce( + (totalTickets, ticket) => totalTickets + Number(ticket?.value), + 0 + ); + if (currentLaneIndex === array.length - 1) { + setPipelineClosedValue(laneTicketsTotal || 0); + return totalLanes; + } + return totalLanes + laneTicketsTotal; + }, 0) || 0 + ); + } + return 0; + }, [selectedPipelineId, pipelines]); + + const pipelineRate = useMemo( + () => + (pipelineClosedValue / (totalPipelineValue + pipelineClosedValue)) * 100, + [pipelineClosedValue, totalPipelineValue] + ); + + return ( + + + Pipeline Value + + Pipeline Progress + +
+
+

+ Closed ${pipelineClosedValue} +

+
+
+

+ Total ${totalPipelineValue + pipelineClosedValue} +

+
+
+ +
+ +

+ Total value of all tickets in the given pipeline except the last lane. + Your last lane is considered your closing lane in every pipeline. +

+ +
+
+ ); +}; + +export default PipelineValue; diff --git a/web_app/src/components/global/SubAccountFunnelChart.tsx b/web_app/src/components/global/SubAccountFunnelChart.tsx new file mode 100644 index 0000000..8506488 --- /dev/null +++ b/web_app/src/components/global/SubAccountFunnelChart.tsx @@ -0,0 +1,65 @@ +"use client"; +import { DonutChart } from "@tremor/react"; +import React from "react"; + +type Props = { data: any }; + +const SubaccountFunnelChart = ({ data }: Props) => { + return ( +
+ +
+ ); +}; + +export default SubaccountFunnelChart; + +const customTooltip = ({ + payload, + active, +}: { + payload: any; + active: boolean; +}) => { + if (!active || !payload) return null; + + const categoryPayload = payload?.[0]; + if (!categoryPayload) return null; + return ( +
+
+
+
+
+

+ {categoryPayload.name} +

+

+ {categoryPayload.value} +

+
+
+
+ {categoryPayload.payload.FunnelPages?.map((page: any) => ( +
+ {page.name} + {page.visits} +
+ ))} +
+ ); +}; diff --git a/web_app/src/components/ui/progress.tsx b/web_app/src/components/ui/progress.tsx new file mode 100644 index 0000000..5c87ea4 --- /dev/null +++ b/web_app/src/components/ui/progress.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/web_app/src/lib/queries.ts b/web_app/src/lib/queries.ts index 0f75f2c..ae5d1e1 100644 --- a/web_app/src/lib/queries.ts +++ b/web_app/src/lib/queries.ts @@ -1057,4 +1057,16 @@ export const getDomainContent = async (subDomainName : string) => { } }) return res; +} + +export const getPipelines = async (subaccountId: string) => { + const response = await db.pipeline.findMany({ + where: { subAccountId: subaccountId }, + include: { + Lane: { + include: { Tickets: true }, + }, + }, + }) + return response } \ No newline at end of file