Fixes #21
+35
@@ -13,6 +13,7 @@ import { categories as categoriesApi, getApiError } from '@/lib/api/index'
|
||||
import type { CategoryResponse } from '@/types/api'
|
||||
|
||||
import { CategorySheet } from './category-sheet'
|
||||
import { CategoryViewSheet } from './category-view-sheet'
|
||||
|
||||
export function CategoriesClient() {
|
||||
const [data, setData] = useState<CategoryResponse[]>([])
|
||||
@@ -21,6 +22,8 @@ export function CategoriesClient() {
|
||||
|
||||
const [sheetOpen, setSheetOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<CategoryResponse | null>(null)
|
||||
const [viewOpen, setViewOpen] = useState(false)
|
||||
const [viewing, setViewing] = useState<CategoryResponse | null>(null)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
@@ -49,6 +52,21 @@ export function CategoriesClient() {
|
||||
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>>(() => ({
|
||||
rowKey: 'id',
|
||||
serverSide: false,
|
||||
@@ -99,11 +117,22 @@ export function CategoriesClient() {
|
||||
],
|
||||
rowActions: {
|
||||
actions: () => [
|
||||
{
|
||||
action: 'view',
|
||||
label: 'View category',
|
||||
onClick: openView,
|
||||
},
|
||||
{
|
||||
action: 'edit',
|
||||
label: 'Edit category',
|
||||
onClick: openEdit,
|
||||
},
|
||||
{
|
||||
action: 'cancel',
|
||||
label: 'Delete category',
|
||||
onClick: handleDelete,
|
||||
visible: (target) => target.isActive,
|
||||
},
|
||||
],
|
||||
},
|
||||
}), [openEdit])
|
||||
@@ -135,6 +164,12 @@ export function CategoriesClient() {
|
||||
category={editing}
|
||||
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,
|
||||
} from '@/components/ui/select'
|
||||
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'
|
||||
|
||||
const schema = z.object({
|
||||
@@ -69,11 +69,11 @@ export function ItemSheet({ open, onClose, item, onSuccess }: ItemSheetProps) {
|
||||
if (open) {
|
||||
Promise.all([
|
||||
categoriesApi.list(),
|
||||
brandsApi.list({ page: 1, pageSize: 200 }),
|
||||
brandsApi.list(),
|
||||
uomsApi.list(),
|
||||
]).then(([cats, brandsRes, uomsRes]) => {
|
||||
setCategories(cats)
|
||||
setBrandsList(getPaginatedItems(brandsRes))
|
||||
setBrandsList(brandsRes)
|
||||
setUomsList(uomsRes)
|
||||
}).catch((err) => {
|
||||
toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
|
||||
|
||||
Reference in New Issue
Block a user