diff --git a/app/api/dashboard/admin/users/route.ts b/app/api/dashboard/admin/users/route.ts
index ad02f85..ee5764a 100644
--- a/app/api/dashboard/admin/users/route.ts
+++ b/app/api/dashboard/admin/users/route.ts
@@ -28,8 +28,6 @@ export async function GET(request: Request) {
const data = await response.json();
- console.log("[Users] - data FROM ROUTE", data);
-
return NextResponse.json(data, { status: response.status });
} catch (err: unknown) {
console.error("Proxy GET /api/v1/users/list error:", err);
diff --git a/app/api/dashboard/route.ts b/app/api/dashboard/route.ts
new file mode 100644
index 0000000..fb64c48
--- /dev/null
+++ b/app/api/dashboard/route.ts
@@ -0,0 +1,33 @@
+import { NextRequest, NextResponse } from "next/server";
+import { fetchHealthDataService } from "@/app/services/health";
+import { transformHealthDataToStats } from "@/app/features/GeneralHealthCard/utils";
+
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const dateStart = searchParams.get("dateStart") ?? undefined;
+ const dateEnd = searchParams.get("dateEnd") ?? undefined;
+
+ const data = await fetchHealthDataService({ dateStart, dateEnd });
+
+ // Transform data to stats format using shared util
+ const stats = transformHealthDataToStats(data);
+
+ console.log("[stats]", stats);
+
+ return NextResponse.json(
+ {
+ ...data,
+ stats,
+ },
+ { status: 200 }
+ );
+ } catch (err: unknown) {
+ console.error("Proxy GET /api/v1/transactions/health error:", err);
+ const errorMessage = err instanceof Error ? err.message : "Unknown error";
+ return NextResponse.json(
+ { success: false, message: errorMessage },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/dashboard/transactions/route.ts b/app/api/dashboard/transactions/route.ts
index 19e55a1..5a74ffb 100644
--- a/app/api/dashboard/transactions/route.ts
+++ b/app/api/dashboard/transactions/route.ts
@@ -98,6 +98,7 @@ export async function POST(request: NextRequest) {
const queryString = queryParts.join("&");
const backendUrl = `${BE_BASE_URL}/api/v1/transactions${queryString ? `?${queryString}` : ""}`;
+ console.log("[Transactions] - backendUrl", backendUrl);
const response = await fetch(backendUrl, {
method: "GET",
headers: {
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 707967c..f614c64 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,9 +1,42 @@
-"use client";
-
import { DashboardHomePage } from "../features/Pages/DashboardHomePage/DashboardHomePage";
+import { transformHealthDataToStats } from "../features/GeneralHealthCard/utils";
+import { fetchHealthDataService } from "../services/health";
+import { getDefaultDateRange } from "../utils/formatDate";
-const DashboardPage = () => {
- return ;
-};
+export default async function DashboardPage() {
+ // Fetch initial health data server-side with default 24h range
+ const defaultDates = getDefaultDateRange();
+ let initialHealthData = null;
+ let initialStats = null;
-export default DashboardPage;
+ try {
+ const healthData = await fetchHealthDataService({
+ dateStart: defaultDates.dateStart,
+ dateEnd: defaultDates.dateEnd,
+ });
+ initialHealthData = healthData;
+ // console.log("[healthData]", healthData);
+ initialStats = healthData.stats ?? transformHealthDataToStats(healthData);
+ } catch (_error: unknown) {
+ // If fetch fails, component will handle it client-side
+ // console.error("Failed to fetch initial health data:", error);
+ const error = _error as Error;
+ console.error("Failed to fetch initial health data:", error.cause);
+ }
+
+ console.log("[initialStats]", initialStats);
+
+ return (
+
+ );
+}
diff --git a/app/features/DateRangePicker/DateRangePicker.tsx b/app/features/DateRangePicker/DateRangePicker.tsx
index 0dad3f4..5ef52d0 100644
--- a/app/features/DateRangePicker/DateRangePicker.tsx
+++ b/app/features/DateRangePicker/DateRangePicker.tsx
@@ -8,8 +8,13 @@ import "react-date-range/dist/theme/default.css";
import "./DateRangePicker.scss";
-export const DateRangePicker = () => {
- const [range, setRange] = useState([
+interface IDateRangePickerProps {
+ value?: Range[];
+ onChange?: (ranges: Range[]) => void;
+}
+
+export const DateRangePicker = ({ value, onChange }: IDateRangePickerProps) => {
+ const [internalRange, setInternalRange] = useState([
{
startDate: new Date(),
endDate: new Date(),
@@ -19,9 +24,16 @@ export const DateRangePicker = () => {
const [anchorEl, setAnchorEl] = useState(null);
+ const range = value ?? internalRange;
+
const handleSelect: DateRangeProps["onChange"] = ranges => {
if (ranges.selection) {
- setRange([ranges.selection]);
+ const newRange = [ranges.selection];
+ if (onChange) {
+ onChange(newRange);
+ } else {
+ setInternalRange(newRange);
+ }
if (ranges.selection.endDate !== ranges.selection.startDate) {
setAnchorEl(null);
}
diff --git a/app/features/GeneralHealthCard/GeneralHealthCard.tsx b/app/features/GeneralHealthCard/GeneralHealthCard.tsx
index 2a6a0f8..9f70368 100644
--- a/app/features/GeneralHealthCard/GeneralHealthCard.tsx
+++ b/app/features/GeneralHealthCard/GeneralHealthCard.tsx
@@ -1,20 +1,121 @@
-import { Box, Card, CardContent, Typography, IconButton } from "@mui/material";
+"use client";
+
+import { useEffect, useState } from "react";
+import {
+ Box,
+ Card,
+ CardContent,
+ Typography,
+ IconButton,
+ Alert,
+} from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
+import { Range } from "react-date-range";
import { DateRangePicker } from "../DateRangePicker/DateRangePicker";
import { StatItem } from "./components/StatItem";
+import { DEFAULT_DATE_RANGE } from "./constants";
+import { IHealthData } from "@/app/services/health";
+import { getHealthData } from "@/app/services/transactions";
+
import "./GeneralHealthCard.scss";
-const stats = [
- { label: "TOTAL", value: 5, change: "-84.85%" },
- { label: "SUCCESSFUL", value: 10, change: "100%" },
- { label: "ACCEPTANCE RATE", value: "0%", change: "-100%" },
- { label: "AMOUNT", value: "€0.00", change: "-100%" },
- { label: "ATV", value: "€0.00", change: "-100%" },
-];
+interface IGeneralHealthCardProps {
+ initialHealthData?: IHealthData | null;
+ initialStats?: Array<{
+ label: string;
+ value: string | number;
+ change: string;
+ }> | null;
+ initialDateRange?: Range[];
+}
+
+const DEBOUNCE_MS = 1000;
+
+export const GeneralHealthCard = ({
+ initialHealthData = null,
+ initialStats = null,
+ initialDateRange,
+}: IGeneralHealthCardProps) => {
+ const [dateRange, setDateRange] = useState(
+ initialDateRange ?? DEFAULT_DATE_RANGE
+ );
+ const [healthData, setHealthData] = useState(
+ initialHealthData
+ );
+ const [error, setError] = useState(null);
+
+ /**
+ * Fetch health data for a given range
+ */
+ const fetchHealthData = async (range: Range[]) => {
+ const startDate = range[0]?.startDate;
+ const endDate = range[0]?.endDate;
+
+ if (!startDate || !endDate) return;
+
+ setError(null);
+
+ try {
+ const data = await getHealthData({
+ dateStart: startDate.toISOString(),
+ dateEnd: endDate.toISOString(),
+ });
+
+ setHealthData(data);
+ } catch (err) {
+ const message =
+ err instanceof Error ? err.message : "Failed to fetch health data";
+ setError(message);
+ setHealthData(null);
+ }
+ };
+
+ /**
+ * Date picker change handler
+ * (state only — side effects are debounced below)
+ */
+ const handleDateRangeChange = (newRange: Range[]) => {
+ setDateRange(newRange);
+ };
+
+ /**
+ * Debounced fetch when date range changes
+ */
+ useEffect(() => {
+ const currentRange = dateRange[0];
+ if (!currentRange?.startDate || !currentRange?.endDate) return;
+
+ const isInitialDateRange =
+ initialDateRange &&
+ currentRange.startDate.getTime() ===
+ initialDateRange[0]?.startDate?.getTime() &&
+ currentRange.endDate.getTime() ===
+ initialDateRange[0]?.endDate?.getTime();
+
+ // Skip fetch if we're still on SSR-provided data
+ if (initialHealthData && isInitialDateRange) return;
+
+ const timeout = setTimeout(() => {
+ fetchHealthData(dateRange);
+ }, DEBOUNCE_MS);
+
+ return () => clearTimeout(timeout);
+ }, [dateRange]);
+
+ /**
+ * Resolve stats source
+ */
+ const stats = healthData?.stats ??
+ initialStats ?? [
+ { label: "TOTAL", value: 0, change: "0%" },
+ { label: "SUCCESSFUL", value: 0, change: "0%" },
+ { label: "ACCEPTANCE RATE", value: "0%", change: "0%" },
+ { label: "AMOUNT", value: "€0.00", change: "0%" },
+ { label: "ATV", value: "€0.00", change: "0%" },
+ ];
-export const GeneralHealthCard = () => {
return (
@@ -22,21 +123,34 @@ export const GeneralHealthCard = () => {
General Health
+
-
+
-
- {stats.map((item, i) => (
-
- ))}
-
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {!error && (
+
+ {stats.map((item, i) => (
+
+ ))}
+
+ )}
);
diff --git a/app/features/GeneralHealthCard/constants.ts b/app/features/GeneralHealthCard/constants.ts
new file mode 100644
index 0000000..a9e70e3
--- /dev/null
+++ b/app/features/GeneralHealthCard/constants.ts
@@ -0,0 +1,9 @@
+import { Range } from "react-date-range";
+
+export const DEFAULT_DATE_RANGE = [
+ {
+ startDate: new Date(),
+ endDate: new Date(),
+ key: "selection",
+ },
+] as Range[];
diff --git a/app/features/GeneralHealthCard/interfaces.ts b/app/features/GeneralHealthCard/interfaces.ts
new file mode 100644
index 0000000..e69de29
diff --git a/app/features/GeneralHealthCard/utils.ts b/app/features/GeneralHealthCard/utils.ts
new file mode 100644
index 0000000..bac015e
--- /dev/null
+++ b/app/features/GeneralHealthCard/utils.ts
@@ -0,0 +1,71 @@
+export const formatCurrency = (value: number | string | undefined): string => {
+ if (value === undefined || value === null) return "€0.00";
+ const numValue = typeof value === "string" ? parseFloat(value) : value;
+ if (isNaN(numValue)) return "€0.00";
+ return `€${numValue.toFixed(2)}`;
+};
+
+export const formatPercentage = (
+ value: number | string | undefined
+): string => {
+ if (value === undefined || value === null) return "0%";
+ const numValue = typeof value === "string" ? parseFloat(value) : value;
+ if (isNaN(numValue)) return "0%";
+ return `${numValue.toFixed(2)}%`;
+};
+
+interface IHealthData {
+ total?: number;
+ successful?: number;
+ acceptance_rate?: number;
+ amount?: number;
+ atv?: number;
+}
+
+interface IStatItem {
+ label: string;
+ value: string | number;
+ change: string;
+}
+
+export const transformHealthDataToStats = (
+ healthData: IHealthData | null
+): IStatItem[] => {
+ if (!healthData) {
+ return [
+ { label: "TOTAL", value: 0, change: "0%" },
+ { label: "SUCCESSFUL", value: 0, change: "0%" },
+ { label: "ACCEPTANCE RATE", value: "0%", change: "0%" },
+ { label: "AMOUNT", value: "€0.00", change: "0%" },
+ { label: "ATV", value: "€0.00", change: "0%" },
+ ];
+ }
+
+ return [
+ {
+ label: "TOTAL",
+ value: healthData.total ?? 0,
+ change: "0%",
+ },
+ {
+ label: "SUCCESSFUL",
+ value: healthData.successful ?? 0,
+ change: "0%",
+ },
+ {
+ label: "ACCEPTANCE RATE",
+ value: formatPercentage(healthData.acceptance_rate),
+ change: "0%",
+ },
+ {
+ label: "AMOUNT",
+ value: formatCurrency(healthData.amount),
+ change: "0%",
+ },
+ {
+ label: "ATV",
+ value: formatCurrency(healthData.atv),
+ change: "0%",
+ },
+ ];
+};
diff --git a/app/features/Pages/Admin/Users/users.tsx b/app/features/Pages/Admin/Users/users.tsx
index 396977f..0c6f3e2 100644
--- a/app/features/Pages/Admin/Users/users.tsx
+++ b/app/features/Pages/Admin/Users/users.tsx
@@ -17,6 +17,8 @@ const Users: React.FC = ({ users }) => {
const [showAddUser, setShowAddUser] = useState(false);
const dispatch = useDispatch();
+ // console.log("[Users] - users", users);
+
return (
{
+interface IDashboardHomePageProps {
+ initialHealthData?: {
+ success?: boolean;
+ message?: string;
+ total?: number;
+ successful?: number;
+ acceptance_rate?: number;
+ amount?: number;
+ atv?: number;
+ } | null;
+ initialStats?: Array<{
+ label: string;
+ value: string | number;
+ change: string;
+ }> | null;
+ initialDateRange?: Range[];
+}
+
+export const DashboardHomePage = ({
+ initialHealthData,
+ initialStats,
+ initialDateRange,
+}: IDashboardHomePageProps) => {
return (
<>
{/* Conditional rendering of the Generic Modal, passing LoginModal as children */}
-
+
-
+ {/* */}
diff --git a/app/features/UserRoles/userRoleCard.tsx b/app/features/UserRoles/userRoleCard.tsx
index b564ae0..1d6715d 100644
--- a/app/features/UserRoles/userRoleCard.tsx
+++ b/app/features/UserRoles/userRoleCard.tsx
@@ -41,6 +41,8 @@ export default function UserRoleCard({ user }: Props) {
const dispatch = useDispatch();
const { username, first_name, last_name, email, groups } = user;
const [newPassword, setNewPassword] = useState(null);
+
+ console.log("[UserRoleCard] - user", user);
const handleEditClick = () => {
setIsEditing(!isEditing);
};
@@ -137,7 +139,7 @@ export default function UserRoleCard({ user }: Props) {
- {groups.map(role => (
+ {groups?.map(role => (
))}
diff --git a/app/services/constants.ts b/app/services/constants.ts
index e6c03bb..90042c4 100644
--- a/app/services/constants.ts
+++ b/app/services/constants.ts
@@ -1,5 +1,6 @@
export const AUDIT_CACHE_TAG = "audits";
export const USERS_CACHE_TAG = "users";
+export const HEALTH_CACHE_TAG = "health";
export const REVALIDATE_SECONDS = 100;
export const BE_BASE_URL = process.env.BE_BASE_URL || "";
diff --git a/app/services/health.ts b/app/services/health.ts
new file mode 100644
index 0000000..45f8f02
--- /dev/null
+++ b/app/services/health.ts
@@ -0,0 +1,83 @@
+"use server";
+import { cookies } from "next/headers";
+
+import {
+ AUTH_COOKIE_NAME,
+ BE_BASE_URL,
+ REVALIDATE_SECONDS,
+ HEALTH_CACHE_TAG,
+} from "./constants";
+
+export interface IHealthData {
+ success?: boolean;
+ message?: string;
+ total?: number;
+ successful?: number;
+ acceptance_rate?: number;
+ amount?: number;
+ atv?: number;
+ stats?: Array<{
+ label: string;
+ value: string | number;
+ change: string;
+ }>;
+}
+
+export interface IFetchHealthDataParams {
+ dateStart?: string;
+ dateEnd?: string;
+}
+
+export async function fetchHealthDataService({
+ dateStart,
+ dateEnd,
+}: IFetchHealthDataParams = {}): Promise {
+ const cookieStore = await cookies();
+ const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;
+
+ if (!token) {
+ throw new Error("Missing auth token");
+ }
+
+ const queryParts: string[] = [];
+
+ // Add date filter if provided
+ if (dateStart && dateEnd) {
+ queryParts.push(
+ `Modified=BETWEEN/${encodeURIComponent(dateStart)}/${encodeURIComponent(dateEnd)}`
+ );
+ } else if (dateStart) {
+ queryParts.push(`Modified=>/${encodeURIComponent(dateStart)}`);
+ } else if (dateEnd) {
+ queryParts.push(`Modified=${encodeURIComponent(dateEnd)}`);
+ }
+
+ const queryString = queryParts.join("&");
+ const backendUrl = `${BE_BASE_URL}/api/v1/transactions/health${
+ queryString ? `?${queryString}` : ""
+ }`;
+
+ const response = await fetch(backendUrl, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ next: {
+ revalidate: REVALIDATE_SECONDS,
+ tags: [HEALTH_CACHE_TAG],
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = await response
+ .json()
+ .catch(() => ({ message: "Failed to fetch health data" }));
+ throw new Error(errorData?.message || "Failed to fetch health data");
+ }
+
+ const data = (await response.json()) as IHealthData;
+
+ console.log("[data]", data.stats);
+ return data;
+}
diff --git a/app/services/transactions.ts b/app/services/transactions.ts
index 9f91d1b..3747652 100644
--- a/app/services/transactions.ts
+++ b/app/services/transactions.ts
@@ -1,3 +1,6 @@
+import { getBaseUrl } from "./constants";
+import { IHealthData, IFetchHealthDataParams } from "./health";
+
export async function getTransactions({
transactionType,
query,
@@ -6,7 +9,7 @@ export async function getTransactions({
query: string;
}) {
const res = await fetch(
- `http://localhost:4000/api/dashboard/transactions/${transactionType}?${query}`,
+ `${getBaseUrl()}/api/dashboard/transactions/${transactionType}?${query}`,
{
cache: "no-store",
}
@@ -22,3 +25,32 @@ export async function getTransactions({
return res.json();
}
+
+/**
+ * Client-side function to fetch health data via the /api/dashboard proxy
+ */
+export async function getHealthData({
+ dateStart,
+ dateEnd,
+}: IFetchHealthDataParams = {}): Promise {
+ const params = new URLSearchParams();
+ if (dateStart) params.set("dateStart", dateStart);
+ if (dateEnd) params.set("dateEnd", dateEnd);
+
+ const queryString = params.toString();
+ const res = await fetch(
+ `${getBaseUrl()}/api/dashboard${queryString ? `?${queryString}` : ""}`,
+ {
+ cache: "no-store",
+ }
+ );
+
+ if (!res.ok) {
+ const errorData = await res
+ .json()
+ .catch(() => ({ message: "Unknown error" }));
+ throw new Error(errorData.message || `HTTP error! status: ${res.status}`);
+ }
+
+ return res.json();
+}
diff --git a/app/utils/formatDate.ts b/app/utils/formatDate.ts
index 6fb9ff9..bd1a494 100644
--- a/app/utils/formatDate.ts
+++ b/app/utils/formatDate.ts
@@ -11,3 +11,13 @@ export const formatToDateTimeString = (dateString: string): string => {
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
+
+export const getDefaultDateRange = () => {
+ const endDate = new Date();
+ const startDate = new Date();
+ startDate.setHours(startDate.getHours() - 24);
+ return {
+ dateStart: startDate.toISOString(),
+ dateEnd: endDate.toISOString(),
+ };
+};