Make subcategory optional, source category picker from real tenant data
Matches the backend: subcategory is no longer a required field in the import modal (defaults to none/0 instead of blocking submission), and the Category dropdown now comes from GET gettenantcategories (what this tenant's own products actually use) instead of the global categories list, which was missing categoryid 2 despite it being the category tenant 1135 actually needs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { X, Save, AlertCircle } from 'lucide-react';
|
import { X, Save, AlertCircle } from 'lucide-react';
|
||||||
import { CatalogueProduct, ImportCatalogueProductRequest } from '../services/catalogueApi';
|
import { CatalogueProduct, ImportCatalogueProductRequest } from '../services/catalogueApi';
|
||||||
import { useProductSubcategories } from '../hooks/useCatalogueImport';
|
import { useProductSubcategories, useTenantCategories } from '../hooks/useCatalogueImport';
|
||||||
import { useFiestaProductCategories } from '../services/fiestaQueries';
|
|
||||||
import { str as fstr } from '../services/fiestaApi';
|
|
||||||
|
|
||||||
interface ImportProductModalProps {
|
interface ImportProductModalProps {
|
||||||
product: CatalogueProduct;
|
product: CatalogueProduct;
|
||||||
@@ -27,14 +25,14 @@ export default function ImportProductModal({
|
|||||||
const [taxPercent, setTaxPercent] = useState<string>('0');
|
const [taxPercent, setTaxPercent] = useState<string>('0');
|
||||||
const [quantity, setQuantity] = useState<string>('1');
|
const [quantity, setQuantity] = useState<string>('1');
|
||||||
|
|
||||||
const { data: categoriesData = [], isLoading: isLoadingCategories } = useFiestaProductCategories();
|
// Categories this tenant's own products actually use — the global
|
||||||
const categories = categoriesData.map((c: any) => ({
|
// categories list is missing categoryids that are nonetheless in real use.
|
||||||
categoryid: Number(c.categoryid),
|
const { data: categories = [], isLoading: isLoadingCategories } = useTenantCategories(tenantid);
|
||||||
categoryname: fstr(c.categoryname),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Subcategories are scoped to the selected category, so a product can't be
|
// Subcategories are scoped to the selected category, so a product can't be
|
||||||
// tagged with a subcategory that doesn't actually belong to its category.
|
// tagged with a subcategory that doesn't actually belong to its category.
|
||||||
|
// Optional: many subcategories are unowned/shared and a product can be
|
||||||
|
// imported without one (falls back to "Uncategorized" in subcategory views).
|
||||||
const { data: subcategories = [], isLoading: isLoadingSubcats } = useProductSubcategories(
|
const { data: subcategories = [], isLoading: isLoadingSubcats } = useProductSubcategories(
|
||||||
tenantid,
|
tenantid,
|
||||||
categoryId ? Number(categoryId) : undefined,
|
categoryId ? Number(categoryId) : undefined,
|
||||||
@@ -42,7 +40,7 @@ export default function ImportProductModal({
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!categoryId || !subcategoryId || !retailPrice || !productCost) {
|
if (!categoryId || !retailPrice || !productCost) {
|
||||||
alert("Please fill in all required fields.");
|
alert("Please fill in all required fields.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -53,7 +51,7 @@ export default function ImportProductModal({
|
|||||||
brand: product.brand,
|
brand: product.brand,
|
||||||
catalogueid: product.id,
|
catalogueid: product.id,
|
||||||
categoryid: Number(categoryId),
|
categoryid: Number(categoryId),
|
||||||
subcategoryid: Number(subcategoryId),
|
subcategoryid: subcategoryId ? Number(subcategoryId) : 0,
|
||||||
quantity: Number(quantity),
|
quantity: Number(quantity),
|
||||||
stocktype: "in",
|
stocktype: "in",
|
||||||
status: "Active",
|
status: "Active",
|
||||||
@@ -112,7 +110,7 @@ export default function ImportProductModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Subcategory *</label>
|
<label className="text-xs font-semibold text-slate-600 uppercase tracking-wider">Subcategory (optional)</label>
|
||||||
{!categoryId ? (
|
{!categoryId ? (
|
||||||
<div className="w-full px-3 py-2 border border-slate-200 rounded-lg text-slate-400">Select a category first</div>
|
<div className="w-full px-3 py-2 border border-slate-200 rounded-lg text-slate-400">Select a category first</div>
|
||||||
) : isLoadingSubcats ? (
|
) : isLoadingSubcats ? (
|
||||||
@@ -122,9 +120,8 @@ export default function ImportProductModal({
|
|||||||
value={subcategoryId}
|
value={subcategoryId}
|
||||||
onChange={e => setSubcategoryId(e.target.value)}
|
onChange={e => setSubcategoryId(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-colors bg-white"
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 transition-colors bg-white"
|
||||||
required
|
|
||||||
>
|
>
|
||||||
<option value="">Select subcategory...</option>
|
<option value="">None</option>
|
||||||
{subcategories.map((s: any) => (
|
{subcategories.map((s: any) => (
|
||||||
<option key={s.subcategoryid || s.id} value={s.subcategoryid || s.id}>
|
<option key={s.subcategoryid || s.id} value={s.subcategoryid || s.id}>
|
||||||
{s.subcategoryname || s.name}
|
{s.subcategoryname || s.name}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
importCatalogueProducts,
|
importCatalogueProducts,
|
||||||
getBrands,
|
getBrands,
|
||||||
getProductSubcategories,
|
getProductSubcategories,
|
||||||
|
getTenantCategories,
|
||||||
removeFromStoreCatalogue,
|
removeFromStoreCatalogue,
|
||||||
ImportCatalogueProductRequest
|
ImportCatalogueProductRequest
|
||||||
} from '../services/catalogueApi';
|
} from '../services/catalogueApi';
|
||||||
@@ -39,6 +40,13 @@ export function useProductSubcategories(tenantid: number, categoryid?: number) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTenantCategories(tenantid: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['catalogue', 'tenantCategories', tenantid],
|
||||||
|
queryFn: () => getTenantCategories(tenantid),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useImportCatalogueProduct(tenantid: number, locationid: number) {
|
export function useImportCatalogueProduct(tenantid: number, locationid: number) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
|
|||||||
@@ -122,3 +122,18 @@ export async function getProductSubcategories(tenantid: number, categoryid?: num
|
|||||||
const { items } = await apiGet<{ subcategoryid: number; subcategoryname: string }>(url);
|
const { items } = await apiGet<{ subcategoryid: number; subcategoryname: string }>(url);
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TenantCategory {
|
||||||
|
categoryid: number;
|
||||||
|
categoryname: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Categories this tenant's own products actually use — not the global
|
||||||
|
// productcategories list, which is missing categoryids that are nonetheless
|
||||||
|
// in real use (e.g. categoryid 2), which would make the picker unusable.
|
||||||
|
export async function getTenantCategories(tenantid: number) {
|
||||||
|
const url = new URL(`${API_BASE}/products/gettenantcategories`);
|
||||||
|
url.searchParams.set("tenantid", String(tenantid));
|
||||||
|
const { items } = await apiGet<TenantCategory>(url);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user