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,115 @@
// admin-spa/src/components/ui/searchable-select.tsx
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
import {
Command,
CommandInput,
CommandList,
CommandItem,
CommandEmpty,
} from "@/components/ui/command";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
export interface Option {
value: string;
/** What to render in the button/list. Can be a string or React node. */
label: React.ReactNode;
/** Optional text used for filtering. Falls back to string label or value. */
searchText?: string;
}
interface Props {
value?: string;
onChange?: (v: string) => void;
options: Option[];
placeholder?: string;
emptyLabel?: string;
className?: string;
disabled?: boolean;
search?: string;
onSearch?: (v: string) => void;
showCheckIndicator?: boolean;
}
export function SearchableSelect({
value,
onChange,
options,
placeholder = "Select...",
emptyLabel = "No results found.",
className,
disabled = false,
search,
onSearch,
showCheckIndicator = true,
}: Props) {
const [open, setOpen] = React.useState(false);
const selected = options.find((o) => o.value === value);
React.useEffect(() => { if (disabled && open) setOpen(false); }, [disabled, open]);
return (
<Popover open={disabled ? false : open} onOpenChange={(o)=> !disabled && setOpen(o)}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className={cn("w-full justify-between", className)}
disabled={disabled}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
>
{selected ? selected.label : placeholder}
<ChevronsUpDown className="opacity-50 h-4 w-4 shrink-0" />
</Button>
</PopoverTrigger>
<PopoverContent
className="p-0 w-[--radix-popover-trigger-width]"
align="start"
sideOffset={4}
>
<Command shouldFilter>
<CommandInput
className="command-palette-search"
placeholder="Search..."
value={search}
onValueChange={onSearch}
/>
<CommandList>
<CommandEmpty>{emptyLabel}</CommandEmpty>
{options.map((opt) => (
<CommandItem
key={opt.value}
value={
typeof opt.searchText === 'string' && opt.searchText.length > 0
? opt.searchText
: (typeof opt.label === 'string' ? opt.label : opt.value)
}
onSelect={() => {
onChange?.(opt.value);
setOpen(false);
}}
>
{showCheckIndicator && (
<Check
className={cn(
"mr-2 h-4 w-4",
opt.value === value ? "opacity-100" : "opacity-0"
)}
/>
)}
{opt.label}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}