feat/build-branch #4
@ -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);
|
||||
|
||||
33
app/api/dashboard/route.ts
Normal file
33
app/api/dashboard/route.ts
Normal file
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -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 <DashboardHomePage />;
|
||||
};
|
||||
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 (
|
||||
<DashboardHomePage
|
||||
initialHealthData={initialHealthData}
|
||||
initialStats={initialStats}
|
||||
initialDateRange={[
|
||||
{
|
||||
startDate: new Date(defaultDates.dateStart),
|
||||
endDate: new Date(defaultDates.dateEnd),
|
||||
key: "selection",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,8 +8,13 @@ import "react-date-range/dist/theme/default.css";
|
||||
|
||||
import "./DateRangePicker.scss";
|
||||
|
||||
export const DateRangePicker = () => {
|
||||
const [range, setRange] = useState<Range[]>([
|
||||
interface IDateRangePickerProps {
|
||||
value?: Range[];
|
||||
onChange?: (ranges: Range[]) => void;
|
||||
}
|
||||
|
||||
export const DateRangePicker = ({ value, onChange }: IDateRangePickerProps) => {
|
||||
const [internalRange, setInternalRange] = useState<Range[]>([
|
||||
{
|
||||
startDate: new Date(),
|
||||
endDate: new Date(),
|
||||
@ -19,9 +24,16 @@ export const DateRangePicker = () => {
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(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);
|
||||
}
|
||||
|
||||
@ -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<Range[]>(
|
||||
initialDateRange ?? DEFAULT_DATE_RANGE
|
||||
);
|
||||
const [healthData, setHealthData] = useState<IHealthData | null>(
|
||||
initialHealthData
|
||||
);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<Card className="general-health-card">
|
||||
<CardContent>
|
||||
@ -22,21 +123,34 @@ export const GeneralHealthCard = () => {
|
||||
<Typography variant="h5" fontWeight="bold">
|
||||
General Health
|
||||
</Typography>
|
||||
|
||||
<Box className="general-health-card__right-side">
|
||||
<CalendarTodayIcon fontSize="small" />
|
||||
<Typography variant="body2">
|
||||
<DateRangePicker />
|
||||
<DateRangePicker
|
||||
value={dateRange}
|
||||
onChange={handleDateRangeChange}
|
||||
/>
|
||||
</Typography>
|
||||
<IconButton size="small">
|
||||
<MoreVertIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box className="general-health-card__stat-items">
|
||||
{stats.map((item, i) => (
|
||||
<StatItem key={item.label + i} {...item} />
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!error && (
|
||||
<Box className="general-health-card__stat-items">
|
||||
{stats.map((item, i) => (
|
||||
<StatItem key={`${item.label}-${i}`} {...item} />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
9
app/features/GeneralHealthCard/constants.ts
Normal file
9
app/features/GeneralHealthCard/constants.ts
Normal file
@ -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[];
|
||||
0
app/features/GeneralHealthCard/interfaces.ts
Normal file
0
app/features/GeneralHealthCard/interfaces.ts
Normal file
71
app/features/GeneralHealthCard/utils.ts
Normal file
71
app/features/GeneralHealthCard/utils.ts
Normal file
@ -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%",
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -17,6 +17,8 @@ const Users: React.FC<UsersProps> = ({ users }) => {
|
||||
const [showAddUser, setShowAddUser] = useState(false);
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
// console.log("[Users] - users", users);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<UserTopBar
|
||||
|
||||
@ -7,15 +7,42 @@ 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";
|
||||
|
||||
export const DashboardHomePage = () => {
|
||||
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 */}
|
||||
<Box sx={{ p: 2 }}>
|
||||
<GeneralHealthCard />
|
||||
<GeneralHealthCard
|
||||
initialHealthData={initialHealthData}
|
||||
initialStats={initialStats}
|
||||
initialDateRange={initialDateRange}
|
||||
/>
|
||||
</Box>
|
||||
<TransactionsOverView />
|
||||
{/* <TransactionsOverView /> */}
|
||||
<FetchReport />
|
||||
<TransactionsWaitingApproval />
|
||||
<Documentation />
|
||||
|
||||
@ -41,6 +41,8 @@ export default function UserRoleCard({ user }: Props) {
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
const { username, first_name, last_name, email, groups } = user;
|
||||
const [newPassword, setNewPassword] = useState<string | null>(null);
|
||||
|
||||
console.log("[UserRoleCard] - user", user);
|
||||
const handleEditClick = () => {
|
||||
setIsEditing(!isEditing);
|
||||
};
|
||||
@ -137,7 +139,7 @@ export default function UserRoleCard({ user }: Props) {
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} mt={1} flexWrap="wrap">
|
||||
<Stack direction="row" spacing={1}>
|
||||
{groups.map(role => (
|
||||
{groups?.map(role => (
|
||||
<Chip key={role} label={role} size="small" />
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
@ -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 || "";
|
||||
|
||||
83
app/services/health.ts
Normal file
83
app/services/health.ts
Normal file
@ -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<IHealthData> {
|
||||
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;
|
||||
}
|
||||
@ -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<IHealthData> {
|
||||
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();
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user