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:
109
admin-spa/src/routes/Products/Edit.tsx
Normal file
109
admin-spa/src/routes/Products/Edit.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user