From 6cb2105f92bff85569a39155960d08f000125b78 Mon Sep 17 00:00:00 2001 From: OlufunbiIK Date: Sun, 29 Mar 2026 17:03:52 +0100 Subject: [PATCH 1/2] =?UTF-8?q?feat:Issue=20#75=20=E2=80=94=20Backend:=20D?= =?UTF-8?q?ashboard=20Controller=20=E2=80=94=20Member=20Dashboard=20Endpoi?= =?UTF-8?q?nts=20#651?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/page.tsx | 343 ++++++++++++++++-- .../admin/analytics/UseGetMemberDashboard.ts | 53 +++ .../admin/analytics/useGetAdminAnalytics.ts | 77 ++++ frontend/lib/react-query/keys/queryKeys.ts | 8 + frontend/package-lock.json | 153 ++++++-- frontend/package.json | 3 +- 6 files changed, 574 insertions(+), 63 deletions(-) create mode 100644 frontend/lib/hooks/admin/analytics/UseGetMemberDashboard.ts create mode 100644 frontend/lib/hooks/admin/analytics/useGetAdminAnalytics.ts create mode 100644 frontend/lib/react-query/keys/queryKeys.ts diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 7dd0a13c..dce2a473 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,27 +1,322 @@ -import { Navbar } from "@/components/ui/Navbar"; -import { Hero } from "@/components/ui/Hero"; -import TrustedBy from "@/components/ui/TrustedBy"; -import FeaturesSection from "@/components/ui/FeaturesSection"; -import HowItWorks from "@/components/ui/HowItWorks"; -import Newsletter from "@/components/ui/Newsletter"; -import Footer from "@/components/ui/Footer"; - -export const metadata = { - title: "ManageHub - Smart Hub & Workspace Management", - description: - "Simplify how you manage workspaces, teams, and resources. ManageHub brings everything together in one place.", -}; - -export default function Home() { +"use client"; + +import { useState } from "react"; +import { RefreshCw } from "lucide-react"; +import DashboardLayout from "@/components/dashboard/DashboardLayout"; +import { + AdminAnalytics, + useGetAdminAnalytics, +} from "@/lib/hooks/admin/analytics/useGetAdminAnalytics"; + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +function formatNaira(kobo: number): string { + return `₦${(kobo / 100).toLocaleString("en-NG", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`; +} + +// ─── Sub-components ────────────────────────────────────────────────────────── + +function StatCard({ + label, + value, + sub, +}: { + label: string; + value: string | number; + sub?: string; +}) { + return ( +
+

{label}

+

{value}

+ {sub &&

{sub}

} +
+ ); +} + +function SectionTitle({ children }: { children: React.ReactNode }) { + return ( +

+ {children} +

+ ); +} + +function RevenueChart({ + trend, +}: { + trend: AdminAnalytics["revenue"]["trend"]; +}) { + const max = Math.max(...trend.map((m) => m.revenueKobo), 1); + + return ( +
+ Revenue Trend (6 months) +
+ {trend.map((m) => { + const pct = Math.round((m.revenueKobo / max) * 100); + return ( +
+
0 ? "4px" : "0" }} + /> + {m.month} +
+ ); + })} +
+
+ ); +} + +function SkeletonCard() { return ( -
- - - - - - -
-
+
+ ); +} + +// ─── Main Page ─────────────────────────────────────────────────────────────── + +export default function AdminAnalyticsPage() { + const [from, setFrom] = useState(""); + const [to, setTo] = useState(""); + const [appliedFrom, setAppliedFrom] = useState(); + const [appliedTo, setAppliedTo] = useState(); + + const { + data: analytics, + isLoading, + refetch, + } = useGetAdminAnalytics({ + from: appliedFrom, + to: appliedTo, + }); + + function handleApply() { + setAppliedFrom(from || undefined); + setAppliedTo(to || undefined); + } + + function handleClear() { + setFrom(""); + setTo(""); + setAppliedFrom(undefined); + setAppliedTo(undefined); + } + + const totalBookings = analytics + ? Object.values(analytics.bookings.byStatus).reduce((a, b) => a + b, 0) + : 0; + + return ( + +
+ {/* Header */} +
+

Analytics

+ + {/* Date filter */} +
+ setFrom(e.target.value)} + className="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" + placeholder="From" + /> + setTo(e.target.value)} + className="rounded-lg border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400" + placeholder="To" + /> + + + +
+
+ + {/* Loading skeleton */} + {isLoading && ( +
+ {Array.from({ length: 12 }).map((_, i) => ( + + ))} +
+ )} + + {/* Empty state */} + {!isLoading && !analytics && ( +
+

No analytics data available.

+
+ )} + + {/* Data */} + {!isLoading && analytics && ( + <> + {/* Revenue */} +
+ Revenue +
+ + + + +
+
+ + {/* Bookings */} +
+ Bookings +
+ + + + +
+
+ + {/* Occupancy */} +
+ Occupancy +
+ + + +
+ {/* Progress bar */} +
+
+
+
+ + {/* Revenue trend chart */} + {analytics.revenue.trend.length > 0 && ( + + )} + + {/* Top workspaces + top members */} +
+ {analytics.topWorkspaces.length > 0 && ( +
+ Top Workspaces +
    + {analytics.topWorkspaces.map((ws, idx) => ( +
  1. +
    + + {idx + 1} + + + {ws.name} + +
    +
    +
    {Number(ws.bookingCount)} bookings
    +
    + {formatNaira(Number(ws.totalRevenue))} +
    +
    +
  2. + ))} +
+
+ )} + + {analytics.topMembers.length > 0 && ( +
+ Top Members +
    + {analytics.topMembers.map((member, idx) => ( +
  1. +
    + + {idx + 1} + + + {member.fullName} + +
    + + {formatNaira(Number(member.totalSpend))} + +
  2. + ))} +
+
+ )} +
+ + )} +
+
); } diff --git a/frontend/lib/hooks/admin/analytics/UseGetMemberDashboard.ts b/frontend/lib/hooks/admin/analytics/UseGetMemberDashboard.ts new file mode 100644 index 00000000..998f2abf --- /dev/null +++ b/frontend/lib/hooks/admin/analytics/UseGetMemberDashboard.ts @@ -0,0 +1,53 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { queryKeys } from "@/lib/react-query/keys/queryKeys"; +import { apiClient } from "@/lib/apiClient"; + +export interface MemberStats { + activeBookings: number; + totalSpentKobo: number; + invoiceCount: number; + lastCheckIn: string | null; +} + +export interface MemberBooking { + id: string; + status: string; + createdAt: string; + workspace: { + id: string; + name: string; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export interface MemberPayment { + id: string; + amountKobo: number; + status: string; + createdAt: string; + [key: string]: unknown; +} + +export interface MemberDashboard { + stats: MemberStats; + recentBookings: MemberBooking[]; + recentPayments: MemberPayment[]; +} + +async function fetchMemberDashboard(): Promise { + const { data } = await apiClient.get<{ + success: boolean; + data: MemberDashboard; + }>("/dashboard/member"); + return data.data; +} + +export function useGetMemberDashboard() { + return useQuery({ + queryKey: queryKeys.memberDashboard(), + queryFn: fetchMemberDashboard, + }); +} diff --git a/frontend/lib/hooks/admin/analytics/useGetAdminAnalytics.ts b/frontend/lib/hooks/admin/analytics/useGetAdminAnalytics.ts new file mode 100644 index 00000000..f4d2711a --- /dev/null +++ b/frontend/lib/hooks/admin/analytics/useGetAdminAnalytics.ts @@ -0,0 +1,77 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { queryKeys } from "@/lib/react-query/keys/queryKeys"; +import { apiClient } from "@/lib/apiClient"; + +export interface RevenueMonth { + month: string; + revenueKobo: number; +} + +export interface TopWorkspace { + id: string; + name: string; + bookingCount: string; + totalRevenue: string; +} + +export interface TopMember { + id: string; + fullName: string; + totalSpend: string; +} + +export interface AdminAnalytics { + revenue: { + totalKobo: number; + thisMonthKobo: number; + lastMonthKobo: number; + trend: RevenueMonth[]; + }; + bookings: { + total: number; + byStatus: Record; + }; + topWorkspaces: TopWorkspace[]; + topMembers: TopMember[]; + invoices: { + paid: number; + total: number; + }; + occupancy: { + rate: number; + occupiedSeats: number; + totalSeats: number; + activeWorkspaces: number; + }; +} + +interface UseGetAdminAnalyticsParams { + from?: string; + to?: string; +} + +async function fetchAdminAnalytics( + params: UseGetAdminAnalyticsParams, +): Promise { + const searchParams = new URLSearchParams(); + if (params.from) searchParams.set("from", params.from); + if (params.to) searchParams.set("to", params.to); + + const query = searchParams.toString(); + const url = `/dashboard/admin/analytics${query ? `?${query}` : ""}`; + + const { data } = await apiClient.get<{ + success: boolean; + data: AdminAnalytics; + }>(url); + return data.data; +} + +export function useGetAdminAnalytics(params: UseGetAdminAnalyticsParams = {}) { + return useQuery({ + queryKey: queryKeys.adminAnalytics(params.from, params.to), + queryFn: () => fetchAdminAnalytics(params), + }); +} diff --git a/frontend/lib/react-query/keys/queryKeys.ts b/frontend/lib/react-query/keys/queryKeys.ts new file mode 100644 index 00000000..c94fb21b --- /dev/null +++ b/frontend/lib/react-query/keys/queryKeys.ts @@ -0,0 +1,8 @@ +export const queryKeys = { + // ... existing keys ... + + adminAnalytics: (from?: string, to?: string) => + ["adminAnalytics", { from, to }] as const, + + memberDashboard: () => ["memberDashboard"] as const, +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1214fc77..5b7926fc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,8 +12,9 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.95.2", "@tanstack/react-query-devtools": "^5.91.2", + "axios": "^1.14.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "js-cookie": "^3.0.5", @@ -1464,9 +1465,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.90.20", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", - "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz", + "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==", "license": "MIT", "funding": { "type": "github", @@ -1484,13 +1485,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.21", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", - "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", + "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", "license": "MIT", - "peer": true, "dependencies": { - "@tanstack/query-core": "5.90.20" + "@tanstack/query-core": "5.95.2" }, "funding": { "type": "github", @@ -1635,7 +1635,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1646,7 +1645,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -1702,7 +1700,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -2215,7 +2212,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2460,6 +2456,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2486,6 +2488,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2550,7 +2563,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2671,6 +2683,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2697,8 +2721,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", @@ -2949,6 +2972,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2976,7 +3008,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3081,7 +3112,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3091,7 +3121,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3129,7 +3158,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3142,7 +3170,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3214,7 +3241,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3771,6 +3797,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3787,11 +3833,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3842,7 +3903,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3867,7 +3927,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3964,7 +4023,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4036,7 +4094,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4049,7 +4106,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4065,7 +4121,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5032,7 +5087,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5062,6 +5116,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5570,6 +5645,15 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5606,7 +5690,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5616,7 +5699,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -5629,7 +5711,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5670,7 +5751,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -5723,8 +5803,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -6431,7 +6510,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6591,7 +6669,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/frontend/package.json b/frontend/package.json index 83ac60c2..d50fd711 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,8 +13,9 @@ "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", - "@tanstack/react-query": "^5.90.19", + "@tanstack/react-query": "^5.95.2", "@tanstack/react-query-devtools": "^5.91.2", + "axios": "^1.14.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "js-cookie": "^3.0.5", From b360ede38c2a4e83cf2955311b2afbeddb155d7a Mon Sep 17 00:00:00 2001 From: OlufunbiIK Date: Sun, 29 Mar 2026 17:04:48 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat:Issue=20#75=20=E2=80=94=20Backend:=20D?= =?UTF-8?q?ashboard=20Controller=20=E2=80=94=20Member=20Dashboard=20Endpoi?= =?UTF-8?q?nts=20#651?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package-lock.json | 41 ++------ backend/src/dashboard/dashboard.controller.ts | 72 +++++++++++++- backend/src/dashboard/dashboard.service.ts | 96 +++++++++++++++++-- .../providers/member-dashboard.provide.ts | 68 +++++++++++++ contracts/Cargo.lock | 14 +++ 5 files changed, 251 insertions(+), 40 deletions(-) create mode 100644 backend/src/dashboard/providers/member-dashboard.provide.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index efb4abf6..cfbc4a9d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -3761,7 +3761,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.22.tgz", "integrity": "sha512-fxJ4v85nDHaqT1PmfNCQ37b/jcv2OojtXTaK1P2uAXhzLf9qq6WNUOFvxBrV4fhQek1EQoT1o9oj5xAZmv3NRw==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "20.4.1", "iterare": "1.2.1", @@ -3808,7 +3807,6 @@ "integrity": "sha512-6IX9+VwjiKtCjx+mXVPncpkQ5ZjKfmssOZPFexmT+6T9H9wZ3svpYACAo7+9e7Nr9DZSoRZw3pffkJP7Z0UjaA==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -3889,7 +3887,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.22.tgz", "integrity": "sha512-ySSq7Py/DFozzZdNDH67m/vHoeVdphDniWBnl6q5QVoXldDdrZIHLXLRMPayTDh5A95nt7jjJzmD4qpTbNQ6tA==", "license": "MIT", - "peer": true, "dependencies": { "body-parser": "1.20.4", "cors": "2.8.5", @@ -4293,6 +4290,7 @@ "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -4333,6 +4331,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -4347,6 +4346,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -5528,7 +5528,6 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5555,7 +5554,6 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -5693,7 +5691,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.35.tgz", "integrity": "sha512-Uarfe6J91b9HAUXxjvSOdiO2UPOKLm07Q1oh0JHxoZ1y8HoqxDAu3gVrsrOHeiio0kSsoVBt4wFrKOm0dKxVPQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5883,7 +5880,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -6280,7 +6276,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6328,7 +6323,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6668,7 +6662,6 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -7022,7 +7015,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7127,7 +7119,6 @@ "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "^4.9.0", "get-port": "^5.1.1", @@ -7166,7 +7157,6 @@ "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz", "integrity": "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==", "license": "MIT", - "peer": true, "dependencies": { "@cacheable/utils": "^2.3.3", "keyv": "^5.5.5" @@ -7402,7 +7392,6 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -7458,15 +7447,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.4", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -8588,7 +8575,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8645,7 +8631,6 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10472,7 +10457,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11538,7 +11522,6 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", - "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -13000,7 +12983,6 @@ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", "license": "MIT-0", - "peer": true, "engines": { "node": ">=6.0.0" } @@ -13324,7 +13306,6 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -13465,7 +13446,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.19.0.tgz", "integrity": "sha512-QIcLGi508BAHkQ3pJNptsFz5WQMlpGbuBGBaIaXsWK8mel2kQ/rThYI+DbgjUvZrIr7MiuEuc9LcChJoEZK1xQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.12.0", @@ -13725,7 +13705,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -14287,7 +14266,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -14359,7 +14337,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -15496,7 +15473,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15674,7 +15650,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.28.tgz", "integrity": "sha512-6GH7wXhtfq2D33ZuRXYwIsl/qM5685WZcODZb7noOOcRMteM9KF2x2ap3H0EBjnSV0VO4gNAfJT5Ukp0PkOlvg==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -15899,7 +15874,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16390,6 +16364,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -16407,6 +16382,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -16420,6 +16396,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -16429,6 +16406,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -16438,6 +16416,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -16450,6 +16429,7 @@ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -16621,7 +16601,6 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", - "peer": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", diff --git a/backend/src/dashboard/dashboard.controller.ts b/backend/src/dashboard/dashboard.controller.ts index 1197d063..f2221022 100644 --- a/backend/src/dashboard/dashboard.controller.ts +++ b/backend/src/dashboard/dashboard.controller.ts @@ -14,12 +14,17 @@ import { UserRole } from '../users/enums/userRoles.enum'; import { CurrentUser } from '../auth/decorators/current.user.decorators'; import { User } from '../users/entities/user.entity'; import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; +import { GetCurrentUser } from 'src/auth/decorators/getCurrentUser.decorator'; +import { MemberDashboardProvider } from './providers/member-dashboard.provide'; @ApiTags('dashboard') @ApiBearerAuth() @Controller('dashboard') export class DashboardController { - constructor(private readonly dashboardService: DashboardService) {} + constructor( + private readonly dashboardService: DashboardService, + private readonly memberDashboardProvider: MemberDashboardProvider, + ) {} @Get('stats') @HttpCode(HttpStatus.OK) @@ -60,4 +65,69 @@ export class DashboardController { ); return { success: true, ...data }; } + + // ────────────────────────────────────────────── + // Member endpoints + // ────────────────────────────────────────────── + + @Get('member') + @HttpCode(HttpStatus.OK) + async getMemberDashboard(@GetCurrentUser('id') userId: string) { + const data = await this.memberDashboardProvider.getMemberDashboard(userId); + return { success: true, data }; + } + + @Get('member/bookings') + @HttpCode(HttpStatus.OK) + async getMemberBookings( + @GetCurrentUser('id') userId: string, + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ) { + const parsedPage = Math.max(1, parseInt(page, 10) || 1); + const parsedLimit = Math.min(50, Math.max(1, parseInt(limit, 10) || 10)); + + const data = await this.dashboardService.getMemberBookings( + userId, + parsedPage, + parsedLimit, + ); + return { success: true, ...data }; + } + + @Get('member/payments') + @HttpCode(HttpStatus.OK) + async getMemberPayments( + @GetCurrentUser('id') userId: string, + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ) { + const parsedPage = Math.max(1, parseInt(page, 10) || 1); + const parsedLimit = Math.min(50, Math.max(1, parseInt(limit, 10) || 10)); + + const data = await this.dashboardService.getMemberPayments( + userId, + parsedPage, + parsedLimit, + ); + return { success: true, ...data }; + } + + @Get('member/invoices') + @HttpCode(HttpStatus.OK) + async getMemberInvoices( + @GetCurrentUser('id') userId: string, + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ) { + const parsedPage = Math.max(1, parseInt(page, 10) || 1); + const parsedLimit = Math.min(50, Math.max(1, parseInt(limit, 10) || 10)); + + const data = await this.dashboardService.getMemberInvoices( + userId, + parsedPage, + parsedLimit, + ); + return { success: true, ...data }; + } } diff --git a/backend/src/dashboard/dashboard.service.ts b/backend/src/dashboard/dashboard.service.ts index be17e886..a71163ca 100644 --- a/backend/src/dashboard/dashboard.service.ts +++ b/backend/src/dashboard/dashboard.service.ts @@ -3,6 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository, MoreThanOrEqual } from 'typeorm'; import { User } from '../users/entities/user.entity'; import { NewsletterSubscriber } from '../newsletter/entities/newsletter.entity'; +import { Booking } from '../bookings/entities/booking.entity'; +import { Payment } from '../payments/entities/payment.entity'; +import { Invoice } from '../invoices/entities/invoice.entity'; +import { PaymentStatus } from '../payments/enums/paymentStatus.enum'; @Injectable() export class DashboardService { @@ -11,6 +15,12 @@ export class DashboardService { private readonly userRepository: Repository, @InjectRepository(NewsletterSubscriber) private readonly newsletterRepository: Repository, + @InjectRepository(Booking) + private readonly bookingRepository: Repository, + @InjectRepository(Payment) + private readonly paymentRepository: Repository, + @InjectRepository(Invoice) + private readonly invoiceRepository: Repository, ) {} /** @@ -28,8 +38,11 @@ export class DashboardService { return { totalMembers, verifiedMembers, - activeWorkspaces: 1, // placeholder until workspaces entity exists - deskOccupancy: Math.min(Math.round((verifiedMembers / Math.max(totalMembers, 1)) * 100), 100), + activeWorkspaces: 1, + deskOccupancy: Math.min( + Math.round((verifiedMembers / Math.max(totalMembers, 1)) * 100), + 100, + ), }; } @@ -40,7 +53,14 @@ export class DashboardService { const recentUsers = await this.userRepository.find({ order: { createdAt: 'DESC' }, take: 10, - select: ['id', 'firstname', 'lastname', 'email', 'createdAt', 'isVerified'], + select: [ + 'id', + 'firstname', + 'lastname', + 'email', + 'createdAt', + 'isVerified', + ], }); return recentUsers.map((u) => ({ @@ -71,8 +91,12 @@ export class DashboardService { newSubscribersThisMonth, ] = await Promise.all([ this.userRepository.count({ where: { isDeleted: false } }), - this.userRepository.count({ where: { isActive: true, isDeleted: false } }), - this.userRepository.count({ where: { isSuspended: true, isDeleted: false } }), + this.userRepository.count({ + where: { isActive: true, isDeleted: false }, + }), + this.userRepository.count({ + where: { isSuspended: true, isDeleted: false }, + }), this.userRepository.count({ where: { createdAt: MoreThanOrEqual(thirtyDaysAgo), isDeleted: false }, }), @@ -84,7 +108,6 @@ export class DashboardService { }), ]); - // Registration trend — last 6 months const registrationTrend = await this.getMonthlyRegistrations(6); return { @@ -151,13 +174,71 @@ export class DashboardService { }; } + async getMemberBookings(userId: string, page: number, limit: number) { + const [data, total] = await this.bookingRepository.findAndCount({ + where: { userId }, + relations: ['workspace'], + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + data, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + + async getMemberPayments(userId: string, page: number, limit: number) { + const [data, total] = await this.paymentRepository.findAndCount({ + where: { userId, status: PaymentStatus.SUCCESS }, + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + data, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + + async getMemberInvoices(userId: string, page: number, limit: number) { + const [data, total] = await this.invoiceRepository.findAndCount({ + where: { userId }, + relations: ['booking', 'booking.workspace'], + order: { createdAt: 'DESC' }, + skip: (page - 1) * limit, + take: limit, + }); + + return { + data, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, + }; + } + private async getMonthlyRegistrations(months: number) { const result: { month: string; count: number }[] = []; const now = new Date(); for (let i = months - 1; i >= 0; i--) { const start = new Date(now.getFullYear(), now.getMonth() - i, 1); - const end = new Date(now.getFullYear(), now.getMonth() - i + 1, 0, 23, 59, 59); const count = await this.userRepository.count({ where: { @@ -166,7 +247,6 @@ export class DashboardService { }, }); - // We need a between query, but MoreThanOrEqual + manual filter works for trend const monthLabel = start.toLocaleString('en', { month: 'short' }); result.push({ month: monthLabel, count }); } diff --git a/backend/src/dashboard/providers/member-dashboard.provide.ts b/backend/src/dashboard/providers/member-dashboard.provide.ts new file mode 100644 index 00000000..bc89936c --- /dev/null +++ b/backend/src/dashboard/providers/member-dashboard.provide.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, In } from 'typeorm'; + +@Injectable() +export class MemberDashboardProvider { + constructor( + @InjectRepository(Booking) + private readonly bookingRepository: Repository, + @InjectRepository(Payment) + private readonly paymentRepository: Repository, + @InjectRepository(Invoice) + private readonly invoiceRepository: Repository, + @InjectRepository(WorkspaceLog) + private readonly workspaceLogRepository: Repository, + ) {} + + async getMemberStats(userId: string) { + const [activeBookings, totalSpentResult, invoiceCount, lastLog] = + await Promise.all([ + this.bookingRepository.count({ + where: { + userId, + status: In([BookingStatus.PENDING, BookingStatus.CONFIRMED]), + }, + }), + this.paymentRepository + .createQueryBuilder('payment') + .select('SUM(payment.amountKobo)', 'total') + .where('payment.userId = :userId', { userId }) + .andWhere('payment.status = :status', { + status: PaymentStatus.SUCCESS, + }) + .getRawOne<{ total: string | null }>(), + this.invoiceRepository.count({ where: { userId } }), + this.workspaceLogRepository.findOne({ + where: { userId }, + order: { checkedInAt: 'DESC' }, + }), + ]); + + return { + activeBookings, + totalSpentKobo: parseInt(totalSpentResult?.total ?? '0', 10) || 0, + invoiceCount, + lastCheckIn: lastLog?.checkedInAt ?? null, + }; + } + + async getMemberDashboard(userId: string) { + const [stats, recentBookings, recentPayments] = await Promise.all([ + this.getMemberStats(userId), + this.bookingRepository.find({ + where: { userId }, + relations: ['workspace'], + order: { createdAt: 'DESC' }, + take: 5, + }), + this.paymentRepository.find({ + where: { userId, status: PaymentStatus.SUCCESS }, + order: { createdAt: 'DESC' }, + take: 5, + }), + ]); + + return { stats, recentBookings, recentPayments }; + } +} \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 33b0463c..4ac077c9 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -927,6 +927,13 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "payment_escrow" +version = "0.0.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1760,6 +1767,13 @@ dependencies = [ "windows-link", ] +[[package]] +name = "workspace_booking" +version = "0.0.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "zerocopy" version = "0.8.27"