diff --git a/admin-spa/src/routes/Products/Edit.tsx b/admin-spa/src/routes/Products/Edit.tsx index af9900f..ab9726f 100644 --- a/admin-spa/src/routes/Products/Edit.tsx +++ b/admin-spa/src/routes/Products/Edit.tsx @@ -6,7 +6,7 @@ import { __ } from '@/lib/i18n'; import { toast } from 'sonner'; import { useFABConfig } from '@/hooks/useFABConfig'; import { usePageHeader } from '@/contexts/PageHeaderContext'; -import { ProductForm, ProductFormData } from './partials/ProductForm'; +import { ProductFormTabbed as ProductForm, ProductFormData } from './partials/ProductFormTabbed'; import { Button } from '@/components/ui/button'; import { ErrorCard } from '@/components/ErrorCard'; import { getPageLoadErrorMessage } from '@/lib/errorHandling'; diff --git a/admin-spa/src/routes/Products/New.tsx b/admin-spa/src/routes/Products/New.tsx index 386bcb5..d694d93 100644 --- a/admin-spa/src/routes/Products/New.tsx +++ b/admin-spa/src/routes/Products/New.tsx @@ -6,7 +6,7 @@ import { __ } from '@/lib/i18n'; import { toast } from 'sonner'; import { useFABConfig } from '@/hooks/useFABConfig'; import { usePageHeader } from '@/contexts/PageHeaderContext'; -import { ProductForm, ProductFormData } from './partials/ProductForm'; +import { ProductFormTabbed as ProductForm, ProductFormData } from './partials/ProductFormTabbed'; import { Button } from '@/components/ui/button'; export default function ProductNew() { diff --git a/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx b/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx new file mode 100644 index 0000000..ba05635 --- /dev/null +++ b/admin-spa/src/routes/Products/partials/ProductFormTabbed.tsx @@ -0,0 +1,252 @@ +import React, { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/lib/api'; +import { __ } from '@/lib/i18n'; +import { Button } from '@/components/ui/button'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Package, DollarSign, Layers, Tag } from 'lucide-react'; +import { toast } from 'sonner'; +import { GeneralTab } from './tabs/GeneralTab'; +import { PricingTab } from './tabs/PricingTab'; +import { InventoryTab } from './tabs/InventoryTab'; +import { VariationsTab, ProductVariant } from './tabs/VariationsTab'; +import { OrganizationTab } from './tabs/OrganizationTab'; + +// Types +export type ProductFormData = { + name: string; + type: 'simple' | 'variable' | 'grouped' | 'external'; + status: 'publish' | 'draft' | 'pending' | 'private'; + description?: string; + short_description?: string; + sku?: string; + regular_price?: string; + sale_price?: string; + manage_stock?: boolean; + stock_quantity?: number; + stock_status?: 'instock' | 'outofstock' | 'onbackorder'; + categories?: number[]; + tags?: number[]; + images?: string[]; + attributes?: Array<{ name: string; options: string[]; variation: boolean }>; + variations?: ProductVariant[]; + virtual?: boolean; + downloadable?: boolean; + featured?: boolean; +}; + +type Props = { + mode: 'create' | 'edit'; + initial?: ProductFormData | null; + onSubmit: (data: ProductFormData) => Promise | void; + className?: string; + formRef?: React.RefObject; + hideSubmitButton?: boolean; +}; + +export function ProductFormTabbed({ + mode, + initial, + onSubmit, + className, + formRef, + hideSubmitButton = false, +}: Props) { + // Form state + const [name, setName] = useState(initial?.name || ''); + const [type, setType] = useState(initial?.type || 'simple'); + const [status, setStatus] = useState(initial?.status || 'publish'); + const [description, setDescription] = useState(initial?.description || ''); + const [shortDescription, setShortDescription] = useState(initial?.short_description || ''); + const [sku, setSku] = useState(initial?.sku || ''); + const [regularPrice, setRegularPrice] = useState(initial?.regular_price || ''); + const [salePrice, setSalePrice] = useState(initial?.sale_price || ''); + const [manageStock, setManageStock] = useState(initial?.manage_stock || false); + const [stockQuantity, setStockQuantity] = useState(initial?.stock_quantity?.toString() || ''); + const [stockStatus, setStockStatus] = useState(initial?.stock_status || 'instock'); + const [selectedCategories, setSelectedCategories] = useState(initial?.categories || []); + const [selectedTags, setSelectedTags] = useState(initial?.tags || []); + const [images, setImages] = useState(initial?.images || []); + const [attributes, setAttributes] = useState>( + initial?.attributes || [] + ); + const [variations, setVariations] = useState(initial?.variations || []); + const [virtual, setVirtual] = useState(initial?.virtual || false); + const [downloadable, setDownloadable] = useState(initial?.downloadable || false); + const [featured, setFeatured] = useState(initial?.featured || false); + const [submitting, setSubmitting] = useState(false); + const [activeTab, setActiveTab] = useState('general'); + + // Fetch categories + const categoriesQ = useQuery({ + queryKey: ['product-categories'], + queryFn: () => api.get('/products/categories'), + }); + + // Fetch tags + const tagsQ = useQuery({ + queryKey: ['product-tags'], + queryFn: () => api.get('/products/tags'), + }); + + const categories = categoriesQ.data || []; + const tags = tagsQ.data || []; + + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (submitting) return; + + // Validation + if (!name.trim()) { + toast.error(__('Product name is required')); + setActiveTab('general'); + return; + } + + if (type === 'simple' && !regularPrice) { + toast.error(__('Regular price is required for simple products')); + setActiveTab('pricing'); + return; + } + + setSubmitting(true); + try { + const payload: ProductFormData = { + name: name.trim(), + type, + status, + description, + short_description: shortDescription, + sku, + regular_price: regularPrice, + sale_price: salePrice, + manage_stock: manageStock, + stock_quantity: manageStock ? parseInt(stockQuantity) || 0 : undefined, + stock_status: stockStatus, + categories: selectedCategories, + tags: selectedTags, + images, + attributes: type === 'variable' ? attributes : undefined, + variations: type === 'variable' ? variations : undefined, + virtual, + downloadable, + featured, + }; + + await onSubmit(payload); + } catch (error: any) { + toast.error(error.message || __('Failed to save product')); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ + + + + {__('General')} + + + + {__('Pricing')} + + + + {__('Inventory')} + + + + {__('Variations')} + + + + {__('Organization')} + + + + {/* General Tab */} + + + + + {/* Pricing Tab */} + + + + + {/* Inventory Tab */} + + + + + {/* Variations Tab */} + + + + + {/* Organization Tab */} + + + + + + {/* Submit Button */} + {!hideSubmitButton && ( +
+ +
+ )} +
+ ); +} diff --git a/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx b/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx new file mode 100644 index 0000000..1274527 --- /dev/null +++ b/admin-spa/src/routes/Products/partials/tabs/GeneralTab.tsx @@ -0,0 +1,190 @@ +import React from 'react'; +import { __ } from '@/lib/i18n'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Separator } from '@/components/ui/separator'; + +type GeneralTabProps = { + name: string; + setName: (value: string) => void; + type: 'simple' | 'variable' | 'grouped' | 'external'; + setType: (value: 'simple' | 'variable' | 'grouped' | 'external') => void; + status: 'publish' | 'draft' | 'pending' | 'private'; + setStatus: (value: 'publish' | 'draft' | 'pending' | 'private') => void; + description: string; + setDescription: (value: string) => void; + shortDescription: string; + setShortDescription: (value: string) => void; + virtual: boolean; + setVirtual: (value: boolean) => void; + downloadable: boolean; + setDownloadable: (value: boolean) => void; + featured: boolean; + setFeatured: (value: boolean) => void; +}; + +export function GeneralTab({ + name, + setName, + type, + setType, + status, + setStatus, + description, + setDescription, + shortDescription, + setShortDescription, + virtual, + setVirtual, + downloadable, + setDownloadable, + featured, + setFeatured, +}: GeneralTabProps) { + return ( + + + {__('Basic Information')} + {__('Essential product details')} + + + {/* Product Name */} +
+ + setName(e.target.value)} + placeholder={__('e.g., Premium Cotton T-Shirt')} + required + className="mt-1.5" + /> +

+ {__('Give your product a short and clear name')} +

+
+ + {/* Type & Status */} +
+
+ + +

+ {type === 'simple' && __('A standalone product')} + {type === 'variable' && __('Product with variations (size, color, etc.)')} + {type === 'grouped' && __('A collection of related products')} + {type === 'external' && __('Product sold on another website')} +

+
+ +
+ + +
+
+ + + + {/* Description */} +
+ +