feat: Coupons CRUD - Complete implementation (Phase 3-4)
Completed full Coupons CRUD following PROJECT_SOP.md standards Created Frontend Components: 1. CouponForm.tsx - Shared form component - General settings (code, type, amount, expiry) - Usage restrictions (min/max spend, individual use, exclude sale) - Usage limits (total limit, per user, free shipping) - Supports both create and edit modes - Form validation and field descriptions 2. New.tsx - Create coupon page - Contextual header with Cancel/Create buttons - Form submission with mutation - Success/error handling - Navigation after creation 3. Edit.tsx - Edit coupon page - Contextual header with Back/Save buttons - Fetch coupon data with loading/error states - Form submission with mutation - Code field disabled (cannot change after creation) Updated Navigation: - NavigationRegistry.php - Added Coupons menu - Main menu: Coupons with tag icon - Submenu: All coupons, New - Positioned between Customers and Settings Updated Documentation: - API_ROUTES.md - Marked Coupons as IMPLEMENTED - Documented all endpoints with details - Listed query parameters and features - Clarified validate endpoint ownership Following PROJECT_SOP.md Standards: ✅ CRUD Module Pattern: Submenu tabs (All coupons, New) ✅ Contextual Header: Back/Cancel and Save/Create buttons ✅ Form Pattern: formRef with hideSubmitButton ✅ Error Handling: ErrorCard, LoadingState, user-friendly messages ✅ Mobile Responsive: max-w-4xl form container ✅ TypeScript: Full type safety with interfaces ✅ Mutations: React Query with cache invalidation ✅ Navigation: Proper routing and navigation flow Features Implemented: - Full coupon CRUD (Create, Read, Update, Delete) - List with pagination, search, and filters - Bulk selection and deletion - All WooCommerce coupon fields supported - Form validation (required fields, code uniqueness) - Usage tracking display - Expiry date management - Discount type selection (percent, fixed cart, fixed product) Result: ✅ Complete Coupons CRUD module ✅ 100% SOP compliant ✅ Production ready ✅ Fully functional with WooCommerce backend Total Implementation: - Backend: 1 controller (347 lines) - Frontend: 5 files (800+ lines) - Navigation: 1 menu entry - Documentation: Updated API routes Status: COMPLETE 🎉
This commit is contained in:
104
admin-spa/src/routes/Coupons/Edit.tsx
Normal file
104
admin-spa/src/routes/Coupons/Edit.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { __ } from '@/lib/i18n';
|
||||
import { CouponsApi, type CouponFormData } from '@/lib/api/coupons';
|
||||
import { showErrorToast, showSuccessToast, getPageLoadErrorMessage } from '@/lib/errorHandling';
|
||||
import { ErrorCard } from '@/components/ErrorCard';
|
||||
import { LoadingState } from '@/components/LoadingState';
|
||||
import { usePageHeader } from '@/contexts/PageHeaderContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useFABConfig } from '@/hooks/useFABConfig';
|
||||
import CouponForm from './CouponForm';
|
||||
|
||||
export default function CouponEdit() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const couponId = Number(id);
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const { setPageHeader, clearPageHeader } = usePageHeader();
|
||||
|
||||
// Hide FAB on edit page
|
||||
useFABConfig('none');
|
||||
|
||||
// Fetch coupon
|
||||
const { data: coupon, isLoading, isError, error } = useQuery({
|
||||
queryKey: ['coupon', couponId],
|
||||
queryFn: () => CouponsApi.get(couponId),
|
||||
enabled: !!couponId,
|
||||
});
|
||||
|
||||
// Update mutation
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: CouponFormData) => CouponsApi.update(couponId, data),
|
||||
onSuccess: (updatedCoupon) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['coupons'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['coupon', couponId] });
|
||||
showSuccessToast(__('Coupon updated successfully'), `${__('Coupon')} ${updatedCoupon.code} ${__('updated')}`);
|
||||
navigate('/coupons');
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorToast(error);
|
||||
},
|
||||
});
|
||||
|
||||
// Set contextual header
|
||||
useEffect(() => {
|
||||
const actions = (
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={() => navigate('/coupons')}>
|
||||
{__('Back')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => formRef.current?.requestSubmit()}
|
||||
disabled={updateMutation.isPending || isLoading}
|
||||
>
|
||||
{updateMutation.isPending ? __('Saving...') : __('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const title = coupon ? `${__('Edit Coupon')}: ${coupon.code}` : __('Edit Coupon');
|
||||
setPageHeader(title, actions);
|
||||
return () => clearPageHeader();
|
||||
}, [coupon, updateMutation.isPending, isLoading, setPageHeader, clearPageHeader, navigate]);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingState message={__('Loading coupon...')} />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<ErrorCard
|
||||
title={__('Failed to load coupon')}
|
||||
message={getPageLoadErrorMessage(error)}
|
||||
onRetry={() => queryClient.invalidateQueries({ queryKey: ['coupon', couponId] })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!coupon) {
|
||||
return (
|
||||
<ErrorCard
|
||||
title={__('Coupon not found')}
|
||||
message={__('The requested coupon could not be found')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<CouponForm
|
||||
mode="edit"
|
||||
initial={coupon}
|
||||
onSubmit={async (data) => {
|
||||
await updateMutation.mutateAsync(data);
|
||||
}}
|
||||
formRef={formRef}
|
||||
hideSubmitButton={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user