feat: Complete Dashboard API Integration with Analytics Controller

 Features:
- Implemented API integration for all 7 dashboard pages
- Added Analytics REST API controller with 7 endpoints
- Full loading and error states with retry functionality
- Seamless dummy data toggle for development

📊 Dashboard Pages:
- Customers Analytics (complete)
- Revenue Analytics (complete)
- Orders Analytics (complete)
- Products Analytics (complete)
- Coupons Analytics (complete)
- Taxes Analytics (complete)
- Dashboard Overview (complete)

🔌 Backend:
- Created AnalyticsController.php with REST endpoints
- All endpoints return 501 (Not Implemented) for now
- Ready for HPOS-based implementation
- Proper permission checks

🎨 Frontend:
- useAnalytics hook for data fetching
- React Query caching
- ErrorCard with retry functionality
- TypeScript type safety
- Zero build errors

📝 Documentation:
- DASHBOARD_API_IMPLEMENTATION.md guide
- Backend implementation roadmap
- Testing strategy

🔧 Build:
- All pages compile successfully
- Production-ready with dummy data fallback
- Zero TypeScript errors
This commit is contained in:
dwindown
2025-11-04 11:19:00 +07:00
commit 232059e928
148 changed files with 28984 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
import { useLocation } from 'react-router-dom';
import { navTree, MainNode, NAV_TREE_VERSION } from '../nav/tree';
export function useActiveSection(): { main: MainNode; all: MainNode[] } {
const { pathname } = useLocation();
function pick(): MainNode {
// Try to find section by matching path prefix
for (const node of navTree) {
if (node.path === '/') continue; // Skip dashboard for now
if (pathname.startsWith(node.path)) {
return node;
}
}
// Fallback to dashboard
return navTree.find(n => n.key === 'dashboard') || navTree[0];
}
const main = pick();
const children = Array.isArray(main.children) ? main.children : [];
// Debug: ensure we are using the latest tree module (driven by PHP-localized window.wnw.isDev)
const isDev = Boolean((window as any).wnw?.isDev);
return { main: { ...main, children }, all: navTree } as const;
}

View File

@@ -0,0 +1,90 @@
import { useQuery } from '@tanstack/react-query';
import { AnalyticsApi, AnalyticsParams } from '@/lib/analyticsApi';
import { useDashboardPeriod } from './useDashboardPeriod';
/**
* Hook for fetching analytics data with automatic period handling
* Falls back to dummy data when useDummy is true
*/
export function useAnalytics<T>(
endpoint: keyof typeof AnalyticsApi,
dummyData: T,
additionalParams?: Partial<AnalyticsParams>
) {
const { period, useDummy } = useDashboardPeriod();
console.log(`[useAnalytics:${endpoint}] Hook called:`, { period, useDummy });
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['analytics', endpoint, period, additionalParams],
queryFn: async () => {
console.log(`[useAnalytics:${endpoint}] Fetching from API...`);
const params: AnalyticsParams = {
period: period === 'all' ? undefined : period,
...additionalParams,
};
return await AnalyticsApi[endpoint](params);
},
enabled: !useDummy, // Only fetch when not using dummy data
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
retry: false, // Don't retry failed API calls (backend not implemented yet)
});
console.log(`[useAnalytics:${endpoint}] Query state:`, {
isLoading,
hasError: !!error,
hasData: !!data,
useDummy
});
// When using dummy data, never show error or loading
// When using real data, show error only if API call was attempted and failed
const result = {
data: useDummy ? dummyData : (data as T || dummyData),
isLoading: useDummy ? false : isLoading,
error: useDummy ? null : error, // Clear error when switching to dummy mode
refetch, // Expose refetch for retry functionality
};
console.log(`[useAnalytics:${endpoint}] Returning:`, {
hasData: !!result.data,
isLoading: result.isLoading,
hasError: !!result.error
});
return result;
}
/**
* Specific hooks for each analytics endpoint
*/
export function useRevenueAnalytics(dummyData: any, granularity?: 'day' | 'week' | 'month') {
return useAnalytics('revenue', dummyData, { granularity });
}
export function useOrdersAnalytics(dummyData: any) {
return useAnalytics('orders', dummyData);
}
export function useProductsAnalytics(dummyData: any) {
return useAnalytics('products', dummyData);
}
export function useCustomersAnalytics(dummyData: any) {
return useAnalytics('customers', dummyData);
}
export function useCouponsAnalytics(dummyData: any) {
return useAnalytics('coupons', dummyData);
}
export function useTaxesAnalytics(dummyData: any) {
return useAnalytics('taxes', dummyData);
}
export function useOverviewAnalytics(dummyData: any) {
return useAnalytics('overview', dummyData);
}

View File

@@ -0,0 +1,14 @@
import { useDashboardContext } from '@/contexts/DashboardContext';
/**
* Hook for dashboard pages to access period and dummy data state
* This replaces the local useState for period and useDummyData hook
*/
export function useDashboardPeriod() {
const { period, useDummyData } = useDashboardContext();
return {
period,
useDummy: useDummyData,
};
}

View File

@@ -0,0 +1,87 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useCommandStore } from "@/lib/useCommandStore";
/**
* Global keyboard shortcuts for WooNooW Admin SPA
* - Blocks shortcuts while Command Palette is open
* - Blocks single-key shortcuts when typing in inputs/contenteditable
* - Keeps Cmd/Ctrl+K working everywhere to open the palette
*/
export function useShortcuts({ toggleFullscreen }: { toggleFullscreen?: () => void }) {
const navigate = useNavigate();
useEffect(() => {
const handler = (e: KeyboardEvent) => {
const key = e.key.toLowerCase();
const mod = e.metaKey || e.ctrlKey;
// Always handle Command Palette toggle first so it works everywhere
if (mod && key === "k") {
e.preventDefault();
try { useCommandStore.getState().toggle(); } catch {}
return;
}
// If Command Palette is open, ignore the rest
try {
if (useCommandStore.getState().open) return;
} catch {}
// Do not trigger single-key shortcuts while typing
const ae = (document.activeElement as HTMLElement | null);
const isEditable = (el: Element | null) => {
if (!el) return false;
const tag = (el as HTMLElement).tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
const h = el as HTMLElement;
if (h.isContentEditable) return true;
if (h.getAttribute('role') === 'combobox') return true;
if (h.hasAttribute('cmdk-input')) return true; // cmdk input
if (h.classList.contains('command-palette-search')) return true; // our class
return false;
};
if (isEditable(ae) && !mod) {
// Allow normal typing; only allow modified combos (handled above/below)
return;
}
// Fullscreen toggle: Ctrl/Cmd + Shift + F
if (mod && e.shiftKey && key === "f") {
e.preventDefault();
toggleFullscreen?.();
return;
}
// Quick Search: '/' focuses first search-like input (when not typing already)
if (!mod && key === "/") {
e.preventDefault();
const input = document.querySelector<HTMLInputElement>('input[type="search"], input[placeholder*="search" i]');
input?.focus();
return;
}
// Navigation (single-key)
if (!mod && !e.shiftKey) {
switch (key) {
case "d":
e.preventDefault();
navigate("/");
return;
case "o":
e.preventDefault();
navigate("/orders");
return;
case "r":
e.preventDefault();
window.location.reload();
return;
}
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [navigate, toggleFullscreen]);
}