Attempt to add saleschart projections
This commit is contained in:
@@ -165,11 +165,26 @@ const MiniSalesChart = ({ className = "" }) => {
|
|||||||
totalOrders: 0,
|
totalOrders: 0,
|
||||||
prevRevenue: 0,
|
prevRevenue: 0,
|
||||||
prevOrders: 0,
|
prevOrders: 0,
|
||||||
growth: {
|
periodProgress: 100
|
||||||
revenue: 0,
|
|
||||||
orders: 0
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
const [projection, setProjection] = useState(null);
|
||||||
|
const [projectionLoading, setProjectionLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchProjection = useCallback(async () => {
|
||||||
|
if (summaryStats.periodProgress >= 100) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setProjectionLoading(true);
|
||||||
|
const response = await axios.get("/api/klaviyo/events/projection", {
|
||||||
|
params: { timeRange: "last30days" }
|
||||||
|
});
|
||||||
|
setProjection(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading projection:", error);
|
||||||
|
} finally {
|
||||||
|
setProjectionLoading(false);
|
||||||
|
}
|
||||||
|
}, [summaryStats.periodProgress]);
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -200,33 +215,30 @@ const MiniSalesChart = ({ className = "" }) => {
|
|||||||
totalOrders: acc.totalOrders + (Number(day.orders) || 0),
|
totalOrders: acc.totalOrders + (Number(day.orders) || 0),
|
||||||
prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0),
|
prevRevenue: acc.prevRevenue + (Number(day.prevRevenue) || 0),
|
||||||
prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0),
|
prevOrders: acc.prevOrders + (Number(day.prevOrders) || 0),
|
||||||
|
periodProgress: day.periodProgress || 100,
|
||||||
}), {
|
}), {
|
||||||
totalRevenue: 0,
|
totalRevenue: 0,
|
||||||
totalOrders: 0,
|
totalOrders: 0,
|
||||||
prevRevenue: 0,
|
prevRevenue: 0,
|
||||||
prevOrders: 0
|
prevOrders: 0,
|
||||||
|
periodProgress: 100
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate growth percentages
|
|
||||||
const growth = {
|
|
||||||
revenue: totals.prevRevenue > 0
|
|
||||||
? ((totals.totalRevenue - totals.prevRevenue) / totals.prevRevenue) * 100
|
|
||||||
: 0,
|
|
||||||
orders: totals.prevOrders > 0
|
|
||||||
? ((totals.totalOrders - totals.prevOrders) / totals.prevOrders) * 100
|
|
||||||
: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
setData(processedData);
|
setData(processedData);
|
||||||
setSummaryStats({ ...totals, growth });
|
setSummaryStats(totals);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Fetch projection if needed
|
||||||
|
if (totals.periodProgress < 100) {
|
||||||
|
fetchProjection();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error);
|
console.error("Error fetching data:", error);
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [fetchProjection]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -294,8 +306,16 @@ const MiniSalesChart = ({ className = "" }) => {
|
|||||||
title="30 Days Revenue"
|
title="30 Days Revenue"
|
||||||
value={formatCurrency(summaryStats.totalRevenue, false)}
|
value={formatCurrency(summaryStats.totalRevenue, false)}
|
||||||
previousValue={formatCurrency(summaryStats.prevRevenue, false)}
|
previousValue={formatCurrency(summaryStats.prevRevenue, false)}
|
||||||
trend={summaryStats.growth.revenue >= 0 ? "up" : "down"}
|
trend={
|
||||||
trendValue={`${Math.abs(Math.round(summaryStats.growth.revenue))}%`}
|
summaryStats.periodProgress < 100
|
||||||
|
? ((projection?.projectedRevenue || summaryStats.totalRevenue) >= summaryStats.prevRevenue ? "up" : "down")
|
||||||
|
: (summaryStats.totalRevenue >= summaryStats.prevRevenue ? "up" : "down")
|
||||||
|
}
|
||||||
|
trendValue={
|
||||||
|
summaryStats.periodProgress < 100
|
||||||
|
? `${Math.abs(Math.round(((projection?.projectedRevenue || summaryStats.totalRevenue) - summaryStats.prevRevenue) / summaryStats.prevRevenue * 100))}%`
|
||||||
|
: `${Math.abs(Math.round(((summaryStats.totalRevenue - summaryStats.prevRevenue) / summaryStats.prevRevenue) * 100))}%`
|
||||||
|
}
|
||||||
colorClass="text-emerald-300"
|
colorClass="text-emerald-300"
|
||||||
titleClass="text-emerald-300 font-bold text-md"
|
titleClass="text-emerald-300 font-bold text-md"
|
||||||
descriptionClass="text-emerald-300 text-md font-semibold pb-1"
|
descriptionClass="text-emerald-300 text-md font-semibold pb-1"
|
||||||
@@ -309,8 +329,16 @@ const MiniSalesChart = ({ className = "" }) => {
|
|||||||
title="30 Days Orders"
|
title="30 Days Orders"
|
||||||
value={summaryStats.totalOrders.toLocaleString()}
|
value={summaryStats.totalOrders.toLocaleString()}
|
||||||
previousValue={summaryStats.prevOrders.toLocaleString()}
|
previousValue={summaryStats.prevOrders.toLocaleString()}
|
||||||
trend={summaryStats.growth.orders >= 0 ? "up" : "down"}
|
trend={
|
||||||
trendValue={`${Math.abs(Math.round(summaryStats.growth.orders))}%`}
|
summaryStats.periodProgress < 100
|
||||||
|
? ((Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress))) >= summaryStats.prevOrders ? "up" : "down")
|
||||||
|
: (summaryStats.totalOrders >= summaryStats.prevOrders ? "up" : "down")
|
||||||
|
}
|
||||||
|
trendValue={
|
||||||
|
summaryStats.periodProgress < 100
|
||||||
|
? `${Math.abs(Math.round(((Math.round(summaryStats.totalOrders * (100 / summaryStats.periodProgress))) - summaryStats.prevOrders) / summaryStats.prevOrders * 100))}%`
|
||||||
|
: `${Math.abs(Math.round(((summaryStats.totalOrders - summaryStats.prevOrders) / summaryStats.prevOrders) * 100))}%`
|
||||||
|
}
|
||||||
colorClass="text-blue-300"
|
colorClass="text-blue-300"
|
||||||
titleClass="text-blue-300 font-bold text-md"
|
titleClass="text-blue-300 font-bold text-md"
|
||||||
descriptionClass="text-blue-300 text-md font-semibold pb-1"
|
descriptionClass="text-blue-300 text-md font-semibold pb-1"
|
||||||
|
|||||||
@@ -342,16 +342,8 @@ const calculateSummaryStats = (data = []) => {
|
|||||||
return best;
|
return best;
|
||||||
}, null);
|
}, null);
|
||||||
|
|
||||||
// Calculate growth percentages
|
// Get period progress from the last day
|
||||||
const growth = {
|
const periodProgress = data[data.length - 1]?.periodProgress || 100;
|
||||||
revenue: prevRevenue
|
|
||||||
? ((totalRevenue - prevRevenue) / prevRevenue) * 100
|
|
||||||
: 0,
|
|
||||||
orders: prevOrders ? ((totalOrders - prevOrders) / prevOrders) * 100 : 0,
|
|
||||||
avgOrderValue: prevAvgOrderValue
|
|
||||||
? ((avgOrderValue - prevAvgOrderValue) / prevAvgOrderValue) * 100
|
|
||||||
: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalRevenue,
|
totalRevenue,
|
||||||
@@ -361,7 +353,7 @@ const calculateSummaryStats = (data = []) => {
|
|||||||
prevRevenue,
|
prevRevenue,
|
||||||
prevOrders,
|
prevOrders,
|
||||||
prevAvgOrderValue,
|
prevAvgOrderValue,
|
||||||
growth,
|
periodProgress,
|
||||||
movingAverages: {
|
movingAverages: {
|
||||||
revenue: data[data.length - 1]?.movingAverage || 0,
|
revenue: data[data.length - 1]?.movingAverage || 0,
|
||||||
orders: data[data.length - 1]?.orderMovingAverage || 0,
|
orders: data[data.length - 1]?.orderMovingAverage || 0,
|
||||||
@@ -371,7 +363,7 @@ const calculateSummaryStats = (data = []) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add memoized SummaryStats component
|
// Add memoized SummaryStats component
|
||||||
const SummaryStats = memo(({ stats = {} }) => {
|
const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = false }) => {
|
||||||
const {
|
const {
|
||||||
totalRevenue = 0,
|
totalRevenue = 0,
|
||||||
totalOrders = 0,
|
totalOrders = 0,
|
||||||
@@ -380,17 +372,39 @@ const SummaryStats = memo(({ stats = {} }) => {
|
|||||||
prevRevenue = 0,
|
prevRevenue = 0,
|
||||||
prevOrders = 0,
|
prevOrders = 0,
|
||||||
prevAvgOrderValue = 0,
|
prevAvgOrderValue = 0,
|
||||||
growth = { revenue: 0, orders: 0, avgOrderValue: 0 },
|
periodProgress = 100
|
||||||
} = stats;
|
} = stats;
|
||||||
|
|
||||||
|
// Calculate projected values when period is incomplete
|
||||||
|
const currentRevenue = periodProgress < 100 ? (projection?.projectedRevenue || totalRevenue) : totalRevenue;
|
||||||
|
const revenueTrend = currentRevenue >= prevRevenue ? "up" : "down";
|
||||||
|
const revenueDiff = Math.abs(currentRevenue - prevRevenue);
|
||||||
|
const revenuePercentage = (revenueDiff / prevRevenue) * 100;
|
||||||
|
|
||||||
|
// Calculate order trends
|
||||||
|
const currentOrders = periodProgress < 100 ? (projection?.projectedOrders || totalOrders) : totalOrders;
|
||||||
|
const ordersTrend = currentOrders >= prevOrders ? "up" : "down";
|
||||||
|
const ordersDiff = Math.abs(currentOrders - prevOrders);
|
||||||
|
const ordersPercentage = (ordersDiff / prevOrders) * 100;
|
||||||
|
|
||||||
|
// Calculate AOV trends
|
||||||
|
const currentAOV = currentOrders ? currentRevenue / currentOrders : avgOrderValue;
|
||||||
|
const aovTrend = currentAOV >= prevAvgOrderValue ? "up" : "down";
|
||||||
|
const aovDiff = Math.abs(currentAOV - prevAvgOrderValue);
|
||||||
|
const aovPercentage = (aovDiff / prevAvgOrderValue) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl">
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Total Revenue"
|
title="Total Revenue"
|
||||||
value={formatCurrency(totalRevenue, false)}
|
value={formatCurrency(totalRevenue, false)}
|
||||||
description={`Previous: ${formatCurrency(prevRevenue, false)}`}
|
description={
|
||||||
trend={growth.revenue >= 0 ? "up" : "down"}
|
periodProgress < 100
|
||||||
trendValue={formatPercentage(growth.revenue)}
|
? `Projected: ${formatCurrency(projection?.projectedRevenue || totalRevenue, false)}`
|
||||||
|
: `Previous: ${formatCurrency(prevRevenue, false)}`
|
||||||
|
}
|
||||||
|
trend={projectionLoading && periodProgress < 100 ? undefined : revenueTrend}
|
||||||
|
trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(revenuePercentage)}
|
||||||
info="Total revenue for the selected period"
|
info="Total revenue for the selected period"
|
||||||
colorClass="text-green-600 dark:text-green-400"
|
colorClass="text-green-600 dark:text-green-400"
|
||||||
/>
|
/>
|
||||||
@@ -398,9 +412,13 @@ const SummaryStats = memo(({ stats = {} }) => {
|
|||||||
<StatCard
|
<StatCard
|
||||||
title="Total Orders"
|
title="Total Orders"
|
||||||
value={totalOrders.toLocaleString()}
|
value={totalOrders.toLocaleString()}
|
||||||
description={`Previous: ${prevOrders.toLocaleString()} orders`}
|
description={
|
||||||
trend={growth.orders >= 0 ? "up" : "down"}
|
periodProgress < 100
|
||||||
trendValue={formatPercentage(growth.orders)}
|
? `Projected: ${(projection?.projectedOrders || totalOrders).toLocaleString()}`
|
||||||
|
: `Previous: ${prevOrders.toLocaleString()}`
|
||||||
|
}
|
||||||
|
trend={projectionLoading && periodProgress < 100 ? undefined : ordersTrend}
|
||||||
|
trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(ordersPercentage)}
|
||||||
info="Total number of orders for the selected period"
|
info="Total number of orders for the selected period"
|
||||||
colorClass="text-blue-600 dark:text-blue-400"
|
colorClass="text-blue-600 dark:text-blue-400"
|
||||||
/>
|
/>
|
||||||
@@ -408,9 +426,13 @@ const SummaryStats = memo(({ stats = {} }) => {
|
|||||||
<StatCard
|
<StatCard
|
||||||
title="AOV"
|
title="AOV"
|
||||||
value={formatCurrency(avgOrderValue)}
|
value={formatCurrency(avgOrderValue)}
|
||||||
description={`Previous: ${formatCurrency(prevAvgOrderValue)}`}
|
description={
|
||||||
trend={growth.avgOrderValue >= 0 ? "up" : "down"}
|
periodProgress < 100
|
||||||
trendValue={formatPercentage(growth.avgOrderValue)}
|
? `Projected: ${formatCurrency(currentAOV)}`
|
||||||
|
: `Previous: ${formatCurrency(prevAvgOrderValue)}`
|
||||||
|
}
|
||||||
|
trend={projectionLoading && periodProgress < 100 ? undefined : aovTrend}
|
||||||
|
trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(aovPercentage)}
|
||||||
info="Average value per order for the selected period"
|
info="Average value per order for the selected period"
|
||||||
colorClass="text-purple-600 dark:text-purple-400"
|
colorClass="text-purple-600 dark:text-purple-400"
|
||||||
/>
|
/>
|
||||||
@@ -519,6 +541,25 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
|
|||||||
showPrevious: false,
|
showPrevious: false,
|
||||||
});
|
});
|
||||||
const [summaryStats, setSummaryStats] = useState({});
|
const [summaryStats, setSummaryStats] = useState({});
|
||||||
|
const [projection, setProjection] = useState(null);
|
||||||
|
const [projectionLoading, setProjectionLoading] = useState(false);
|
||||||
|
|
||||||
|
// Add function to fetch projection
|
||||||
|
const fetchProjection = useCallback(async (params) => {
|
||||||
|
if (!params) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setProjectionLoading(true);
|
||||||
|
const response = await axios.get("/api/klaviyo/events/projection", {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
setProjection(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading projection:", error);
|
||||||
|
} finally {
|
||||||
|
setProjectionLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch data function
|
// Fetch data function
|
||||||
const fetchData = useCallback(async (params) => {
|
const fetchData = useCallback(async (params) => {
|
||||||
@@ -551,13 +592,18 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
|
|||||||
setData(processedData);
|
setData(processedData);
|
||||||
setSummaryStats(stats);
|
setSummaryStats(stats);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// Fetch projection if needed
|
||||||
|
if (stats.periodProgress < 100) {
|
||||||
|
fetchProjection(params);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error);
|
console.error("Error fetching data:", error);
|
||||||
setError(error.message);
|
setError(error.message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [fetchProjection]);
|
||||||
|
|
||||||
// Handle time range change
|
// Handle time range change
|
||||||
const handleTimeRangeChange = useCallback(
|
const handleTimeRangeChange = useCallback(
|
||||||
@@ -832,7 +878,11 @@ const SalesChart = ({ timeRange = "last30days", title = "Sales Overview" }) => {
|
|||||||
(loading ? (
|
(loading ? (
|
||||||
<SkeletonStats />
|
<SkeletonStats />
|
||||||
) : (
|
) : (
|
||||||
<SummaryStats stats={summaryStats} />
|
<SummaryStats
|
||||||
|
stats={summaryStats}
|
||||||
|
projection={projection}
|
||||||
|
projectionLoading={projectionLoading}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Show metric toggles only if not in error state */}
|
{/* Show metric toggles only if not in error state */}
|
||||||
|
|||||||
@@ -1256,6 +1256,98 @@ const SkeletonTable = ({ rows = 8 }) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const SummaryStats = memo(({ stats = {}, projection = null, projectionLoading = false }) => {
|
||||||
|
const {
|
||||||
|
totalRevenue = 0,
|
||||||
|
totalOrders = 0,
|
||||||
|
avgOrderValue = 0,
|
||||||
|
bestDay = null,
|
||||||
|
prevRevenue = 0,
|
||||||
|
prevOrders = 0,
|
||||||
|
prevAvgOrderValue = 0,
|
||||||
|
periodProgress = 100
|
||||||
|
} = stats;
|
||||||
|
|
||||||
|
// Calculate projected values when period is incomplete
|
||||||
|
const currentRevenue = periodProgress < 100 ? (projection?.projectedRevenue || totalRevenue) : totalRevenue;
|
||||||
|
const revenueTrend = currentRevenue >= prevRevenue ? "up" : "down";
|
||||||
|
const revenueDiff = Math.abs(currentRevenue - prevRevenue);
|
||||||
|
const revenuePercentage = (revenueDiff / prevRevenue) * 100;
|
||||||
|
|
||||||
|
// Calculate order trends
|
||||||
|
const currentOrders = periodProgress < 100 ? Math.round(totalOrders * (100 / periodProgress)) : totalOrders;
|
||||||
|
const ordersTrend = currentOrders >= prevOrders ? "up" : "down";
|
||||||
|
const ordersDiff = Math.abs(currentOrders - prevOrders);
|
||||||
|
const ordersPercentage = (ordersDiff / prevOrders) * 100;
|
||||||
|
|
||||||
|
// Calculate AOV trends
|
||||||
|
const currentAOV = currentOrders ? currentRevenue / currentOrders : avgOrderValue;
|
||||||
|
const aovTrend = currentAOV >= prevAvgOrderValue ? "up" : "down";
|
||||||
|
const aovDiff = Math.abs(currentAOV - prevAvgOrderValue);
|
||||||
|
const aovPercentage = (aovDiff / prevAvgOrderValue) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 py-4 max-w-3xl">
|
||||||
|
<StatCard
|
||||||
|
title="Total Revenue"
|
||||||
|
value={formatCurrency(totalRevenue, false)}
|
||||||
|
description={
|
||||||
|
periodProgress < 100
|
||||||
|
? `Projected: ${formatCurrency(currentRevenue, false)}`
|
||||||
|
: `Previous: ${formatCurrency(prevRevenue, false)}`
|
||||||
|
}
|
||||||
|
trend={projectionLoading && periodProgress < 100 ? undefined : revenueTrend}
|
||||||
|
trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(revenuePercentage)}
|
||||||
|
info="Total revenue for the selected period"
|
||||||
|
colorClass="text-green-600 dark:text-green-400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="Total Orders"
|
||||||
|
value={totalOrders.toLocaleString()}
|
||||||
|
description={
|
||||||
|
periodProgress < 100
|
||||||
|
? `Projected: ${currentOrders.toLocaleString()}`
|
||||||
|
: `Previous: ${prevOrders.toLocaleString()}`
|
||||||
|
}
|
||||||
|
trend={projectionLoading && periodProgress < 100 ? undefined : ordersTrend}
|
||||||
|
trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(ordersPercentage)}
|
||||||
|
info="Total number of orders for the selected period"
|
||||||
|
colorClass="text-blue-600 dark:text-blue-400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="AOV"
|
||||||
|
value={formatCurrency(avgOrderValue)}
|
||||||
|
description={
|
||||||
|
periodProgress < 100
|
||||||
|
? `Projected: ${formatCurrency(currentAOV)}`
|
||||||
|
: `Previous: ${formatCurrency(prevAvgOrderValue)}`
|
||||||
|
}
|
||||||
|
trend={projectionLoading && periodProgress < 100 ? undefined : aovTrend}
|
||||||
|
trendValue={projectionLoading && periodProgress < 100 ? null : formatPercentage(aovPercentage)}
|
||||||
|
info="Average value per order for the selected period"
|
||||||
|
colorClass="text-purple-600 dark:text-purple-400"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="Best Day"
|
||||||
|
value={formatCurrency(bestDay?.revenue || 0, false)}
|
||||||
|
description={
|
||||||
|
bestDay?.timestamp
|
||||||
|
? `${new Date(bestDay.timestamp).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})} - ${bestDay.orders} orders`
|
||||||
|
: "No data"
|
||||||
|
}
|
||||||
|
info="Day with highest revenue in the selected period"
|
||||||
|
colorClass="text-orange-600 dark:text-orange-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const StatCards = ({
|
const StatCards = ({
|
||||||
timeRange: initialTimeRange = "today",
|
timeRange: initialTimeRange = "today",
|
||||||
startDate,
|
startDate,
|
||||||
|
|||||||
Reference in New Issue
Block a user