diff --git a/backend/src/dashboard/dashboard.controller.ts b/backend/src/dashboard/dashboard.controller.ts index d83c2a2..68de24f 100644 --- a/backend/src/dashboard/dashboard.controller.ts +++ b/backend/src/dashboard/dashboard.controller.ts @@ -16,6 +16,8 @@ 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() @@ -23,6 +25,7 @@ import { ApiTags, ApiBearerAuth } from '@nestjs/swagger'; export class DashboardController { constructor( private readonly dashboardService: DashboardService, + private readonly memberDashboardProvider: MemberDashboardProvider, private readonly adminAnalyticsProvider: AdminAnalyticsProvider, ) {} @@ -66,15 +69,68 @@ export class DashboardController { return { success: true, ...data }; } - @Get('admin/analytics') + // ────────────────────────────────────────────── + // Member endpoints + // ────────────────────────────────────────────── + + @Get('member') @HttpCode(HttpStatus.OK) - @Roles(UserRole.ADMIN, UserRole.SUPER_ADMIN, UserRole.STAFF) - @UseGuards(JwtAuthGuard, RolesGuard) - async getAdminAnalytics(@Query() query: AnalyticsQueryDto) { - const data = await this.adminAnalyticsProvider.getFullAdminDashboard( - query.from, - query.to, - ); + 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 be17e88..a71163c 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 0000000..bc89936 --- /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/frontend/app/page.tsx b/frontend/app/page.tsx index 7dd0a13..dce2a47 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 0000000..998f2ab --- /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 0000000..f4d2711 --- /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 index 44a9cdd..e17bec1 100644 --- a/frontend/lib/react-query/keys/queryKeys.ts +++ b/frontend/lib/react-query/keys/queryKeys.ts @@ -24,6 +24,11 @@ export const queryKeys = { detail: (id: string) => ["invoices", "detail", id] as const, }, + adminAnalytics: (from?: string, to?: string) => + ["adminAnalytics", { from, to }] as const, + + memberDashboard: () => ["memberDashboard"] as const, + // ── Notification keys ────────────────────────────────────────────────────── notifications: { /** Base key — used for invalidating all notification queries at once */ @@ -36,4 +41,4 @@ export const queryKeys = { list: (params: { page?: number; limit?: number }) => ["notifications", "list", params] as const, }, -}; \ No newline at end of file +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1214fc7..5b7926f 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 83ac60c..d50fd71 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",