toggle screen for product configurations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { AlertTriangle, ArrowLeft, Plus, Save, Trash2 } from "lucide-react"
|
||||
@@ -13,7 +13,8 @@ import { warehousesApi } from "@/lib/api/warehouses"
|
||||
import { errorMessage, fieldErrors } from "@/lib/error-map"
|
||||
import { validateConversionLine, validateItemForm, validateReorderLine } from "@/lib/validations/master-data"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Item, ItemReorderSetting, ItemType, TrackingMode, UomConversion } from "@/types/master-data"
|
||||
import { useProductConfig } from "@/hooks/use-product-config"
|
||||
import { Category, Item, ItemReorderSetting, ItemType, TrackingMode, UomConversion } from "@/types/master-data"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
@@ -49,10 +50,12 @@ export default function ItemDetailPage() {
|
||||
const params = useParams<{ id: string }>()
|
||||
const router = useRouter()
|
||||
const itemId = Number(params.id)
|
||||
const { config } = useProductConfig()
|
||||
const subcategoriesEnabled = !!config?.subcategories
|
||||
|
||||
const [item, setItem] = useState<Item | null>(null)
|
||||
const [etag, setEtag] = useState<string | null>(null)
|
||||
const [categories, setCategories] = useState<{ categoryId: number; name: string }[]>([])
|
||||
const [categories, setCategories] = useState<Category[]>([])
|
||||
const [uoms, setUoms] = useState<{ uomId: number; name: string }[]>([])
|
||||
const [vendors, setVendors] = useState<{ vendorId: number; code: string; name: string }[]>([])
|
||||
const [warehouses, setWarehouses] = useState<{ warehouseId: number; code: string; name: string }[]>([])
|
||||
@@ -63,6 +66,7 @@ export default function ItemDetailPage() {
|
||||
const [name, setName] = useState("")
|
||||
const [description, setDescription] = useState("")
|
||||
const [categoryId, setCategoryId] = useState<number | null>(null)
|
||||
const [subCategoryId, setSubCategoryId] = useState<number | null>(null)
|
||||
const [baseUomId, setBaseUomId] = useState<number | null>(null)
|
||||
const [defaultVendorId, setDefaultVendorId] = useState<number | null>(null)
|
||||
const [itemType, setItemType] = useState<ItemType>("Stocked")
|
||||
@@ -117,7 +121,7 @@ export default function ItemDetailPage() {
|
||||
useEffect(() => {
|
||||
if (!Number.isFinite(itemId)) return
|
||||
load()
|
||||
Promise.all([categoriesApi.list(), uomsApi.list(), vendorsApi.list({ pageSize: 200 }), warehousesApi.list()])
|
||||
Promise.all([categoriesApi.list({ pageSize: 200 }), uomsApi.list(), vendorsApi.list({ pageSize: 200 }), warehousesApi.list()])
|
||||
.then(([cat, uo, ve, wh]) => {
|
||||
setCategories(cat.items)
|
||||
setUoms(uo.items)
|
||||
@@ -128,10 +132,42 @@ export default function ItemDetailPage() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemId])
|
||||
|
||||
// Once both the item and the full category list are in, split a subcategory
|
||||
// item's categoryId into its top-level Category + Subcategory pair for the UI.
|
||||
useEffect(() => {
|
||||
if (!item || categories.length === 0) return
|
||||
if (!subcategoriesEnabled) {
|
||||
setSubCategoryId(null)
|
||||
return
|
||||
}
|
||||
const current = categories.find((c) => c.categoryId === item.categoryId)
|
||||
if (current && current.parentId !== null) {
|
||||
setCategoryId(current.parentId)
|
||||
setSubCategoryId(current.categoryId)
|
||||
} else {
|
||||
setSubCategoryId(null)
|
||||
}
|
||||
}, [item, categories, subcategoriesEnabled])
|
||||
|
||||
const topCategories = useMemo(
|
||||
() => (subcategoriesEnabled ? categories.filter((c) => c.parentId === null) : categories),
|
||||
[categories, subcategoriesEnabled]
|
||||
)
|
||||
const subCategoryOptions = useMemo(
|
||||
() => categories.filter((c) => c.parentId === categoryId),
|
||||
[categories, categoryId]
|
||||
)
|
||||
const effectiveCategoryId = subcategoriesEnabled ? (subCategoryId ?? categoryId) : categoryId
|
||||
|
||||
function handleCategoryChange(value: number | null) {
|
||||
setCategoryId(value)
|
||||
setSubCategoryId(null)
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!item || !etag) return
|
||||
setSaveError(null)
|
||||
const nextErrors = validateItemForm({ sku, name, categoryId, baseUomId })
|
||||
const nextErrors = validateItemForm({ sku, name, categoryId: effectiveCategoryId, baseUomId })
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) return
|
||||
|
||||
@@ -139,7 +175,7 @@ export default function ItemDetailPage() {
|
||||
try {
|
||||
const result = await itemsApi.update(
|
||||
item.itemId,
|
||||
{ sku, name, description: description || null, categoryId: categoryId as number, baseUomId: baseUomId as number, defaultVendorId, itemType, trackingMode, taxClass: taxClass || null },
|
||||
{ sku, name, description: description || null, categoryId: effectiveCategoryId as number, baseUomId: baseUomId as number, defaultVendorId, itemType, trackingMode, taxClass: taxClass || null },
|
||||
etag
|
||||
)
|
||||
applyItem(result.data)
|
||||
@@ -339,12 +375,12 @@ export default function ItemDetailPage() {
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Category</Label>
|
||||
<Select<number | null> value={categoryId} onValueChange={setCategoryId} disabled={conflict}>
|
||||
<Select<number | null> value={categoryId} onValueChange={handleCategoryChange} disabled={conflict}>
|
||||
<SelectTrigger className="h-12! w-full text-base" aria-invalid={!!errors.categoryId}>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((c) => (
|
||||
{topCategories.map((c) => (
|
||||
<SelectItem key={c.categoryId} value={c.categoryId} className="text-base">
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
@@ -353,6 +389,27 @@ export default function ItemDetailPage() {
|
||||
</Select>
|
||||
<FieldError errors={[errors.categoryId ? { message: errors.categoryId } : undefined]} />
|
||||
</div>
|
||||
{subcategoriesEnabled && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Subcategory (optional)</Label>
|
||||
<Select<number | null>
|
||||
value={subCategoryId}
|
||||
onValueChange={setSubCategoryId}
|
||||
disabled={conflict || subCategoryOptions.length === 0}
|
||||
>
|
||||
<SelectTrigger className="h-12! w-full text-base">
|
||||
<SelectValue placeholder={subCategoryOptions.length === 0 ? "No subcategories" : "Select subcategory"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{subCategoryOptions.map((c) => (
|
||||
<SelectItem key={c.categoryId} value={c.categoryId} className="text-base">
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Base UOM</Label>
|
||||
<Select<number | null> value={baseUomId} onValueChange={setBaseUomId} disabled={conflict}>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { categoriesApi } from "@/lib/api/categories"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { validateCategoryName } from "@/lib/validations/master-data"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useProductConfig } from "@/hooks/use-product-config"
|
||||
import { PaginationMeta } from "@/types/common"
|
||||
import { Category } from "@/types/master-data"
|
||||
|
||||
@@ -26,10 +27,17 @@ type SortOrder = "asc" | "desc"
|
||||
const PAGE_SIZE = 5
|
||||
|
||||
export default function CategoriesPage() {
|
||||
const { config } = useProductConfig()
|
||||
const subcategoriesEnabled = !!config?.subcategories
|
||||
|
||||
const [categories, setCategories] = useState<Category[] | null>(null)
|
||||
const [pagination, setPagination] = useState<PaginationMeta | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Unfiltered/unpaginated copy used to populate the parent-category picker and to
|
||||
// resolve a row's parent name regardless of which page it's showing.
|
||||
const [allCategories, setAllCategories] = useState<Category[]>([])
|
||||
|
||||
const [searchInput, setSearchInput] = useState("")
|
||||
const [search, setSearch] = useState("")
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>("asc")
|
||||
@@ -38,6 +46,7 @@ export default function CategoriesPage() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<Category | null>(null)
|
||||
const [name, setName] = useState("")
|
||||
const [parentId, setParentId] = useState<number | null>(null)
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [deletingId, setDeletingId] = useState<number | null>(null)
|
||||
@@ -62,13 +71,28 @@ export default function CategoriesPage() {
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}
|
||||
|
||||
function loadAllCategories() {
|
||||
categoriesApi
|
||||
.list({ pageSize: 500 })
|
||||
.then((res) => setAllCategories(res.items))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(load, [search, sortOrder, page])
|
||||
useEffect(loadAllCategories, [])
|
||||
|
||||
const hasFilters = search.trim().length > 0
|
||||
const parentOptions = allCategories.filter((c) => c.parentId === null && c.categoryId !== editing?.categoryId)
|
||||
|
||||
function parentName(category: Category): string {
|
||||
if (category.parentId === null) return "—"
|
||||
return allCategories.find((c) => c.categoryId === category.parentId)?.name ?? `#${category.parentId}`
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
setEditing(null)
|
||||
setName("")
|
||||
setParentId(null)
|
||||
setErrors({})
|
||||
setOpen(true)
|
||||
}
|
||||
@@ -76,6 +100,7 @@ export default function CategoriesPage() {
|
||||
function openEditDialog(category: Category) {
|
||||
setEditing(category)
|
||||
setName(category.name)
|
||||
setParentId(category.parentId)
|
||||
setErrors({})
|
||||
setOpen(true)
|
||||
}
|
||||
@@ -88,14 +113,16 @@ export default function CategoriesPage() {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const category = editing
|
||||
? await categoriesApi.update(editing.categoryId, { name })
|
||||
: await categoriesApi.create({ name })
|
||||
? await categoriesApi.update(editing.categoryId, { name, parentId: subcategoriesEnabled ? parentId : null })
|
||||
: await categoriesApi.create({ name, parentId: subcategoriesEnabled ? parentId : null })
|
||||
toast.success(editing ? "Category updated" : "Category created", category.name)
|
||||
setOpen(false)
|
||||
setName("")
|
||||
setParentId(null)
|
||||
setEditing(null)
|
||||
setErrors({})
|
||||
load()
|
||||
loadAllCategories()
|
||||
} catch (err) {
|
||||
setErrors({ name: errorMessage(err) })
|
||||
toast.error(editing ? "Could not update category" : "Could not create category", errorMessage(err))
|
||||
@@ -135,7 +162,9 @@ export default function CategoriesPage() {
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader className="items-center text-center">
|
||||
<DialogTitle>{editing ? "Edit category" : "New category"}</DialogTitle>
|
||||
<DialogDescription>Give the category a name.</DialogDescription>
|
||||
<DialogDescription>
|
||||
{subcategoriesEnabled ? "Give the category a name and, optionally, a parent." : "Give the category a name."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!errors.name}>
|
||||
@@ -143,6 +172,23 @@ export default function CategoriesPage() {
|
||||
<Input id="cat-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Fasteners" aria-invalid={!!errors.name} />
|
||||
<FieldError errors={[errors.name ? { message: errors.name } : undefined]} />
|
||||
</Field>
|
||||
{subcategoriesEnabled && (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="cat-parent">Parent category (optional)</FieldLabel>
|
||||
<Select<number | null> value={parentId} onValueChange={setParentId}>
|
||||
<SelectTrigger id="cat-parent" className="w-full">
|
||||
<SelectValue placeholder="None (top-level category)" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{parentOptions.map((c) => (
|
||||
<SelectItem key={c.categoryId} value={c.categoryId}>
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
)}
|
||||
</FieldGroup>
|
||||
<div className="flex justify-center gap-3 pt-2">
|
||||
<Button variant="outline" className="min-w-36" onClick={() => setOpen(false)} disabled={submitting}>
|
||||
@@ -206,6 +252,7 @@ export default function CategoriesPage() {
|
||||
<TableRow className="hover:bg-indigo-50">
|
||||
<TableHead className="h-12 px-3 text-sm text-indigo-700">ID</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm text-indigo-700">Name</TableHead>
|
||||
{subcategoriesEnabled && <TableHead className="h-12 px-3 text-sm text-indigo-700">Parent</TableHead>}
|
||||
<TableHead className="h-12 px-3 text-sm text-indigo-700">Created At</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm text-indigo-700">Actions</TableHead>
|
||||
</TableRow>
|
||||
@@ -215,6 +262,9 @@ export default function CategoriesPage() {
|
||||
<TableRow key={c.categoryId}>
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">#{c.categoryId}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 font-medium">{c.name}</TableCell>
|
||||
{subcategoriesEnabled && (
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{parentName(c)}</TableCell>
|
||||
)}
|
||||
<TableCell className="px-3 py-3.5 text-muted-foreground">{new Date(c.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft, Boxes, Package, Tag } from "lucide-react"
|
||||
|
||||
import { productConfigApi } from "@/lib/api/product-config"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ProductConfig } from "@/types/settings"
|
||||
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
interface ToggleField {
|
||||
key: keyof ProductConfig
|
||||
label: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface Section {
|
||||
title: string
|
||||
icon: typeof Package
|
||||
fields: ToggleField[]
|
||||
}
|
||||
|
||||
const SECTIONS: Section[] = [
|
||||
{
|
||||
title: "Product Capabilities",
|
||||
icon: Package,
|
||||
fields: [
|
||||
{
|
||||
key: "subcategories",
|
||||
label: "Subcategories",
|
||||
description: "Category hierarchy includes a subcategory level. Off ⇒ products attach directly to a main category.",
|
||||
},
|
||||
{ key: "brands", label: "Brands", description: "Products may carry a brand." },
|
||||
{ key: "productImages", label: "Product images", description: "Enable image upload on products." },
|
||||
{
|
||||
key: "serialNumbers",
|
||||
label: "Serial numbers",
|
||||
description: "Track individual units by serial number (captured at receipt, selected at sale).",
|
||||
},
|
||||
{ key: "batchLotTracking", label: "Batch / lot tracking", description: "Maintain stock batch/lot-wise." },
|
||||
{
|
||||
key: "expiryMfgDates",
|
||||
label: "Expiry / mfg dates",
|
||||
description: "Track expiry & manufacture dates (typically implies batch; drives FEFO).",
|
||||
},
|
||||
{
|
||||
key: "warranty",
|
||||
label: "Warranty",
|
||||
description: "Capture warranty period/terms (independent of serial & batch).",
|
||||
},
|
||||
{ key: "serviceItems", label: "Service items", description: "Sell non-stock service items (no stock/costing)." },
|
||||
{
|
||||
key: "adHocSaleLines",
|
||||
label: "Ad-hoc sale lines",
|
||||
description: "Allow non-inventory typed lines (name + price) on sales/quotations.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Pricing & Quantity",
|
||||
icon: Tag,
|
||||
fields: [
|
||||
{
|
||||
key: "minPriceFloor",
|
||||
label: "Minimum price floor",
|
||||
description: "Enforce a per-variant minimum sale price (even after discount).",
|
||||
},
|
||||
{
|
||||
key: "maxPriceCeiling",
|
||||
label: "Maximum price ceiling",
|
||||
description: "Enforce a per-variant maximum sale price.",
|
||||
},
|
||||
{
|
||||
key: "freePricingProducts",
|
||||
label: "Free-pricing products",
|
||||
description: "Products may be flagged free-pricing (operator sets any price, bypasses min/max).",
|
||||
},
|
||||
{
|
||||
key: "fractionalQuantities",
|
||||
label: "Fractional quantities",
|
||||
description: "Allow fractional quantities (e.g. 1.5 kg) where the unit permits.",
|
||||
},
|
||||
{
|
||||
key: "packConversion",
|
||||
label: "Pack conversion (UoM)",
|
||||
description: "Per-product pack units (buy Box / sell Piece). Stock is always stored in one base unit.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Stock Behaviour",
|
||||
icon: Boxes,
|
||||
fields: [
|
||||
{
|
||||
key: "allowNegativeStock",
|
||||
label: "Allow negative stock",
|
||||
description:
|
||||
"Company default for allowing negative stock (a product may narrow). Costed items are blocked from going negative regardless.",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export default function ProductConfigurationPage() {
|
||||
const [config, setConfig] = useState<ProductConfig | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
productConfigApi
|
||||
.get()
|
||||
.then(setConfig)
|
||||
.catch((err) => setError(errorMessage(err)))
|
||||
}, [])
|
||||
|
||||
function toggle(key: keyof ProductConfig, checked: boolean) {
|
||||
setConfig((prev) => (prev ? { ...prev, [key]: checked } : prev))
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!config) return
|
||||
setSaving(true)
|
||||
try {
|
||||
const saved = await productConfigApi.update(config)
|
||||
setConfig(saved)
|
||||
toast.success("Product configuration saved")
|
||||
} catch (err) {
|
||||
toast.error("Could not save product configuration", errorMessage(err))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/dashboard/products" className={cn(buttonVariants({ variant: "ghost", size: "icon-lg" }))}>
|
||||
<ArrowLeft className="size-5" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Product Configuration</h1>
|
||||
<p className="text-base text-muted-foreground">
|
||||
Company-wide toggles that control which product & category capabilities are available.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button size="lg" onClick={handleSave} disabled={!config || saving}>
|
||||
{saving ? "Saving…" : "Save changes"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>
|
||||
)}
|
||||
|
||||
{!error && !config && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<Skeleton className="h-64 w-full" />
|
||||
<Skeleton className="h-40 w-full" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{config && (
|
||||
<div className="flex flex-col gap-6">
|
||||
{SECTIONS.map((section) => (
|
||||
<div key={section.title} className="flex flex-col gap-5 rounded-2xl bg-card p-6 ring-1 ring-foreground/10">
|
||||
<div className="flex items-center gap-2.5 border-b pb-4">
|
||||
<section.icon className="size-5 text-indigo-600" />
|
||||
<h2 className="text-lg font-semibold text-foreground">{section.title}</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-6 sm:grid-cols-2">
|
||||
{section.fields.map((field) => (
|
||||
<label
|
||||
key={field.key}
|
||||
htmlFor={field.key}
|
||||
className="flex cursor-pointer items-start gap-3"
|
||||
>
|
||||
<Switch
|
||||
id={field.key}
|
||||
checked={config[field.key]}
|
||||
onCheckedChange={(checked) => toggle(field.key, checked)}
|
||||
className={cn("mt-0.5")}
|
||||
/>
|
||||
<span className="flex flex-col gap-0.5">
|
||||
<span className="text-base font-semibold text-foreground">{field.label}</span>
|
||||
<span className="text-sm text-muted-foreground">{field.description}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { variantCategoriesApi } from "@/lib/api/variants"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { validateVariantCategoryName, validateVariantItemForm } from "@/lib/validations/master-data"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useProductConfig } from "@/hooks/use-product-config"
|
||||
import { Category, VariantCategory } from "@/types/master-data"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -57,6 +58,8 @@ const DEFAULT_BASE_UOM_ID = 1
|
||||
|
||||
export default function NewItemPage() {
|
||||
const router = useRouter()
|
||||
const { config } = useProductConfig()
|
||||
const subcategoriesEnabled = !!config?.subcategories
|
||||
|
||||
const [categories, setCategories] = useState<Category[] | null>(null)
|
||||
const [brands, setBrands] = useState<{ brandId: number; name: string }[] | null>(null)
|
||||
@@ -92,7 +95,10 @@ export default function NewItemPage() {
|
||||
.catch((err) => setLoadError(errorMessage(err)))
|
||||
}, [])
|
||||
|
||||
const topCategories = useMemo(() => (categories ?? []).filter((c) => c.parentId === null), [categories])
|
||||
const topCategories = useMemo(
|
||||
() => (subcategoriesEnabled ? (categories ?? []).filter((c) => c.parentId === null) : (categories ?? [])),
|
||||
[categories, subcategoriesEnabled]
|
||||
)
|
||||
const subCategoryOptions = useMemo(
|
||||
() => (categories ?? []).filter((c) => c.parentId === categoryId),
|
||||
[categories, categoryId]
|
||||
@@ -259,21 +265,23 @@ export default function NewItemPage() {
|
||||
</Select>
|
||||
<FieldError errors={[errors.categoryId ? { message: errors.categoryId } : undefined]} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Subcategory (optional)</Label>
|
||||
<Select<number | null> value={subCategoryId} onValueChange={setSubCategoryId} disabled={subCategoryOptions.length === 0}>
|
||||
<SelectTrigger className="h-12! w-full text-base">
|
||||
<SelectValue placeholder={subCategoryOptions.length === 0 ? "No subcategories" : "Select subcategory"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{subCategoryOptions.map((c) => (
|
||||
<SelectItem key={c.categoryId} value={c.categoryId} className="text-base">
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{subcategoriesEnabled && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Subcategory (optional)</Label>
|
||||
<Select<number | null> value={subCategoryId} onValueChange={setSubCategoryId} disabled={subCategoryOptions.length === 0}>
|
||||
<SelectTrigger className="h-12! w-full text-base">
|
||||
<SelectValue placeholder={subCategoryOptions.length === 0 ? "No subcategories" : "Select subcategory"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{subCategoryOptions.map((c) => (
|
||||
<SelectItem key={c.categoryId} value={c.categoryId} className="text-base">
|
||||
{c.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label className="text-base">Brand (optional)</Label>
|
||||
<Select<number | null> value={brandId} onValueChange={setBrandId}>
|
||||
|
||||
Reference in New Issue
Block a user