feat: Product New/Edit pages with comprehensive form

Implemented full Product CRUD create/edit functionality.

Product New Page (New.tsx):
 Create new products
 Page header with back/create buttons
 Form submission with React Query mutation
 Success toast & navigation
 Error handling

Product Edit Page (Edit.tsx):
 Load existing product data
 Update product with PUT request
 Loading & error states
 Page header with back/save buttons
 Query invalidation on success

ProductForm Component (partials/ProductForm.tsx - 600+ lines):
 Basic Information (name, type, status, descriptions)
 Product Types: Simple, Variable, Grouped, External
 Pricing (regular, sale, SKU) for simple products
 Inventory Management (stock tracking, quantity, status)
 Categories & Tags (multi-select with checkboxes)
 Attributes & Variations (for variable products)
  - Add/remove attributes
  - Define attribute options
  - Generate all variations automatically
  - Per-variation pricing & stock
 Additional Options (virtual, downloadable, featured)
 Form validation
 Reusable for create/edit modes
 Full i18n support

Features:
- Dynamic category/tag fetching from API
- Variation generator from attributes
- Manage stock toggle
- Stock status badges
- Form ref for external submit
- Hide submit button option (for page header buttons)
- Comprehensive validation
- Toast notifications

Pattern:
- Follows PROJECT_SOP.md CRUD template
- Consistent with Orders module
- Clean separation of concerns
- Type-safe with TypeScript
This commit is contained in:
dwindown
2025-11-19 20:36:26 +07:00
parent 757a425169
commit 479293ed09
3 changed files with 789 additions and 4 deletions

View File

@@ -1,11 +1,76 @@
import React from 'react';
import React, { useRef, useEffect } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { api } from '@/lib/api';
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 { Button } from '@/components/ui/button';
export default function ProductNew() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const formRef = useRef<HTMLFormElement>(null);
const { setPageHeader, clearPageHeader } = usePageHeader();
// Hide FAB on new product page
useFABConfig('none');
// Create mutation
const createMutation = useMutation({
mutationFn: async (data: ProductFormData) => {
return api.post('/products', data);
},
onSuccess: (response: any) => {
toast.success(__('Product created successfully'));
queryClient.invalidateQueries({ queryKey: ['products'] });
// Navigate to product detail or edit page
if (response?.id) {
navigate(`/products/${response.id}`);
} else {
navigate('/products');
}
},
onError: (error: any) => {
toast.error(error.message || __('Failed to create product'));
},
});
const handleSubmit = async (data: ProductFormData) => {
await createMutation.mutateAsync(data);
};
// Set page header with back button and create button
useEffect(() => {
const actions = (
<div className="flex gap-2">
<Button size="sm" variant="ghost" onClick={() => navigate('/products')}>
{__('Back')}
</Button>
<Button
size="sm"
onClick={() => formRef.current?.requestSubmit()}
disabled={createMutation.isPending}
>
{createMutation.isPending ? __('Creating...') : __('Create')}
</Button>
</div>
);
setPageHeader(__('New Product'), actions);
return () => clearPageHeader();
}, [createMutation.isPending, setPageHeader, clearPageHeader, navigate]);
return (
<div>
<h1 className="text-xl font-semibold mb-3">{__('New Product')}</h1>
<p className="opacity-70">{__('Coming soon — SPA product create form.')}</p>
<div className="space-y-4">
<ProductForm
mode="create"
onSubmit={handleSubmit}
formRef={formRef}
hideSubmitButton={true}
/>
</div>
);
}