diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..7a73a41
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,2 @@
+{
+}
\ No newline at end of file
diff --git a/app/api/dashboard/route.ts b/app/api/dashboard/route.ts
index fb64c48..116d251 100644
--- a/app/api/dashboard/route.ts
+++ b/app/api/dashboard/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
-import { fetchHealthDataService } from "@/app/services/health";
+import { fetchDashboardDataService } from "@/app/services/health";
import { transformHealthDataToStats } from "@/app/features/GeneralHealthCard/utils";
export async function GET(request: NextRequest) {
@@ -8,22 +8,24 @@ export async function GET(request: NextRequest) {
const dateStart = searchParams.get("dateStart") ?? undefined;
const dateEnd = searchParams.get("dateEnd") ?? undefined;
- const data = await fetchHealthDataService({ dateStart, dateEnd });
+ // Fetch all dashboard data (health, overview, and review transactions) concurrently
+ const dashboardData = await fetchDashboardDataService({
+ dateStart,
+ dateEnd,
+ });
- // Transform data to stats format using shared util
- const stats = transformHealthDataToStats(data);
+ // Transform health data to stats format using shared util
+ const stats = transformHealthDataToStats(dashboardData.healthData);
- console.log("[stats]", stats);
+ const response = {
+ ...dashboardData.healthData,
+ stats,
+ overviewData: dashboardData.overviewData,
+ reviewTransactions: dashboardData.reviewTransactions,
+ };
- return NextResponse.json(
- {
- ...data,
- stats,
- },
- { status: 200 }
- );
+ return NextResponse.json(response, { 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 },
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index f614c64..f3ec067 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,31 +1,33 @@
import { DashboardHomePage } from "../features/Pages/DashboardHomePage/DashboardHomePage";
import { transformHealthDataToStats } from "../features/GeneralHealthCard/utils";
-import { fetchHealthDataService } from "../services/health";
+import { fetchDashboardDataService } from "../services/health";
import { getDefaultDateRange } from "../utils/formatDate";
export default async function DashboardPage() {
- // Fetch initial health data server-side with default 24h range
+ // Fetch all dashboard data (health, overview, and review transactions) concurrently with default 24h range
const defaultDates = getDefaultDateRange();
let initialHealthData = null;
let initialStats = null;
+ let initialOverviewData = null;
+ let initialReviewTransactions = null;
try {
- const healthData = await fetchHealthDataService({
+ const dashboardData = await fetchDashboardDataService({
dateStart: defaultDates.dateStart,
dateEnd: defaultDates.dateEnd,
});
+
+ const { healthData, overviewData, reviewTransactions } = dashboardData;
initialHealthData = healthData;
- // console.log("[healthData]", healthData);
initialStats = healthData.stats ?? transformHealthDataToStats(healthData);
+ initialOverviewData = overviewData;
+ initialReviewTransactions = reviewTransactions;
} 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.error("Failed to fetch dashboard data:", error.cause);
}
- console.log("[initialStats]", initialStats);
-
return (
);
}
diff --git a/app/features/GeneralHealthCard/GeneralHealthCard.tsx b/app/features/GeneralHealthCard/GeneralHealthCard.tsx
index 9f70368..a317e23 100644
--- a/app/features/GeneralHealthCard/GeneralHealthCard.tsx
+++ b/app/features/GeneralHealthCard/GeneralHealthCard.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useEffect, useState } from "react";
+import { useState } from "react";
import {
Box,
Card,
@@ -8,6 +8,7 @@ import {
Typography,
IconButton,
Alert,
+ CircularProgress,
} from "@mui/material";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import CalendarTodayIcon from "@mui/icons-material/CalendarToday";
@@ -17,7 +18,9 @@ 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 { useDebouncedDateRange } from "@/app/hooks/useDebouncedDateRange";
+import { dashboardService } from "@/app/services/dashboardService";
+import { normalizeDateRangeForAPI } from "@/app/utils/formatDate";
import "./GeneralHealthCard.scss";
@@ -38,71 +41,56 @@ export const GeneralHealthCard = ({
initialStats = null,
initialDateRange,
}: IGeneralHealthCardProps) => {
- const [dateRange, setDateRange] = useState(
- initialDateRange ?? DEFAULT_DATE_RANGE
- );
const [healthData, setHealthData] = useState(
initialHealthData
);
const [error, setError] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
/**
* Fetch health data for a given range
*/
const fetchHealthData = async (range: Range[]) => {
+ if (!range || !range[0]) return;
+
const startDate = range[0]?.startDate;
const endDate = range[0]?.endDate;
if (!startDate || !endDate) return;
setError(null);
+ setIsLoading(true);
try {
- const data = await getHealthData({
- dateStart: startDate.toISOString(),
- dateEnd: endDate.toISOString(),
+ // Normalize dates to ensure full day coverage
+ const { dateStart, dateEnd } = normalizeDateRangeForAPI(
+ startDate,
+ endDate
+ );
+
+ // This will update the service and notify all subscribers
+ const { healthData } = await dashboardService.fetchDashboardData({
+ dateStart,
+ dateEnd,
});
- setHealthData(data);
+ setHealthData(healthData);
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to fetch health data";
setError(message);
setHealthData(null);
+ } finally {
+ setIsLoading(false);
}
};
- /**
- * 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]);
+ const { dateRange, handleDateRangeChange } = useDebouncedDateRange({
+ initialDateRange: initialDateRange ?? DEFAULT_DATE_RANGE,
+ debounceMs: DEBOUNCE_MS,
+ onDateRangeChange: fetchHealthData,
+ skipInitialFetch: !!initialHealthData,
+ });
/**
* Resolve stats source
@@ -144,12 +132,25 @@ export const GeneralHealthCard = ({
)}
- {!error && (
-
- {stats.map((item, i) => (
-
- ))}
+ {isLoading ? (
+
+
+ ) : (
+ !error && (
+
+ {stats.map((item, i) => (
+
+ ))}
+
+ )
)}
diff --git a/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx b/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx
index 4798b15..cc6c52c 100644
--- a/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx
+++ b/app/features/Pages/DashboardHomePage/DashboardHomePage.tsx
@@ -1,13 +1,15 @@
"use client";
+import { useEffect } from "react";
import { Box } from "@mui/material";
import { GeneralHealthCard } from "../../GeneralHealthCard/GeneralHealthCard";
import { TransactionsWaitingApproval } from "../../TransactionsWaitingApproval/TransactionsWaitingApproval";
-import { FetchReport } from "../../FetchReports/FetchReports";
-import { Documentation } from "../../Documentation/Documentation";
-import { AccountIQ } from "../../AccountIQ/AccountIQ";
-import { WhatsNew } from "../../WhatsNew/WhatsNew";
import { TransactionsOverView } from "../../TransactionsOverView/TransactionsOverview";
import { Range } from "react-date-range";
+import {
+ ITransactionsOverviewData,
+ IReviewTransactionsData,
+} from "@/app/services/health";
+import { dashboardService } from "@/app/services/dashboardService";
interface IDashboardHomePageProps {
initialHealthData?: {
@@ -25,13 +27,28 @@ interface IDashboardHomePageProps {
change: string;
}> | null;
initialDateRange?: Range[];
+ initialOverviewData?: ITransactionsOverviewData | null;
+ initialReviewTransactions?: IReviewTransactionsData | null;
}
export const DashboardHomePage = ({
initialHealthData,
initialStats,
initialDateRange,
+ initialOverviewData,
+ initialReviewTransactions,
}: IDashboardHomePageProps) => {
+ // Initialize service with server data if available
+ useEffect(() => {
+ if (initialHealthData && initialOverviewData && initialReviewTransactions) {
+ dashboardService.updateDashboardData({
+ healthData: initialHealthData,
+ overviewData: initialOverviewData,
+ reviewTransactions: initialReviewTransactions,
+ });
+ }
+ }, [initialHealthData, initialOverviewData, initialReviewTransactions]);
+
return (
<>
{/* Conditional rendering of the Generic Modal, passing LoginModal as children */}
@@ -42,12 +59,14 @@ export const DashboardHomePage = ({
initialDateRange={initialDateRange}
/>
- {/* */}
-
-
-
+
+ {/* */}
+
+ {/*
-
+ */}
>
);
};
diff --git a/app/features/PieCharts/PieCharts.tsx b/app/features/PieCharts/PieCharts.tsx
index 77a3ac8..9a2d766 100644
--- a/app/features/PieCharts/PieCharts.tsx
+++ b/app/features/PieCharts/PieCharts.tsx
@@ -2,15 +2,25 @@
import { Box } from "@mui/material";
import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts";
import "./PieCharts.scss";
-const data = [
+
+interface IPieChartData {
+ name: string;
+ value: number;
+}
+
+interface IPieChartsProps {
+ data?: IPieChartData[];
+}
+
+const COLORS = ["#4caf50", "#ff9800", "#f44336", "#9e9e9e"];
+
+const defaultData: IPieChartData[] = [
{ name: "Group A", value: 100 },
{ name: "Group B", value: 200 },
{ name: "Group C", value: 400 },
{ name: "Group D", value: 300 },
];
-const COLORS = ["#4caf50", "#ff9800", "#f44336", "#9e9e9e"];
-
const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({
cx,
@@ -37,7 +47,7 @@ const renderCustomizedLabel = ({
);
};
-export const PieCharts = () => {
+export const PieCharts = ({ data = defaultData }: IPieChartsProps) => {
return (
diff --git a/app/features/TransactionsOverview/TransactionsOverview.tsx b/app/features/TransactionsOverview/TransactionsOverview.tsx
index e5a8a21..67e0d61 100644
--- a/app/features/TransactionsOverview/TransactionsOverview.tsx
+++ b/app/features/TransactionsOverview/TransactionsOverview.tsx
@@ -1,26 +1,97 @@
+"use client";
+
+import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Box, Button, IconButton, Paper, Typography } from "@mui/material";
import { PieCharts } from "../PieCharts/PieCharts";
import MoreVertIcon from "@mui/icons-material/MoreVert";
import { TransactionsOverViewTable } from "./components/TransactionsOverViewTable";
+import { type ITransactionsOverviewData } from "@/app/services/types";
+import { dashboardService } from "@/app/services/dashboardService";
+import {
+ transformOverviewResponse,
+ calculatePercentages,
+ enrichOverviewData,
+} from "./utils";
import "./TransactionsOverView.scss";
-export const TransactionsOverView = () => {
+interface ITransactionsOverviewProps {
+ initialOverviewData?: ITransactionsOverviewData | null;
+}
+
+export const TransactionsOverView = ({
+ initialOverviewData = null,
+}: ITransactionsOverviewProps) => {
const router = useRouter();
+ const [overviewData, setOverviewData] =
+ useState(
+ initialOverviewData ||
+ dashboardService.getCurrentDashboardData()?.overviewData ||
+ null
+ );
+
+ /**
+ * Subscribe to dashboard data changes
+ */
+ useEffect(() => {
+ const subscription = dashboardService
+ .getDashboardData$()
+ .subscribe(data => {
+ if (data?.overviewData) {
+ setOverviewData(data.overviewData);
+ }
+ });
+
+ // Cleanup subscription on unmount
+ return () => subscription.unsubscribe();
+ }, []);
+
+ /**
+ * Transform overview data from flat object to array format
+ */
+ const transformedData = overviewData
+ ? transformOverviewResponse({
+ cancelled: overviewData.cancelled,
+ failed: overviewData.failed,
+ successful: overviewData.successful,
+ waiting: overviewData.waiting,
+ })
+ : [];
+
+ /**
+ * Calculate percentages and enrich with colors
+ */
+ const enrichedData =
+ transformedData.length > 0
+ ? enrichOverviewData(calculatePercentages(transformedData))
+ : [];
+
+ /**
+ * Transform overview data for PieCharts (expects { name, value }[])
+ */
+ const pieChartData = enrichedData.map(item => ({
+ name: item.state,
+ value: item.count,
+ }));
+
+ /**
+ * Transform overview data for table (expects { state, count, percentage, color }[])
+ */
+ const tableData = enrichedData;
+
return (
- {/* Title and All Transactions Button */}
- Transactions Overview (Last 24h)
+ Transactions Overview
@@ -30,11 +101,12 @@ export const TransactionsOverView = () => {
- {/* Chart and Table */}
-
-
-
-
+ {overviewData && (
+
+
+
+
+ )}
);
};
diff --git a/app/features/TransactionsOverview/components/TransactionsOverViewTable.tsx b/app/features/TransactionsOverview/components/TransactionsOverViewTable.tsx
index dd16547..d66cf8c 100644
--- a/app/features/TransactionsOverview/components/TransactionsOverViewTable.tsx
+++ b/app/features/TransactionsOverview/components/TransactionsOverViewTable.tsx
@@ -12,14 +12,27 @@ import {
import "./TransactionsOverViewTable.scss";
-const data1 = [
+interface ITableData {
+ state: string;
+ count: number;
+ percentage: string;
+ color?: string;
+}
+
+interface ITransactionsOverViewTableProps {
+ data?: ITableData[];
+}
+
+const defaultData: ITableData[] = [
{ state: "Success", count: 120, percentage: "60%", color: "green" },
{ state: "Pending", count: 50, percentage: "25%", color: "orange" },
{ state: "Failed", count: 20, percentage: "10%", color: "red" },
{ state: "Other", count: 10, percentage: "5%", color: "gray" },
];
-export const TransactionsOverViewTable = () => {
+export const TransactionsOverViewTable = ({
+ data = defaultData,
+}: ITransactionsOverViewTableProps) => {
return (
@@ -32,8 +45,8 @@ export const TransactionsOverViewTable = () => {
- {data1.map((row, i) => (
-
+ {data.map((row, i) => (
+
{
+ const normalizedState = state.toLowerCase();
+
+ switch (normalizedState) {
+ case "success":
+ case "completed":
+ case "successful":
+ return "#4caf50"; // green
+ case "pending":
+ case "waiting":
+ return "#ff9800"; // orange
+ case "failed":
+ case "error":
+ return "#f44336"; // red
+ case "cancelled":
+ case "canceled":
+ return "#9e9e9e"; // gray
+ default:
+ return "#9e9e9e"; // gray
+ }
+};
+
+/**
+ * Transform flat API overview response to array format
+ */
+export const transformOverviewResponse = (data: {
+ cancelled?: number;
+ failed?: number;
+ successful?: number;
+ waiting?: number;
+}): Array<{
+ state: string;
+ count: number;
+}> => {
+ const states = [
+ { key: "successful", label: "Successful" },
+ { key: "waiting", label: "Waiting" },
+ { key: "failed", label: "Failed" },
+ { key: "cancelled", label: "Cancelled" },
+ ];
+
+ return states
+ .map(({ key, label }) => ({
+ state: label,
+ count: data[key as keyof typeof data] || 0,
+ }))
+ .filter(item => item.count > 0); // Only include states with counts > 0
+};
+
+/**
+ * Calculate percentage for each state
+ */
+export const calculatePercentages = (
+ items: Array<{ state: string; count: number }>
+): Array<{
+ state: string;
+ count: number;
+ percentage: string;
+}> => {
+ const total = items.reduce((sum, item) => sum + item.count, 0);
+
+ if (total === 0) {
+ return items.map(item => ({
+ ...item,
+ percentage: "0%",
+ }));
+ }
+
+ return items.map(item => ({
+ ...item,
+ percentage: `${Math.round((item.count / total) * 100)}%`,
+ }));
+};
+
+/**
+ * Transform API overview data to include colors if missing
+ */
+export const enrichOverviewData = (
+ data: Array<{
+ state: string;
+ count: number;
+ percentage: string;
+ color?: string;
+ }>
+): Array<{
+ state: string;
+ count: number;
+ percentage: string;
+ color: string;
+}> => {
+ return data.map(item => ({
+ ...item,
+ color: item.color || getStateColor(item.state),
+ }));
+};
diff --git a/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx b/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx
index 34ddc01..1f1d092 100644
--- a/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx
+++ b/app/features/TransactionsWaitingApproval/TransactionsWaitingApproval.tsx
@@ -1,3 +1,6 @@
+"use client";
+
+import { useEffect, useState } from "react";
import {
Box,
Button,
@@ -13,110 +16,71 @@ import {
} from "@mui/material";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import CancelIcon from "@mui/icons-material/Cancel";
-
import MoreVertIcon from "@mui/icons-material/MoreVert";
-import "./TransactionsWaitingApproval.scss";
-const transactions = [
- {
- id: "1049078821",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078822",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078823",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078824",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078821",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078822",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078823",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078824",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078821",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078822",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078823",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
- {
- id: "1049078824",
- user: "17",
- created: "2025-06-17 16:45",
- type: "BestPayWithdrawal",
- amount: "-787.49 TRY",
- psp: "BestPay",
- },
-];
+import { dashboardService } from "@/app/services/dashboardService";
+import { formatToDateTimeString } from "@/app/utils/formatDate";
+
+import "./TransactionsWaitingApproval.scss";
+import {
+ type IReviewTransactionsData,
+ type IReviewTransaction,
+} from "@/app/services/types";
+
+interface ITransactionsWaitingApprovalProps {
+ initialReviewTransactions?: IReviewTransactionsData | null;
+}
+
+export const TransactionsWaitingApproval = ({
+ initialReviewTransactions = null,
+}: ITransactionsWaitingApprovalProps) => {
+ const [reviewTransactions, setReviewTransactions] =
+ useState(
+ initialReviewTransactions ||
+ dashboardService.getCurrentDashboardData()?.reviewTransactions ||
+ null
+ );
+
+ /**
+ * Subscribe to dashboard data changes
+ */
+ useEffect(() => {
+ const subscription = dashboardService
+ .getDashboardData$()
+ .subscribe(data => {
+ if (data?.reviewTransactions) {
+ setReviewTransactions(data.reviewTransactions);
+ }
+ });
+
+ // Cleanup subscription on unmount
+ return () => subscription.unsubscribe();
+ }, []);
+
+ /**
+ * Format transaction for display
+ */
+ const formatTransaction = (tx: IReviewTransaction) => {
+ const createdDate = tx.created || tx.modified || "";
+ const formattedDate = createdDate
+ ? formatToDateTimeString(createdDate)
+ : "";
+
+ return {
+ id: tx.id?.toString() || tx.external_id || "",
+ user: tx.customer || "",
+ created: formattedDate,
+ type: tx.type || "",
+ amount: tx.amount
+ ? `${tx.amount < 0 ? "-" : ""}${Math.abs(tx.amount).toFixed(2)} ${tx.currency || ""}`
+ : "",
+ psp: tx.psp_id || "",
+ };
+ };
+
+ const transactions = reviewTransactions?.transactions || [];
+ const displayTransactions = transactions.map(formatTransaction);
-export const TransactionsWaitingApproval = () => {
return (
@@ -128,65 +92,77 @@ export const TransactionsWaitingApproval = () => {
- {" "}
+
-
-
-
-
-
- ID
-
-
- User
-
-
- Created
-
-
- Type
-
-
- Amount
-
-
- PSP
-
-
- Action
-
-
-
-
- {transactions.map((tx, i) => (
-
- {tx.id}
- {tx.user}
- {tx.created}
- {tx.type}
- {tx.amount}
- {tx.psp}
+ {reviewTransactions && (
+
+
+
+
-
-
-
-
-
-
+ ID
+
+
+ User
+
+
+ Created
+
+
+ Type
+
+
+ Amount
+
+
+ PSP
+
+
+ Action
- ))}
-
-
-
+
+
+ {displayTransactions.length > 0 ? (
+ displayTransactions.map((tx, i) => (
+
+ {tx.id}
+ {tx.user}
+ {tx.created}
+ {tx.type}
+ {tx.amount}
+ {tx.psp}
+
+
+
+
+
+
+
+
+
+ ))
+ ) : (
+
+
+
+ No transactions waiting for approval
+
+
+
+ )}
+
+
+
+ )}
);
diff --git a/app/hooks/useDebouncedDateRange.ts b/app/hooks/useDebouncedDateRange.ts
new file mode 100644
index 0000000..53ecf4a
--- /dev/null
+++ b/app/hooks/useDebouncedDateRange.ts
@@ -0,0 +1,65 @@
+"use client";
+
+import { useState, useRef, useEffect, useCallback } from "react";
+import { Range } from "react-date-range";
+
+interface IUseDebouncedDateRangeOptions {
+ initialDateRange?: Range[];
+ debounceMs?: number;
+ onDateRangeChange?: (range: Range[]) => void | Promise;
+ skipInitialFetch?: boolean;
+}
+
+export const useDebouncedDateRange = ({
+ initialDateRange,
+ debounceMs = 1000,
+ onDateRangeChange,
+ skipInitialFetch = false,
+}: IUseDebouncedDateRangeOptions = {}) => {
+ const [dateRange, setDateRange] = useState(initialDateRange ?? []);
+ const debounceTimeoutRef = useRef(null);
+ const isFirstMount = useRef(true);
+
+ const handleDateRangeChange = useCallback(
+ (newRange: Range[]) => {
+ // Update state immediately for UI responsiveness
+ setDateRange(newRange);
+
+ // Clear any existing debounce timeout
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+
+ // Skip fetch on first mount if requested
+ if (isFirstMount.current) {
+ isFirstMount.current = false;
+ if (skipInitialFetch) {
+ return;
+ }
+ }
+
+ const currentRange = newRange[0];
+ if (!currentRange?.startDate || !currentRange?.endDate) return;
+
+ // Debounce the callback
+ debounceTimeoutRef.current = setTimeout(() => {
+ onDateRangeChange?.(newRange);
+ }, debounceMs);
+ },
+ [onDateRangeChange, debounceMs, skipInitialFetch]
+ );
+
+ // Cleanup timeout on unmount
+ useEffect(() => {
+ return () => {
+ if (debounceTimeoutRef.current) {
+ clearTimeout(debounceTimeoutRef.current);
+ }
+ };
+ }, []);
+
+ return {
+ dateRange,
+ handleDateRangeChange,
+ };
+};
diff --git a/app/services/dashboardService.ts b/app/services/dashboardService.ts
new file mode 100644
index 0000000..84906c2
--- /dev/null
+++ b/app/services/dashboardService.ts
@@ -0,0 +1,42 @@
+import { BehaviorSubject, Observable } from "rxjs";
+import { IDashboardData } from "./types";
+import { getDashboardData } from "./transactions";
+
+class DashboardService {
+ private dashboardData$ = new BehaviorSubject(null);
+
+ /**
+ * Get observable for dashboard data
+ */
+ getDashboardData$(): Observable {
+ return this.dashboardData$.asObservable();
+ }
+
+ /**
+ * Get current dashboard data
+ */
+ getCurrentDashboardData(): IDashboardData | null {
+ return this.dashboardData$.getValue();
+ }
+
+ /**
+ * Update dashboard data (called when fetching new data)
+ */
+ updateDashboardData(data: IDashboardData): void {
+ this.dashboardData$.next(data);
+ }
+
+ /**
+ * Fetch and update dashboard data
+ */
+ async fetchDashboardData(params: {
+ dateStart?: string;
+ dateEnd?: string;
+ }): Promise {
+ const data = await getDashboardData(params);
+ this.updateDashboardData(data);
+ return data;
+ }
+}
+
+export const dashboardService = new DashboardService();
diff --git a/app/services/health.ts b/app/services/health.ts
index 45f8f02..31ece41 100644
--- a/app/services/health.ts
+++ b/app/services/health.ts
@@ -7,31 +7,23 @@ import {
REVALIDATE_SECONDS,
HEALTH_CACHE_TAG,
} from "./constants";
+import {
+ type IDashboardData,
+ type IFetchHealthDataParams,
+ type IHealthData,
+ type IReviewTransactionsData,
+ type ITransactionsOverviewData,
+} from "./types";
-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({
+/**
+ * Fetch both health and overview data concurrently
+ * This is optimized for initial page load
+ * Always includes overview data with the provided date range
+ */
+export async function fetchDashboardDataService({
dateStart,
dateEnd,
-}: IFetchHealthDataParams = {}): Promise {
+}: IFetchHealthDataParams): Promise {
const cookieStore = await cookies();
const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;
@@ -53,11 +45,8 @@ export async function fetchHealthDataService({
}
const queryString = queryParts.join("&");
- const backendUrl = `${BE_BASE_URL}/api/v1/transactions/health${
- queryString ? `?${queryString}` : ""
- }`;
- const response = await fetch(backendUrl, {
+ const fetchConfig = {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -67,17 +56,78 @@ export async function fetchHealthDataService({
revalidate: REVALIDATE_SECONDS,
tags: [HEALTH_CACHE_TAG],
},
- });
+ };
- if (!response.ok) {
- const errorData = await response
+ // Fetch all three endpoints concurrently
+ const [healthResponse, overviewResponse, reviewResponse] = await Promise.all([
+ fetch(
+ `${BE_BASE_URL}/api/v1/transactions/health${
+ queryString ? `?${queryString}` : ""
+ }`,
+ fetchConfig
+ ),
+ fetch(
+ `${BE_BASE_URL}/api/v1/transactions/overview${
+ queryString ? `?${queryString}` : ""
+ }`,
+ fetchConfig
+ ),
+ fetch(
+ `${BE_BASE_URL}/api/v1/transactions?limit=1000${
+ queryString ? `&${queryString}` : ""
+ }&Status==/review`,
+ fetchConfig
+ ),
+ ]);
+
+ // Handle health data response
+ if (!healthResponse.ok) {
+ const errorData = await healthResponse
.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;
+ const healthData = (await healthResponse.json()) as IHealthData;
- console.log("[data]", data.stats);
- return data;
+ // Handle overview data response
+ let overviewData: ITransactionsOverviewData = {
+ success: false,
+ cancelled: 0,
+ failed: 0,
+ successful: 0,
+ waiting: 0,
+ };
+
+ if (!overviewResponse.ok) {
+ // Don't fail the whole request if overview fails, just log it
+ console.error("Failed to fetch transactions overview");
+ } else {
+ overviewData = (await overviewResponse.json()) as ITransactionsOverviewData;
+ }
+
+ // Handle review transactions response
+ let reviewTransactions: IReviewTransactionsData = {
+ success: false,
+ transactions: [],
+ total: 0,
+ };
+
+ if (!reviewResponse.ok) {
+ // Don't fail the whole request if review transactions fail, just log it
+ console.error("Failed to fetch review transactions");
+ } else {
+ const reviewData = (await reviewResponse.json()) as IReviewTransactionsData;
+ reviewTransactions = {
+ success: reviewData.success ?? true,
+ transactions: reviewData.transactions || [],
+ total: reviewData.total || 0,
+ };
+ }
+
+ return {
+ healthData,
+ overviewData,
+ reviewTransactions,
+ };
}
diff --git a/app/services/transactions.ts b/app/services/transactions.ts
index 3747652..f3d5d67 100644
--- a/app/services/transactions.ts
+++ b/app/services/transactions.ts
@@ -1,5 +1,5 @@
import { getBaseUrl } from "./constants";
-import { IHealthData, IFetchHealthDataParams } from "./health";
+import { IFetchHealthDataParams, IHealthData, IDashboardData } from "./types";
export async function getTransactions({
transactionType,
@@ -27,12 +27,13 @@ export async function getTransactions({
}
/**
- * Client-side function to fetch health data via the /api/dashboard proxy
+ * Client-side function to fetch dashboard data (health + overview) via the /api/dashboard proxy
+ * This function calls a single endpoint that returns both health and overview data
*/
-export async function getHealthData({
+export async function getDashboardData({
dateStart,
dateEnd,
-}: IFetchHealthDataParams = {}): Promise {
+}: IFetchHealthDataParams = {}): Promise {
const params = new URLSearchParams();
if (dateStart) params.set("dateStart", dateStart);
if (dateEnd) params.set("dateEnd", dateEnd);
@@ -52,5 +53,24 @@ export async function getHealthData({
throw new Error(errorData.message || `HTTP error! status: ${res.status}`);
}
- return res.json();
+ const data = await res.json();
+
+ // Extract overviewData and reviewTransactions from the response
+ const { overviewData, reviewTransactions, ...healthDataWithStats } = data;
+
+ return {
+ healthData: healthDataWithStats as IHealthData,
+ overviewData: overviewData || {
+ success: false,
+ cancelled: 0,
+ failed: 0,
+ successful: 0,
+ waiting: 0,
+ },
+ reviewTransactions: reviewTransactions || {
+ success: false,
+ transactions: [],
+ total: 0,
+ },
+ };
}
diff --git a/app/services/types.ts b/app/services/types.ts
new file mode 100644
index 0000000..82c6c11
--- /dev/null
+++ b/app/services/types.ts
@@ -0,0 +1,63 @@
+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 interface ITransactionsOverviewData {
+ success?: boolean;
+ message?: string;
+ cancelled?: number;
+ failed?: number;
+ successful?: number;
+ waiting?: number;
+ data?: Array<{
+ state: string;
+ count: number;
+ percentage: string;
+ color?: string;
+ }>;
+ total?: number;
+}
+
+export interface IDashboardData {
+ healthData: IHealthData;
+ overviewData: ITransactionsOverviewData;
+ reviewTransactions: IReviewTransactionsData;
+}
+
+export interface IReviewTransaction {
+ id: number;
+ customer?: string;
+ external_id?: string;
+ type?: string;
+ currency?: string;
+ amount?: number;
+ status?: string;
+ created?: string;
+ modified?: string;
+ merchant_id?: string;
+ psp_id?: string;
+ method_id?: string;
+}
+
+export interface IReviewTransactionsData {
+ success?: boolean;
+ message?: string;
+ transactions?: IReviewTransaction[];
+ total?: number;
+}
diff --git a/app/utils/formatDate.ts b/app/utils/formatDate.ts
index bd1a494..a399f4d 100644
--- a/app/utils/formatDate.ts
+++ b/app/utils/formatDate.ts
@@ -21,3 +21,29 @@ export const getDefaultDateRange = () => {
dateEnd: endDate.toISOString(),
};
};
+
+/**
+ * Normalize date range for API calls
+ * - Start date is set to beginning of day (00:00:00.000)
+ * - End date is set to end of day (23:59:59.999)
+ * This ensures same-day selections include the entire day
+ */
+export const normalizeDateRangeForAPI = (
+ startDate: Date,
+ endDate: Date
+): { dateStart: string; dateEnd: string } => {
+ // Clone dates to avoid mutating originals
+ const normalizedStart = new Date(startDate);
+ const normalizedEnd = new Date(endDate);
+
+ // Set start date to beginning of day
+ normalizedStart.setHours(0, 0, 0, 0);
+
+ // Set end date to end of day
+ normalizedEnd.setHours(23, 59, 59, 999);
+
+ return {
+ dateStart: normalizedStart.toISOString(),
+ dateEnd: normalizedEnd.toISOString(),
+ };
+};
diff --git a/services/roles.services.ts b/services/roles.services.ts
index 718180a..20b8d3d 100644
--- a/services/roles.services.ts
+++ b/services/roles.services.ts
@@ -8,7 +8,6 @@ export async function editUser(id: string, data: IEditUserForm) {
});
if (!res.ok) {
- console.log("[editUser] - FAILING", id, data);
throw new Error("Failed to update user");
}