Some checks failed
Build Frontend / build (push) Failing after 7s
Introduces a modular dashboard system with draggable, configurable widgets including revenue, low stock, recent customers, and pending chats. Adds a dashboard editor for layout customization, widget visibility, and settings. Refactors dashboard content to use the new widget system and improves UI consistency and interactivity.
594 lines
15 KiB
TypeScript
594 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { clientFetch } from "../api-client";
|
|
|
|
// Analytics Types
|
|
export interface AnalyticsOverview {
|
|
orders: {
|
|
total: number;
|
|
completed: number;
|
|
pending: number;
|
|
completionRate: string;
|
|
};
|
|
revenue: {
|
|
total: number;
|
|
monthly: number;
|
|
weekly: number;
|
|
averageOrderValue: number;
|
|
};
|
|
products: {
|
|
total: number;
|
|
};
|
|
customers: {
|
|
unique: number;
|
|
};
|
|
userType?: "vendor" | "staff";
|
|
}
|
|
|
|
export interface RevenueData {
|
|
_id: {
|
|
year: number;
|
|
month: number;
|
|
day: number;
|
|
};
|
|
revenue: number;
|
|
orders: number;
|
|
}
|
|
|
|
export interface ProductPerformance {
|
|
productId: string;
|
|
name: string;
|
|
image: string;
|
|
unitType: string;
|
|
currentStock: number;
|
|
stockStatus: string;
|
|
totalSold: number;
|
|
totalRevenue: number;
|
|
orderCount: number;
|
|
averagePrice: number;
|
|
}
|
|
|
|
export interface CustomerInsights {
|
|
totalCustomers: number;
|
|
segments: {
|
|
new: number;
|
|
returning: number;
|
|
loyal: number;
|
|
vip: number;
|
|
};
|
|
topCustomers: Array<{
|
|
_id: string;
|
|
orderCount: number;
|
|
totalSpent: number;
|
|
averageOrderValue: number;
|
|
firstOrder: string;
|
|
lastOrder: string;
|
|
displayName?: string;
|
|
username?: string;
|
|
}>;
|
|
averageOrdersPerCustomer: string;
|
|
pagination?: {
|
|
currentPage: number;
|
|
totalPages: number;
|
|
totalCustomers: number;
|
|
limit: number;
|
|
hasNextPage: boolean;
|
|
hasPrevPage: boolean;
|
|
startIndex: number;
|
|
endIndex: number;
|
|
};
|
|
}
|
|
|
|
export interface OrderAnalytics {
|
|
statusDistribution: Array<{
|
|
_id: string;
|
|
count: number;
|
|
}>;
|
|
}
|
|
|
|
export interface GrowthAnalytics {
|
|
launchDate: string;
|
|
generatedAt: string;
|
|
daily: Array<{
|
|
date: string;
|
|
orders: number;
|
|
revenue: number;
|
|
customers: number;
|
|
avgOrderValue: number;
|
|
}>;
|
|
monthly: Array<{
|
|
month: string;
|
|
orders: number;
|
|
revenue: number;
|
|
customers: number;
|
|
avgOrderValue: number;
|
|
newCustomers: number;
|
|
}>;
|
|
customers: {
|
|
total: number;
|
|
segments: {
|
|
new: number;
|
|
returning: number;
|
|
loyal: number;
|
|
vip: number;
|
|
};
|
|
segmentDetails: {
|
|
[key: string]: {
|
|
count: number;
|
|
totalRevenue: number;
|
|
avgOrderCount: number;
|
|
avgSpent: number;
|
|
};
|
|
};
|
|
segmentPercentages: {
|
|
new: number;
|
|
returning: number;
|
|
loyal: number;
|
|
vip: number;
|
|
};
|
|
};
|
|
cumulative: {
|
|
orders: number;
|
|
revenue: number;
|
|
customers: number;
|
|
products: number;
|
|
avgOrderValue: number;
|
|
};
|
|
}
|
|
|
|
// Analytics Service Functions
|
|
|
|
/**
|
|
* Get analytics overview data
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getAnalyticsOverview = async (
|
|
storeId?: string,
|
|
): Promise<AnalyticsOverview> => {
|
|
const url = storeId
|
|
? `/analytics/overview?storeId=${storeId}`
|
|
: "/analytics/overview";
|
|
return clientFetch<AnalyticsOverview>(url);
|
|
};
|
|
|
|
/**
|
|
* Get revenue trends data
|
|
* @param period Time period in days (7, 30, 90)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getRevenueTrends = async (
|
|
period: string = "30",
|
|
storeId?: string,
|
|
): Promise<RevenueData[]> => {
|
|
const params = new URLSearchParams({ period });
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/revenue-trends?${params.toString()}`;
|
|
return clientFetch<RevenueData[]>(url);
|
|
};
|
|
|
|
/**
|
|
* Get product performance data
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getProductPerformance = async (
|
|
storeId?: string,
|
|
): Promise<ProductPerformance[]> => {
|
|
const url = storeId
|
|
? `/analytics/product-performance?storeId=${storeId}`
|
|
: "/analytics/product-performance";
|
|
return clientFetch<ProductPerformance[]>(url);
|
|
};
|
|
|
|
/**
|
|
* Get customer insights data
|
|
* @param storeId Optional storeId for staff users
|
|
* @param page Page number (default: 1)
|
|
* @param limit Number of customers per page (default: 10)
|
|
*/
|
|
export const getCustomerInsights = async (
|
|
storeId?: string,
|
|
page: number = 1,
|
|
limit: number = 10,
|
|
sortBy: string = "spent",
|
|
): Promise<CustomerInsights> => {
|
|
const params = new URLSearchParams({
|
|
page: page.toString(),
|
|
limit: limit.toString(),
|
|
sort: sortBy,
|
|
});
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/customer-insights?${params.toString()}`;
|
|
return clientFetch<CustomerInsights>(url);
|
|
};
|
|
|
|
/**
|
|
* Get order analytics data
|
|
* @param period Time period in days (7, 30, 90)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getOrderAnalytics = async (
|
|
period: string = "30",
|
|
storeId?: string,
|
|
): Promise<OrderAnalytics> => {
|
|
const params = new URLSearchParams({ period });
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/order-analytics?${params.toString()}`;
|
|
return clientFetch<OrderAnalytics>(url);
|
|
};
|
|
|
|
/**
|
|
* Get growth analytics data (since first order)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getGrowthAnalytics = async (
|
|
storeId?: string,
|
|
): Promise<GrowthAnalytics> => {
|
|
const params = new URLSearchParams();
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = params.toString()
|
|
? `/analytics/growth?${params.toString()}`
|
|
: "/analytics/growth";
|
|
return clientFetch<GrowthAnalytics>(url);
|
|
};
|
|
|
|
// Helper function to determine if user is staff and get storeId
|
|
export const getStoreIdForUser = (): string | undefined => {
|
|
if (typeof window === "undefined") return undefined;
|
|
|
|
// Check if user is staff (you might need to adjust this based on your auth system)
|
|
const userType = localStorage.getItem("userType");
|
|
const storeId = localStorage.getItem("storeId");
|
|
|
|
if (userType === "staff" && storeId) {
|
|
return storeId;
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
// Enhanced analytics functions that automatically handle storeId for staff users
|
|
export const getAnalyticsOverviewWithStore =
|
|
async (): Promise<AnalyticsOverview> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getAnalyticsOverview(storeId);
|
|
};
|
|
|
|
export const getRevenueTrendsWithStore = async (
|
|
period: string = "30",
|
|
): Promise<RevenueData[]> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getRevenueTrends(period, storeId);
|
|
};
|
|
|
|
export const getProductPerformanceWithStore = async (): Promise<
|
|
ProductPerformance[]
|
|
> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getProductPerformance(storeId);
|
|
};
|
|
|
|
export const getCustomerInsightsWithStore = async (
|
|
page: number = 1,
|
|
limit: number = 10,
|
|
sortBy: string = "spent",
|
|
): Promise<CustomerInsights> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getCustomerInsights(storeId, page, limit, sortBy);
|
|
};
|
|
|
|
export const getOrderAnalyticsWithStore = async (
|
|
period: string = "30",
|
|
): Promise<OrderAnalytics> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getOrderAnalytics(period, storeId);
|
|
};
|
|
|
|
export const getGrowthAnalyticsWithStore =
|
|
async (): Promise<GrowthAnalytics> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getGrowthAnalytics(storeId);
|
|
};
|
|
|
|
export function formatGBP(value: number) {
|
|
return value.toLocaleString("en-GB", {
|
|
style: "currency",
|
|
currency: "GBP",
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
});
|
|
}
|
|
|
|
// Prediction Types
|
|
export interface SalesPrediction {
|
|
predicted: number | null;
|
|
predictedOrders: number | null;
|
|
dailyPredictions?: Array<{
|
|
day: number;
|
|
predicted: number;
|
|
date: string;
|
|
}>;
|
|
confidence: "very_high" | "high" | "medium" | "low";
|
|
method: string;
|
|
methods?: {
|
|
aiNeuralNetwork?: number | null;
|
|
weightedMovingAverage?: number | null;
|
|
exponentialSmoothing?: number | null;
|
|
holtWinters?: number | null;
|
|
weightedLinearRegression?: number | null;
|
|
trendAdjusted?: number | null;
|
|
};
|
|
aiModel?: {
|
|
used: boolean;
|
|
available?: boolean;
|
|
confidence?: string;
|
|
modelAccuracy?: number;
|
|
error?: string | null;
|
|
};
|
|
trend?: {
|
|
direction: "up" | "down" | "neutral";
|
|
strength: number;
|
|
acceleration: number;
|
|
slope?: number;
|
|
};
|
|
variance?: number;
|
|
minPrediction?: number;
|
|
maxPrediction?: number;
|
|
confidenceScore?: number;
|
|
confidenceIntervals?: {
|
|
lower: number;
|
|
upper: number;
|
|
confidenceScore?: number;
|
|
modelAgreement?: number;
|
|
avgModelAccuracy?: number;
|
|
dataConsistency?: number;
|
|
};
|
|
modelPerformance?: {
|
|
[key: string]: {
|
|
mae?: number;
|
|
mape?: number;
|
|
rmse?: number;
|
|
accuracy?: number;
|
|
confidence?: string;
|
|
};
|
|
};
|
|
seasonality?: {
|
|
dayOfWeek: Record<string, number>;
|
|
month: Record<string, number>;
|
|
confidence: string;
|
|
};
|
|
historicalPeriod: number;
|
|
predictionPeriod: number;
|
|
message?: string;
|
|
}
|
|
|
|
export interface DemandPrediction {
|
|
predictedDaily: number | null;
|
|
predictedWeekly: number | null;
|
|
predictedMonthly: number | null;
|
|
confidence: "very_high" | "high" | "medium" | "low";
|
|
averageDaily?: number;
|
|
trendFactor?: number;
|
|
stdDev?: number;
|
|
confidenceIntervals?: {
|
|
lower: number;
|
|
upper: number;
|
|
confidenceScore?: number;
|
|
modelAgreement?: number;
|
|
avgModelAccuracy?: number;
|
|
dataConsistency?: number;
|
|
};
|
|
historicalPeriod: number;
|
|
predictionPeriod: number;
|
|
productId?: string | null;
|
|
message?: string;
|
|
}
|
|
|
|
export interface StockPrediction {
|
|
productId: string;
|
|
productName: string;
|
|
currentStock: number;
|
|
lowStockThreshold: number;
|
|
unitType: string;
|
|
prediction: {
|
|
daysUntilOutOfStock: number | null;
|
|
estimatedDate: string | null;
|
|
confidence: "very_high" | "high" | "medium" | "low";
|
|
averageDailySales?: number;
|
|
stdDev?: number;
|
|
optimisticDays?: number | null;
|
|
pessimisticDays?: number | null;
|
|
optimisticDate?: string | null;
|
|
pessimisticDate?: string | null;
|
|
message?: string;
|
|
};
|
|
needsRestock: boolean;
|
|
}
|
|
|
|
export interface StockPredictionsResponse {
|
|
predictions: StockPrediction[];
|
|
historicalPeriod: number;
|
|
totalProducts: number;
|
|
productsNeedingRestock: number;
|
|
}
|
|
|
|
export interface PredictionsOverview {
|
|
sales: SalesPrediction;
|
|
demand: DemandPrediction;
|
|
stock: {
|
|
totalProducts: number;
|
|
message?: string;
|
|
};
|
|
historicalPeriod: number;
|
|
predictionPeriod: number;
|
|
message?: string;
|
|
}
|
|
|
|
export interface BatchPredictionsResponse {
|
|
success: boolean;
|
|
storeId?: string;
|
|
historicalPeriod?: number;
|
|
horizons?: number[];
|
|
simulationFactors?: number[];
|
|
predictions?: {
|
|
[horizon: string]: {
|
|
[simulationFactor: string]: PredictionsOverview;
|
|
};
|
|
};
|
|
totalEntries?: number;
|
|
generatedAt?: string;
|
|
message?: string;
|
|
}
|
|
|
|
|
|
// Prediction Service Functions
|
|
|
|
/**
|
|
* Get sales/revenue predictions
|
|
* @param daysAhead Number of days to predict ahead (default: 7)
|
|
* @param period Historical period in days (default: 30)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getSalesPredictions = async (
|
|
daysAhead: number = 7,
|
|
period: number = 30,
|
|
storeId?: string,
|
|
): Promise<SalesPrediction> => {
|
|
const params = new URLSearchParams({
|
|
daysAhead: daysAhead.toString(),
|
|
period: period.toString(),
|
|
});
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/predictions/sales?${params.toString()}`;
|
|
return clientFetch<SalesPrediction>(url);
|
|
};
|
|
|
|
/**
|
|
* Get product demand predictions
|
|
* @param productId Optional product ID for specific product prediction
|
|
* @param daysAhead Number of days to predict ahead (default: 7)
|
|
* @param period Historical period in days (default: 30)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getDemandPredictions = async (
|
|
productId?: string,
|
|
daysAhead: number = 7,
|
|
period: number = 30,
|
|
storeId?: string,
|
|
): Promise<DemandPrediction> => {
|
|
const params = new URLSearchParams({
|
|
daysAhead: daysAhead.toString(),
|
|
period: period.toString(),
|
|
});
|
|
if (productId) params.append("productId", productId);
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/predictions/demand?${params.toString()}`;
|
|
return clientFetch<DemandPrediction>(url);
|
|
};
|
|
|
|
/**
|
|
* Get stock depletion predictions
|
|
* @param period Historical period in days (default: 30)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getStockPredictions = async (
|
|
period: number = 30,
|
|
storeId?: string,
|
|
): Promise<StockPredictionsResponse> => {
|
|
const params = new URLSearchParams({
|
|
period: period.toString(),
|
|
});
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/predictions/stock?${params.toString()}`;
|
|
return clientFetch<StockPredictionsResponse>(url);
|
|
};
|
|
|
|
/**
|
|
* Get comprehensive predictions overview
|
|
* @param daysAhead Number of days to predict ahead (default: 7)
|
|
* @param period Historical period in days (default: 30)
|
|
* @param storeId Optional storeId for staff users
|
|
* @param simulation Simulation factor (e.g. 0.1 for +10%)
|
|
*/
|
|
export const getPredictionsOverview = async (
|
|
daysAhead: number = 7,
|
|
period: number = 30,
|
|
storeId?: string,
|
|
simulation: number = 0,
|
|
): Promise<PredictionsOverview> => {
|
|
const params = new URLSearchParams({
|
|
daysAhead: daysAhead.toString(),
|
|
period: period.toString(),
|
|
simulation: simulation.toString(),
|
|
});
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/predictions/overview?${params.toString()}`;
|
|
return clientFetch<PredictionsOverview>(url);
|
|
};
|
|
|
|
// Helper functions with automatic storeId handling
|
|
export const getSalesPredictionsWithStore = async (
|
|
daysAhead: number = 7,
|
|
period: number = 30,
|
|
): Promise<SalesPrediction> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getSalesPredictions(daysAhead, period, storeId);
|
|
};
|
|
|
|
export const getDemandPredictionsWithStore = async (
|
|
productId?: string,
|
|
daysAhead: number = 7,
|
|
period: number = 30,
|
|
): Promise<DemandPrediction> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getDemandPredictions(productId, daysAhead, period, storeId);
|
|
};
|
|
|
|
export const getStockPredictionsWithStore = async (
|
|
period: number = 30,
|
|
): Promise<StockPredictionsResponse> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getStockPredictions(period, storeId);
|
|
};
|
|
|
|
export const getPredictionsOverviewWithStore = async (
|
|
daysAhead: number = 7,
|
|
period: number = 30,
|
|
simulation: number = 0,
|
|
): Promise<PredictionsOverview> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getPredictionsOverview(daysAhead, period, storeId, simulation);
|
|
};
|
|
|
|
/**
|
|
* Get all cached predictions in one request (for client-side switching)
|
|
* @param period Historical period in days (default: 90)
|
|
* @param storeId Optional storeId for staff users
|
|
*/
|
|
export const getBatchPredictions = async (
|
|
period: number = 90,
|
|
storeId?: string,
|
|
): Promise<BatchPredictionsResponse> => {
|
|
const params = new URLSearchParams({
|
|
period: period.toString(),
|
|
});
|
|
if (storeId) params.append("storeId", storeId);
|
|
|
|
const url = `/analytics/predictions/batch?${params.toString()}`;
|
|
return clientFetch<BatchPredictionsResponse>(url);
|
|
};
|
|
|
|
export const getBatchPredictionsWithStore = async (
|
|
period: number = 90,
|
|
): Promise<BatchPredictionsResponse> => {
|
|
const storeId = getStoreIdForUser();
|
|
return getBatchPredictions(period, storeId);
|
|
};
|