new updates
This commit is contained in:
@@ -20,10 +20,19 @@ type Props = {
|
||||
data: number[]
|
||||
backgroundColor?: string | string[]
|
||||
}>
|
||||
/** Container height in px — defaults to the original 260 so existing callers are unaffected. */
|
||||
height?: number
|
||||
/** Fraction of each category's slot a bar occupies (Chart.js default 0.9) — raise for thicker bars. */
|
||||
barPercentage?: number
|
||||
/** Fraction of the axis each category slot occupies (Chart.js default 0.8) — raise to shrink gaps between categories. */
|
||||
categoryPercentage?: number
|
||||
}
|
||||
|
||||
export default function BarChart({ labels, datasets }: Props) {
|
||||
const data = { labels, datasets }
|
||||
export default function BarChart({ labels, datasets, height = 260, barPercentage, categoryPercentage }: Props) {
|
||||
const data = {
|
||||
labels,
|
||||
datasets: datasets.map((d) => ({ ...d, barPercentage, categoryPercentage })),
|
||||
}
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
@@ -33,7 +42,7 @@ export default function BarChart({ labels, datasets }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", minHeight: 260 }}>
|
||||
<div style={{ width: "100%", height }}>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Search, SlidersHorizontal, X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
|
||||
interface FilterBarProps {
|
||||
search: string
|
||||
onSearchChange: (value: string) => void
|
||||
searchPlaceholder?: string
|
||||
searchAriaLabel?: string
|
||||
/** Rendered to the left of the search box — status tabs/pills, typically. */
|
||||
children?: React.ReactNode
|
||||
/** Content shown inside the "Advanced" popover. Omit to hide the Advanced button entirely. */
|
||||
advanced?: React.ReactNode
|
||||
/** Shows a dot on the Advanced button when any advanced filter is set. */
|
||||
advancedActive?: boolean
|
||||
/** Shown as a "Clear advanced filters" action inside the popover when provided. */
|
||||
onClearAdvanced?: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard list-page filter bar: search box + optional left-side content (status
|
||||
* tabs/pills) + an optional "Advanced" popover for filters that don't fit inline
|
||||
* (customer, warehouse, department, date range, ...). One shape, reused across
|
||||
* Sales, HRM, and Warehouses list pages instead of each rolling its own.
|
||||
*/
|
||||
export function FilterBar({
|
||||
search, onSearchChange, searchPlaceholder = "Search…", searchAriaLabel,
|
||||
children, advanced, advancedActive, onClearAdvanced, className,
|
||||
}: FilterBarProps) {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-3 lg:flex-row lg:items-center", className)}>
|
||||
{children}
|
||||
|
||||
<div className="flex flex-1 flex-col gap-3 lg:flex-row lg:items-center lg:justify-end">
|
||||
<div className="relative w-full lg:max-w-sm">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
className="h-10 pl-9 pr-9"
|
||||
aria-label={searchAriaLabel ?? searchPlaceholder}
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSearchChange("")}
|
||||
aria-label="Clear search"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{advanced && (
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button variant="outline" size="sm" className="relative shrink-0">
|
||||
<SlidersHorizontal className="size-4" />
|
||||
Advanced
|
||||
{advancedActive && (
|
||||
<span className="absolute -right-1 -top-1 size-2 rounded-full bg-primary" aria-hidden="true" />
|
||||
)}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<PopoverContent align="end" className="w-72 p-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
{advanced}
|
||||
{onClearAdvanced && (
|
||||
<Button variant="ghost" size="sm" onClick={onClearAdvanced} className="self-start">
|
||||
Clear advanced filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { type LucideIcon } from "lucide-react"
|
||||
|
||||
import { StatCard } from "@/components/ui/stat-card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
export interface KpiTileSpec {
|
||||
label: string
|
||||
value: number
|
||||
icon?: LucideIcon
|
||||
}
|
||||
|
||||
/**
|
||||
* A small row of KPI tiles for a list page — reuses the same GET /dashboard/stats
|
||||
* aggregate the Dashboard home page renders (see hooks/use-dashboard-stats.ts), just
|
||||
* picking out the one or two counts relevant to this page. Pass `null` while stats are
|
||||
* still loading to render skeleton placeholders instead of a layout jump.
|
||||
*/
|
||||
export function KpiRow({ tiles }: { tiles: KpiTileSpec[] | null }) {
|
||||
return (
|
||||
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
{tiles
|
||||
? tiles.map((tile) => (
|
||||
<StatCard key={tile.label} label={tile.label} value={tile.value} icon={tile.icon} />
|
||||
))
|
||||
: Array.from({ length: 2 }).map((_, i) => <Skeleton key={i} className="h-[90px] rounded-xl" />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user