Add category viewing
This commit is contained in:
+35
@@ -13,6 +13,7 @@ import { categories as categoriesApi, getApiError } from '@/lib/api/index'
|
|||||||
import type { CategoryResponse } from '@/types/api'
|
import type { CategoryResponse } from '@/types/api'
|
||||||
|
|
||||||
import { CategorySheet } from './category-sheet'
|
import { CategorySheet } from './category-sheet'
|
||||||
|
import { CategoryViewSheet } from './category-view-sheet'
|
||||||
|
|
||||||
export function CategoriesClient() {
|
export function CategoriesClient() {
|
||||||
const [data, setData] = useState<CategoryResponse[]>([])
|
const [data, setData] = useState<CategoryResponse[]>([])
|
||||||
@@ -21,6 +22,8 @@ export function CategoriesClient() {
|
|||||||
|
|
||||||
const [sheetOpen, setSheetOpen] = useState(false)
|
const [sheetOpen, setSheetOpen] = useState(false)
|
||||||
const [editing, setEditing] = useState<CategoryResponse | null>(null)
|
const [editing, setEditing] = useState<CategoryResponse | null>(null)
|
||||||
|
const [viewOpen, setViewOpen] = useState(false)
|
||||||
|
const [viewing, setViewing] = useState<CategoryResponse | null>(null)
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -49,6 +52,21 @@ export function CategoriesClient() {
|
|||||||
setSheetOpen(true)
|
setSheetOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openView(category: CategoryResponse) {
|
||||||
|
setViewing(category)
|
||||||
|
setViewOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(category: CategoryResponse) {
|
||||||
|
try {
|
||||||
|
await categoriesApi.remove(category.id)
|
||||||
|
toast.add({ title: 'Category deactivated', type: 'success' })
|
||||||
|
fetchData()
|
||||||
|
} catch (err) {
|
||||||
|
toast.add({ title: 'Failed to deactivate category', description: getApiError(err), type: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tableConfig = useMemo<TableConfig<CategoryResponse>>(() => ({
|
const tableConfig = useMemo<TableConfig<CategoryResponse>>(() => ({
|
||||||
rowKey: 'id',
|
rowKey: 'id',
|
||||||
serverSide: false,
|
serverSide: false,
|
||||||
@@ -99,11 +117,22 @@ export function CategoriesClient() {
|
|||||||
],
|
],
|
||||||
rowActions: {
|
rowActions: {
|
||||||
actions: () => [
|
actions: () => [
|
||||||
|
{
|
||||||
|
action: 'view',
|
||||||
|
label: 'View category',
|
||||||
|
onClick: openView,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
action: 'edit',
|
action: 'edit',
|
||||||
label: 'Edit category',
|
label: 'Edit category',
|
||||||
onClick: openEdit,
|
onClick: openEdit,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
action: 'cancel',
|
||||||
|
label: 'Delete category',
|
||||||
|
onClick: handleDelete,
|
||||||
|
visible: (target) => target.isActive,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}), [openEdit])
|
}), [openEdit])
|
||||||
@@ -135,6 +164,12 @@ export function CategoriesClient() {
|
|||||||
category={editing}
|
category={editing}
|
||||||
onSuccess={() => fetchData()}
|
onSuccess={() => fetchData()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CategoryViewSheet
|
||||||
|
open={viewOpen}
|
||||||
|
onClose={() => setViewOpen(false)}
|
||||||
|
category={viewing}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import type { CategoryResponse } from '@/types/api'
|
||||||
|
|
||||||
|
const fmtDate = (v: string) =>
|
||||||
|
new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
|
||||||
|
|
||||||
|
interface CategoryViewSheetProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
category: CategoryResponse | null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Row({ label, value }: { label: string; value: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between py-2 text-sm border-b last:border-0">
|
||||||
|
<span className="text-muted-foreground">{label}</span>
|
||||||
|
<span className="font-medium text-right">{value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategoryViewSheet({ open, onClose, category }: CategoryViewSheetProps) {
|
||||||
|
if (!category) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(v) => !v && onClose()}>
|
||||||
|
<DialogContent className="overflow-y-auto sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Category Details</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Row label="Name" value={category.name} />
|
||||||
|
<Row label="Status" value={<Badge variant={category.isActive ? 'default' : 'secondary'}>{category.isActive ? 'Active' : 'Inactive'}</Badge>} />
|
||||||
|
<Row label="Created At" value={fmtDate(category.createdAt)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">Sub-categories ({category.subCategories.length})</p>
|
||||||
|
{category.subCategories.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">No sub-categories yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{category.subCategories.map((sub) => (
|
||||||
|
<div key={sub.id} className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
|
||||||
|
<span>{sub.name}</span>
|
||||||
|
<Badge variant={sub.isActive ? 'default' : 'secondary'}>{sub.isActive ? 'Active' : 'Inactive'}</Badge>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { toast } from '@/components/ui/toast'
|
import { toast } from '@/components/ui/toast'
|
||||||
import { items as itemsApi, categories as categoriesApi, brands as brandsApi, uoms as uomsApi, getApiError, getPaginatedItems } from '@/lib/api/index'
|
import { items as itemsApi, categories as categoriesApi, brands as brandsApi, uoms as uomsApi, getApiError } from '@/lib/api/index'
|
||||||
import type { ItemSummaryResponse, CategoryResponse, BrandResponse, UomResponse } from '@/types/api'
|
import type { ItemSummaryResponse, CategoryResponse, BrandResponse, UomResponse } from '@/types/api'
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
@@ -69,11 +69,11 @@ export function ItemSheet({ open, onClose, item, onSuccess }: ItemSheetProps) {
|
|||||||
if (open) {
|
if (open) {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
categoriesApi.list(),
|
categoriesApi.list(),
|
||||||
brandsApi.list({ page: 1, pageSize: 200 }),
|
brandsApi.list(),
|
||||||
uomsApi.list(),
|
uomsApi.list(),
|
||||||
]).then(([cats, brandsRes, uomsRes]) => {
|
]).then(([cats, brandsRes, uomsRes]) => {
|
||||||
setCategories(cats)
|
setCategories(cats)
|
||||||
setBrandsList(getPaginatedItems(brandsRes))
|
setBrandsList(brandsRes)
|
||||||
setUomsList(uomsRes)
|
setUomsList(uomsRes)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
|
toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
|
||||||
|
|||||||
Reference in New Issue
Block a user