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

@@ -0,0 +1,109 @@
import React, { useRef, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate, useParams } 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';
import { ErrorCard } from '@/components/ErrorCard';
import { getPageLoadErrorMessage } from '@/lib/errorHandling';
import { Skeleton } from '@/components/ui/skeleton';
export default function ProductEdit() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const formRef = useRef<HTMLFormElement>(null);
const { setPageHeader, clearPageHeader } = usePageHeader();
// Hide FAB on edit product page
useFABConfig('none');
// Fetch product
const productQ = useQuery({
queryKey: ['products', id],
queryFn: () => api.get(`/products/${id}`),
enabled: !!id,
});
// Update mutation
const updateMutation = useMutation({
mutationFn: async (data: ProductFormData) => {
return api.put(`/products/${id}`, data);
},
onSuccess: (response: any) => {
toast.success(__('Product updated successfully'));
queryClient.invalidateQueries({ queryKey: ['products'] });
queryClient.invalidateQueries({ queryKey: ['products', id] });
// Navigate back to product detail or list
navigate(`/products/${id}`);
},
onError: (error: any) => {
toast.error(error.message || __('Failed to update product'));
},
});
const handleSubmit = async (data: ProductFormData) => {
await updateMutation.mutateAsync(data);
};
// Set page header with back button and save 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={updateMutation.isPending || productQ.isLoading}
>
{updateMutation.isPending ? __('Saving...') : __('Save')}
</Button>
</div>
);
setPageHeader(__('Edit Product'), actions);
return () => clearPageHeader();
}, [updateMutation.isPending, productQ.isLoading, setPageHeader, clearPageHeader, navigate]);
// Loading state
if (productQ.isLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-64 w-full" />
<Skeleton className="h-32 w-full" />
</div>
);
}
// Error state
if (productQ.isError) {
return (
<ErrorCard
title={__('Failed to load product')}
message={getPageLoadErrorMessage(productQ.error)}
onRetry={() => productQ.refetch()}
/>
);
}
const product = productQ.data;
return (
<div className="space-y-4">
<ProductForm
mode="edit"
initial={product}
onSubmit={handleSubmit}
formRef={formRef}
hideSubmitButton={true}
/>
</div>
);
}