Fe changes
This commit is contained in:
+67
@@ -3,12 +3,14 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
|
||||
import { DataTable } from '@/components/shared/data-table'
|
||||
import { ExportButtons } from '@/components/shared/export-buttons'
|
||||
import type { TableConfig } from '@/components/shared/data-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import { getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||
import { exportExcel, exportPdf, fetchAllPages, type ExportColumn } from '@/lib/export'
|
||||
import {
|
||||
productReports,
|
||||
type ProductReportSummary,
|
||||
@@ -71,6 +73,44 @@ const returnsColumns: TableConfig<ProductReturnRow>['columns'] = [
|
||||
},
|
||||
]
|
||||
|
||||
const STOCK_EXPORT: ExportColumn<ProductStockRow>[] = [
|
||||
{ header: 'SKU', value: r => r.sku },
|
||||
{ header: 'Item', value: r => r.name },
|
||||
{ header: 'Brand', value: r => r.brand ?? '' },
|
||||
{ header: 'Category', value: r => r.category },
|
||||
{ header: 'UOM', value: r => r.uom },
|
||||
{ header: 'Warehouse', value: r => r.warehouseQty, numeric: true },
|
||||
{ header: 'Vehicles', value: r => r.vehicleQty, numeric: true },
|
||||
{ header: 'Total', value: r => r.totalQty, numeric: true },
|
||||
{ header: 'Stock Value', value: r => r.stockValue, numeric: true },
|
||||
{ header: 'Low Stock', value: r => (r.isLowStock ? 'Yes' : 'No') },
|
||||
]
|
||||
|
||||
const SALES_EXPORT: ExportColumn<ProductSalesRow>[] = [
|
||||
{ header: 'SKU', value: r => r.sku },
|
||||
{ header: 'Item', value: r => r.name },
|
||||
{ header: 'Orders', value: r => r.orders, numeric: true },
|
||||
{ header: 'Stores', value: r => r.stores, numeric: true },
|
||||
{ header: 'Qty Ordered', value: r => r.qtyOrdered, numeric: true },
|
||||
{ header: 'Qty Fulfilled', value: r => r.qtyFulfilled, numeric: true },
|
||||
{ header: 'Sales', value: r => r.salesValue, numeric: true },
|
||||
{ header: 'Margin', value: r => r.margin, numeric: true },
|
||||
{ header: 'Margin %', value: r => r.marginPercent, numeric: true },
|
||||
]
|
||||
|
||||
const RETURNS_EXPORT: ExportColumn<ProductReturnRow>[] = [
|
||||
{ header: 'SKU', value: r => r.sku },
|
||||
{ header: 'Item', value: r => r.name },
|
||||
{ header: 'Returned Qty', value: r => r.returnedQty, numeric: true },
|
||||
{ header: 'Returned Value', value: r => r.returnedValue, numeric: true },
|
||||
{ header: 'Sold Qty', value: r => r.soldQty, numeric: true },
|
||||
{ header: 'Return %', value: r => r.returnPercent, numeric: true },
|
||||
{
|
||||
header: 'By Reason',
|
||||
value: r => Object.entries(r.qtyByReason).map(([reason, qty]) => `${reason}: ${num(qty)}`).join(', '),
|
||||
},
|
||||
]
|
||||
|
||||
export function ProductReportClient() {
|
||||
const [tab, setTab] = useState<Tab>('stock')
|
||||
const [search, setSearch] = useState('')
|
||||
@@ -128,6 +168,30 @@ export function ProductReportClient() {
|
||||
setTotal(0)
|
||||
}
|
||||
|
||||
async function handleExport(format: 'excel' | 'pdf') {
|
||||
const filters = (p: number, s: number) => ({
|
||||
page: p, pageSize: s, search: search || undefined, from: from || undefined, to: to || undefined,
|
||||
})
|
||||
const range = `${from || 'start'} to ${to || 'today'}`
|
||||
|
||||
if (tab === 'stock') {
|
||||
const rows = await fetchAllPages((p, s) =>
|
||||
productReports.stock({ ...filters(p, s), lowStockOnly }).then(r => ({ items: getPaginatedItems(r), total: r.total })))
|
||||
if (format === 'excel') await exportExcel('product-stock', STOCK_EXPORT, rows)
|
||||
else await exportPdf('product-stock', 'Product Report: Stock', STOCK_EXPORT, rows, lowStockOnly ? 'Low stock only' : undefined)
|
||||
} else if (tab === 'sales') {
|
||||
const rows = await fetchAllPages((p, s) =>
|
||||
productReports.sales(filters(p, s)).then(r => ({ items: getPaginatedItems(r), total: r.total })))
|
||||
if (format === 'excel') await exportExcel('product-sales', SALES_EXPORT, rows)
|
||||
else await exportPdf('product-sales', 'Product Report: Sales', SALES_EXPORT, rows, range)
|
||||
} else {
|
||||
const rows = await fetchAllPages((p, s) =>
|
||||
productReports.returns(filters(p, s)).then(r => ({ items: getPaginatedItems(r), total: r.total })))
|
||||
if (format === 'excel') await exportExcel('product-returns', RETURNS_EXPORT, rows)
|
||||
else await exportPdf('product-returns', 'Product Report: Returns', RETURNS_EXPORT, rows, range)
|
||||
}
|
||||
}
|
||||
|
||||
const common = { serverSide: true, stickyHeader: true, pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] } }
|
||||
|
||||
const stockConfig = useMemo<TableConfig<ProductStockRow>>(
|
||||
@@ -197,6 +261,9 @@ export function ProductReportClient() {
|
||||
Low stock only
|
||||
</Button>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ExportButtons onExport={handleExport} disabled={loading || total === 0} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tab === 'stock' && (
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||||
|
||||
import { DataTable } from '@/components/shared/data-table'
|
||||
import { ExportButtons } from '@/components/shared/export-buttons'
|
||||
import type { TableConfig } from '@/components/shared/data-table'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import { getApiError, getPaginatedItems } from '@/lib/api/index'
|
||||
import { exportExcel, exportPdf, fetchAllPages, type ExportColumn } from '@/lib/export'
|
||||
import { categories } from '@/lib/api/catalogue'
|
||||
import { vehicles, warehouses } from '@/lib/api/master'
|
||||
import {
|
||||
stockReports,
|
||||
type StockExpiryRow,
|
||||
type StockLedgerRow,
|
||||
type StockOverview,
|
||||
} from '@/lib/api/reports'
|
||||
|
||||
type Tab = 'ledger' | 'expiry'
|
||||
|
||||
const TABS: { key: Tab; label: string }[] = [
|
||||
{ key: 'ledger', label: 'Stock Ledger' },
|
||||
{ key: 'expiry', label: 'Expiry' },
|
||||
]
|
||||
|
||||
const ALL = 'all'
|
||||
const EXPIRY_WINDOWS = [7, 30, 60, 90]
|
||||
|
||||
const num = (v: number | null | undefined) =>
|
||||
v == null ? '—' : v.toLocaleString('en-IN', { maximumFractionDigits: 2 })
|
||||
|
||||
const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB')
|
||||
|
||||
const STATUS_CLASS: Record<string, string> = {
|
||||
OK: 'text-emerald-600',
|
||||
Low: 'text-amber-600',
|
||||
Out: 'text-red-600',
|
||||
}
|
||||
|
||||
const ledgerColumns: TableConfig<StockLedgerRow>['columns'] = [
|
||||
{ key: 'name', title: 'Item', fields: [{ key: 'name', primary: true }, { key: 'sku' }], minWidth: 220 },
|
||||
{ key: 'category', title: 'Category', fields: ['category'], width: 140 },
|
||||
{ key: 'uom', title: 'UOM', fields: ['uom'], width: 70 },
|
||||
{ key: 'openingQty', title: 'Opening', fields: [{ key: 'openingQty', transform: v => num(v as number) }], width: 100 },
|
||||
{ key: 'qtyIn', title: 'In', fields: [{ key: 'qtyIn', transform: v => num(v as number) }], width: 90 },
|
||||
{ key: 'qtyOut', title: 'Out', fields: [{ key: 'qtyOut', transform: v => num(v as number) }], width: 90 },
|
||||
{ key: 'closingQty', title: 'Closing', fields: [{ key: 'closingQty', transform: v => num(v as number) }], width: 100 },
|
||||
{ key: 'currentQty', title: 'On Hand', fields: [{ key: 'currentQty', transform: v => num(v as number) }], width: 100 },
|
||||
{ key: 'stockValue', title: 'Value', fields: [{ key: 'stockValue', transform: v => num(v as number) }], width: 110 },
|
||||
{
|
||||
key: 'status',
|
||||
title: 'Status',
|
||||
fields: ['status'],
|
||||
width: 90,
|
||||
cellRender: row => <span className={`text-sm font-medium ${STATUS_CLASS[row.status] ?? ''}`}>{row.status}</span>,
|
||||
},
|
||||
]
|
||||
|
||||
const expiryColumns: TableConfig<StockExpiryRow>['columns'] = [
|
||||
{ key: 'name', title: 'Item', fields: [{ key: 'name', primary: true }, { key: 'sku' }], minWidth: 220 },
|
||||
{ key: 'batchNumber', title: 'Batch', fields: [{ key: 'batchNumber', transform: v => (v as string | null) ?? '—' }], width: 130 },
|
||||
{ key: 'locationName', title: 'Location', fields: [{ key: 'locationName', primary: true }, { key: 'locationType' }], width: 150 },
|
||||
{ key: 'quantity', title: 'Qty', fields: [{ key: 'quantity', transform: v => num(v as number) }], width: 90 },
|
||||
{ key: 'value', title: 'Value', fields: [{ key: 'value', transform: v => num(v as number) }], width: 110 },
|
||||
{ key: 'expiryDate', title: 'Expiry', fields: [{ key: 'expiryDate', transform: v => fmtDate(v as string) }], width: 110 },
|
||||
{
|
||||
key: 'daysLeft',
|
||||
title: 'Days Left',
|
||||
fields: ['daysLeft'],
|
||||
width: 110,
|
||||
cellRender: row => (
|
||||
<span className={`text-sm font-medium ${row.daysLeft < 0 ? 'text-red-600' : row.daysLeft <= 7 ? 'text-amber-600' : ''}`}>
|
||||
{row.daysLeft < 0 ? `Expired ${-row.daysLeft}d ago` : `${row.daysLeft}d`}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const LEDGER_EXPORT: ExportColumn<StockLedgerRow>[] = [
|
||||
{ header: 'SKU', value: r => r.sku },
|
||||
{ header: 'Item', value: r => r.name },
|
||||
{ header: 'Category', value: r => r.category },
|
||||
{ header: 'UOM', value: r => r.uom },
|
||||
{ header: 'Opening', value: r => r.openingQty, numeric: true },
|
||||
{ header: 'In', value: r => r.qtyIn, numeric: true },
|
||||
{ header: 'Out', value: r => r.qtyOut, numeric: true },
|
||||
{ header: 'Closing', value: r => r.closingQty, numeric: true },
|
||||
{ header: 'On Hand', value: r => r.currentQty, numeric: true },
|
||||
{ header: 'Value', value: r => r.stockValue, numeric: true },
|
||||
{ header: 'Status', value: r => r.status },
|
||||
]
|
||||
|
||||
const EXPIRY_EXPORT: ExportColumn<StockExpiryRow>[] = [
|
||||
{ header: 'SKU', value: r => r.sku },
|
||||
{ header: 'Item', value: r => r.name },
|
||||
{ header: 'Batch', value: r => r.batchNumber ?? '' },
|
||||
{ header: 'Location', value: r => r.locationName },
|
||||
{ header: 'Qty', value: r => r.quantity, numeric: true },
|
||||
{ header: 'Value', value: r => r.value, numeric: true },
|
||||
{ header: 'Expiry', value: r => fmtDate(r.expiryDate) },
|
||||
{ header: 'Days Left', value: r => r.daysLeft, numeric: true },
|
||||
]
|
||||
|
||||
export function StockReportClient() {
|
||||
const [tab, setTab] = useState<Tab>('ledger')
|
||||
const [search, setSearch] = useState('')
|
||||
const [from, setFrom] = useState('')
|
||||
const [to, setTo] = useState('')
|
||||
const [locationId, setLocationId] = useState(ALL)
|
||||
const [categoryId, setCategoryId] = useState(ALL)
|
||||
const [status, setStatus] = useState(ALL)
|
||||
const [expiryDays, setExpiryDays] = useState(30)
|
||||
|
||||
const [locationOptions, setLocationOptions] = useState<{ id: string; label: string }[]>([])
|
||||
const [categoryOptions, setCategoryOptions] = useState<{ id: string; name: string }[]>([])
|
||||
const [overview, setOverview] = useState<StockOverview | null>(null)
|
||||
const [ledgerData, setLedgerData] = useState<StockLedgerRow[]>([])
|
||||
const [expiryData, setExpiryData] = useState<StockExpiryRow[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([warehouses.list({ pageSize: 200 }), vehicles.list({ pageSize: 200 }), categories.list()])
|
||||
.then(([w, v, c]) => {
|
||||
setLocationOptions([
|
||||
...getPaginatedItems(w).map(x => ({ id: x.id, label: `Warehouse · ${x.name}` })),
|
||||
...getPaginatedItems(v).map(x => ({ id: x.id, label: `Vehicle · ${x.numberPlate}` })),
|
||||
])
|
||||
setCategoryOptions(c.map(x => ({ id: x.id, name: x.name })))
|
||||
})
|
||||
.catch(err => toast.add({ title: 'Failed to load filters', description: getApiError(err), type: 'error' }))
|
||||
}, [])
|
||||
|
||||
const location = locationId === ALL ? undefined : locationId
|
||||
|
||||
const ledgerParams = useCallback(
|
||||
(p: number, s: number) => ({
|
||||
page: p,
|
||||
pageSize: s,
|
||||
search: search || undefined,
|
||||
locationId: location,
|
||||
from: from || undefined,
|
||||
to: to || undefined,
|
||||
categoryId: categoryId === ALL ? undefined : categoryId,
|
||||
status: status === ALL ? undefined : (status as 'low' | 'out'),
|
||||
}),
|
||||
[search, location, from, to, categoryId, status],
|
||||
)
|
||||
|
||||
const expiryParams = useCallback(
|
||||
(p: number, s: number) => ({ page: p, pageSize: s, search: search || undefined, locationId: location, days: expiryDays }),
|
||||
[search, location, expiryDays],
|
||||
)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
if (tab === 'ledger') {
|
||||
const res = await stockReports.ledger(ledgerParams(page, pageSize))
|
||||
setLedgerData(getPaginatedItems(res))
|
||||
setTotal(res.total)
|
||||
} else {
|
||||
const res = await stockReports.expiry(expiryParams(page, pageSize))
|
||||
setExpiryData(getPaginatedItems(res))
|
||||
setTotal(res.total)
|
||||
}
|
||||
} catch (err) {
|
||||
toast.add({ title: 'Failed to load stock report', description: getApiError(err), type: 'error' })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [tab, page, pageSize, ledgerParams, expiryParams])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
|
||||
useEffect(() => {
|
||||
stockReports
|
||||
.overview({ locationId: location, expiryDays })
|
||||
.then(setOverview)
|
||||
.catch(err => toast.add({ title: 'Failed to load summary', description: getApiError(err), type: 'error' }))
|
||||
}, [location, expiryDays])
|
||||
|
||||
function changeTab(next: Tab) {
|
||||
setTab(next)
|
||||
setPage(1)
|
||||
setTotal(0)
|
||||
}
|
||||
|
||||
async function handleExport(format: 'excel' | 'pdf') {
|
||||
const locationLabel = locationOptions.find(o => o.id === locationId)?.label ?? 'All locations'
|
||||
if (tab === 'ledger') {
|
||||
const rows = await fetchAllPages((p, s) =>
|
||||
stockReports.ledger(ledgerParams(p, s)).then(r => ({ items: getPaginatedItems(r), total: r.total })),
|
||||
)
|
||||
const range = `${from || 'last 30 days'} to ${to || 'today'}`
|
||||
if (format === 'excel') await exportExcel('stock-ledger', LEDGER_EXPORT, rows)
|
||||
else await exportPdf('stock-ledger', 'Stock Ledger', LEDGER_EXPORT, rows, `${locationLabel} · ${range}`)
|
||||
} else {
|
||||
const rows = await fetchAllPages((p, s) =>
|
||||
stockReports.expiry(expiryParams(p, s)).then(r => ({ items: getPaginatedItems(r), total: r.total })),
|
||||
)
|
||||
if (format === 'excel') await exportExcel('stock-expiry', EXPIRY_EXPORT, rows)
|
||||
else await exportPdf('stock-expiry', 'Stock Expiry', EXPIRY_EXPORT, rows, `${locationLabel} · within ${expiryDays} days`)
|
||||
}
|
||||
}
|
||||
|
||||
const common = { serverSide: true, stickyHeader: true, pagination: { enabled: true, pageSize, pageSizeOptions: [10, 20, 50] } }
|
||||
|
||||
const ledgerConfig = useMemo<TableConfig<StockLedgerRow>>(
|
||||
() => ({ ...common, rowKey: r => r.itemId, emptyMessage: 'No products found.', columns: ledgerColumns }),
|
||||
[pageSize],
|
||||
)
|
||||
const expiryConfig = useMemo<TableConfig<StockExpiryRow>>(
|
||||
() => ({
|
||||
...common,
|
||||
rowKey: r => `${r.batchId}-${r.locationId}`,
|
||||
emptyMessage: 'No batches expiring in this window.',
|
||||
columns: expiryColumns,
|
||||
}),
|
||||
[pageSize],
|
||||
)
|
||||
|
||||
const onServerSideChange = ({ page: p }: { page: { page: number; pageSize: number } }) => {
|
||||
setPage(p.page)
|
||||
setPageSize(p.pageSize)
|
||||
}
|
||||
|
||||
const cards: [string, string][] = overview
|
||||
? [
|
||||
['Products in stock', num(overview.totalSkus)],
|
||||
['Total qty', num(overview.totalQty)],
|
||||
['Stock value', num(overview.totalValue)],
|
||||
['Low stock', num(overview.lowStockCount)],
|
||||
['Out of stock', num(overview.outOfStockCount)],
|
||||
[`Expiring in ${expiryDays}d`, num(overview.expiringSoonCount)],
|
||||
['Expired', num(overview.expiredCount)],
|
||||
]
|
||||
: []
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-foreground">Stock Report</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Opening, in, out and closing stock across all locations or one warehouse or vehicle, plus batches nearing expiry.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{overview && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3">
|
||||
{cards.map(([label, value]) => (
|
||||
<Card key={label} className="rounded-3xl border-border/60">
|
||||
<CardContent className="space-y-1">
|
||||
<CardDescription>{label}</CardDescription>
|
||||
<CardTitle className="text-2xl">{value}</CardTitle>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{TABS.map(t => (
|
||||
<Button key={t.key} size="sm" variant={tab === t.key ? 'default' : 'outline'} onClick={() => changeTab(t.key)}>
|
||||
{t.label}
|
||||
</Button>
|
||||
))}
|
||||
<Select value={locationId} onValueChange={v => { setLocationId(v ?? ALL); setPage(1) }}>
|
||||
<SelectTrigger className="w-56"><SelectValue placeholder="Location" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL}>All locations</SelectItem>
|
||||
{locationOptions.map(o => <SelectItem key={o.id} value={o.id}>{o.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
className="w-56"
|
||||
placeholder="Search name or SKU"
|
||||
value={search}
|
||||
onChange={e => { setSearch(e.target.value); setPage(1) }}
|
||||
/>
|
||||
{tab === 'ledger' ? (
|
||||
<>
|
||||
<Select value={categoryId} onValueChange={v => { setCategoryId(v ?? ALL); setPage(1) }}>
|
||||
<SelectTrigger className="w-44"><SelectValue placeholder="Category" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL}>All categories</SelectItem>
|
||||
{categoryOptions.map(c => <SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={status} onValueChange={v => { setStatus(v ?? ALL); setPage(1) }}>
|
||||
<SelectTrigger className="w-40"><SelectValue placeholder="Status" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={ALL}>All statuses</SelectItem>
|
||||
<SelectItem value="low">Low stock</SelectItem>
|
||||
<SelectItem value="out">Out of stock</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input className="w-40" type="date" value={from} onChange={e => { setFrom(e.target.value); setPage(1) }} />
|
||||
<Input className="w-40" type="date" value={to} onChange={e => { setTo(e.target.value); setPage(1) }} />
|
||||
</>
|
||||
) : (
|
||||
<Select value={String(expiryDays)} onValueChange={v => { setExpiryDays(Number(v)); setPage(1) }}>
|
||||
<SelectTrigger className="w-44"><SelectValue placeholder="Window" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{EXPIRY_WINDOWS.map(d => <SelectItem key={d} value={String(d)}>Within {d} days</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<ExportButtons onExport={handleExport} disabled={loading || total === 0} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tab === 'ledger' && (
|
||||
<DataTable config={ledgerConfig} data={ledgerData} loading={loading} totalCount={total} onServerSideChange={onServerSideChange} />
|
||||
)}
|
||||
{tab === 'expiry' && (
|
||||
<DataTable config={expiryConfig} data={expiryData} loading={loading} totalCount={total} onServerSideChange={onServerSideChange} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { StockReportClient } from './_components/stock-report-client'
|
||||
|
||||
export default function StockReportPage() {
|
||||
return <StockReportClient />
|
||||
}
|
||||
Reference in New Issue
Block a user