feat: Add zone management backend + drawer z-index fix + SettingsCard action prop
## 1. Fixed Drawer Z-Index ✅ - Increased drawer z-index from 50 to 60 - Now appears above bottom navigation (z-50) - Fixes mobile drawer visibility issue ## 2. Zone Management Backend ✅ Added full CRUD for shipping zones: - POST /settings/shipping/zones - Create zone - PUT /settings/shipping/zones/{id} - Update zone - DELETE /settings/shipping/zones/{id} - Delete zone - GET /settings/shipping/locations - Get countries/states/continents Features: - Create zones with name and regions - Update zone name and regions - Delete zones - Region selector with continents, countries, and states - Proper cache invalidation ## 3. Zone Management Frontend (In Progress) ⏳ - Added state for zone CRUD (showAddZone, editingZone, deletingZone) - Added mutations (createZone, updateZone, deleteZone) - Added "Add Zone" button to SettingsCard - Updated empty state with "Create First Zone" button ## 4. Enhanced SettingsCard Component ✅ - Added optional `action` prop for header buttons - Flexbox layout for title/description + action - Used in Shipping zones for "Add Zone" button ## Next Steps: - Add delete button to each zone - Create Add/Edit Zone dialog with region selector - Add delete confirmation dialog - Then move to Tax rates and Email subjects
This commit is contained in:
@@ -26,7 +26,7 @@ const DrawerOverlay = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DrawerPrimitive.Overlay
|
<DrawerPrimitive.Overlay
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
className={cn("fixed inset-0 z-[60] bg-black/80", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@@ -41,7 +41,7 @@ const DrawerContent = React.forwardRef<
|
|||||||
<DrawerPrimitive.Content
|
<DrawerPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
"fixed inset-x-0 bottom-0 z-[60] mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ export default function ShippingPage() {
|
|||||||
const [expandedMethod, setExpandedMethod] = useState<string>('');
|
const [expandedMethod, setExpandedMethod] = useState<string>('');
|
||||||
const [methodSettings, setMethodSettings] = useState<Record<string, any>>({});
|
const [methodSettings, setMethodSettings] = useState<Record<string, any>>({});
|
||||||
const [deletingMethod, setDeletingMethod] = useState<{ zoneId: number; instanceId: number; name: string } | null>(null);
|
const [deletingMethod, setDeletingMethod] = useState<{ zoneId: number; instanceId: number; name: string } | null>(null);
|
||||||
|
const [showAddZone, setShowAddZone] = useState(false);
|
||||||
|
const [editingZone, setEditingZone] = useState<any | null>(null);
|
||||||
|
const [deletingZone, setDeletingZone] = useState<any | null>(null);
|
||||||
const isDesktop = useMediaQuery("(min-width: 768px)");
|
const isDesktop = useMediaQuery("(min-width: 768px)");
|
||||||
|
|
||||||
// Fetch shipping zones from WooCommerce
|
// Fetch shipping zones from WooCommerce
|
||||||
@@ -170,6 +173,52 @@ export default function ShippingPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Zone mutations
|
||||||
|
const createZoneMutation = useMutation({
|
||||||
|
mutationFn: async (data: { name: string; regions: any[] }) => {
|
||||||
|
return api.post('/settings/shipping/zones', data);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['shipping-zones'] });
|
||||||
|
toast.success(__('Zone created successfully'));
|
||||||
|
setShowAddZone(false);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || __('Failed to create zone'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateZoneMutation = useMutation({
|
||||||
|
mutationFn: async ({ zoneId, ...data }: { zoneId: number; name?: string; regions?: any[] }) => {
|
||||||
|
return api.wpFetch(`/woonoow/v1/settings/shipping/zones/${zoneId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['shipping-zones'] });
|
||||||
|
toast.success(__('Zone updated successfully'));
|
||||||
|
setEditingZone(null);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || __('Failed to update zone'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteZoneMutation = useMutation({
|
||||||
|
mutationFn: async (zoneId: number) => {
|
||||||
|
return api.del(`/settings/shipping/zones/${zoneId}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['shipping-zones'] });
|
||||||
|
toast.success(__('Zone deleted successfully'));
|
||||||
|
setDeletingZone(null);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
toast.error(error?.message || __('Failed to delete zone'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<SettingsLayout
|
<SettingsLayout
|
||||||
@@ -203,19 +252,27 @@ export default function ShippingPage() {
|
|||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={__('Shipping Zones')}
|
title={__('Shipping Zones')}
|
||||||
description={__('Create zones to group regions with similar shipping rates')}
|
description={__('Create zones to group regions with similar shipping rates')}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowAddZone(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
|
{__('Add Zone')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{zones.length === 0 ? (
|
{zones.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
{__('No shipping zones configured yet.')}
|
{__('No shipping zones configured yet. Create your first zone to start offering shipping.')}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
onClick={() => setShowAddZone(true)}
|
||||||
asChild
|
|
||||||
>
|
>
|
||||||
<a href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=shipping`} target="_blank" rel="noopener noreferrer">
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{__('Configure in WooCommerce')}
|
{__('Create First Zone')}
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,14 +6,20 @@ interface SettingsCardProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
action?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsCard({ title, description, children, className = '' }: SettingsCardProps) {
|
export function SettingsCard({ title, description, children, className = '', action }: SettingsCardProps) {
|
||||||
return (
|
return (
|
||||||
<Card className={className}>
|
<Card className={className}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{title}</CardTitle>
|
<div className="flex items-start justify-between gap-4">
|
||||||
{description && <CardDescription>{description}</CardDescription>}
|
<div className="flex-1">
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
{description && <CardDescription>{description}</CardDescription>}
|
||||||
|
</div>
|
||||||
|
{action && <div className="flex-shrink-0">{action}</div>}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -30,6 +30,60 @@ class ShippingController extends WP_REST_Controller {
|
|||||||
'callback' => array( $this, 'get_zones' ),
|
'callback' => array( $this, 'get_zones' ),
|
||||||
'permission_callback' => array( $this, 'check_permission' ),
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
),
|
),
|
||||||
|
array(
|
||||||
|
'methods' => \WP_REST_Server::CREATABLE,
|
||||||
|
'callback' => array( $this, 'create_zone' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'name' => array(
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
),
|
||||||
|
'regions' => array(
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'array',
|
||||||
|
'default' => array(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update/Delete specific zone
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/zones/(?P<zone_id>\d+)',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => \WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => array( $this, 'update_zone' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'zone_id' => array(
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
'name' => array(
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'string',
|
||||||
|
),
|
||||||
|
'regions' => array(
|
||||||
|
'required' => false,
|
||||||
|
'type' => 'array',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'methods' => \WP_REST_Server::DELETABLE,
|
||||||
|
'callback' => array( $this, 'delete_zone' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
'args' => array(
|
||||||
|
'zone_id' => array(
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'integer',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,6 +198,19 @@ class ShippingController extends WP_REST_Controller {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get available countries and states for zone regions
|
||||||
|
register_rest_route(
|
||||||
|
$this->namespace,
|
||||||
|
'/' . $this->rest_base . '/locations',
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'methods' => \WP_REST_Server::READABLE,
|
||||||
|
'callback' => array( $this, 'get_locations' ),
|
||||||
|
'permission_callback' => array( $this, 'check_permission' ),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -676,6 +743,193 @@ class ShippingController extends WP_REST_Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new shipping zone
|
||||||
|
*/
|
||||||
|
public function create_zone( WP_REST_Request $request ) {
|
||||||
|
$name = sanitize_text_field( $request->get_param( 'name' ) );
|
||||||
|
$regions = $request->get_param( 'regions' ) ?: array();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$zone = new \WC_Shipping_Zone( null );
|
||||||
|
$zone->set_zone_name( $name );
|
||||||
|
$zone->set_zone_order( 0 );
|
||||||
|
$zone->save();
|
||||||
|
|
||||||
|
// Add regions/locations
|
||||||
|
if ( ! empty( $regions ) ) {
|
||||||
|
foreach ( $regions as $region ) {
|
||||||
|
$zone->add_location( $region['code'], $region['type'] );
|
||||||
|
}
|
||||||
|
$zone->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
\WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
|
||||||
|
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'success' => true,
|
||||||
|
'zone_id' => $zone->get_id(),
|
||||||
|
'message' => __( 'Shipping zone created successfully', 'woonoow' ),
|
||||||
|
),
|
||||||
|
201
|
||||||
|
);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'error' => 'create_failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
),
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a shipping zone
|
||||||
|
*/
|
||||||
|
public function update_zone( WP_REST_Request $request ) {
|
||||||
|
$zone_id = $request->get_param( 'zone_id' );
|
||||||
|
$name = $request->get_param( 'name' );
|
||||||
|
$regions = $request->get_param( 'regions' );
|
||||||
|
|
||||||
|
try {
|
||||||
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
||||||
|
if ( ! $zone || ! $zone->get_id() ) {
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'error' => 'zone_not_found',
|
||||||
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
||||||
|
),
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $name ) {
|
||||||
|
$zone->set_zone_name( sanitize_text_field( $name ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $regions !== null ) {
|
||||||
|
// Clear existing locations
|
||||||
|
$zone->clear_locations();
|
||||||
|
|
||||||
|
// Add new locations
|
||||||
|
foreach ( $regions as $region ) {
|
||||||
|
$zone->add_location( $region['code'], $region['type'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone->save();
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
\WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
|
||||||
|
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( 'Shipping zone updated successfully', 'woonoow' ),
|
||||||
|
),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'error' => 'update_failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
),
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a shipping zone
|
||||||
|
*/
|
||||||
|
public function delete_zone( WP_REST_Request $request ) {
|
||||||
|
$zone_id = $request->get_param( 'zone_id' );
|
||||||
|
|
||||||
|
try {
|
||||||
|
$zone = \WC_Shipping_Zones::get_zone( $zone_id );
|
||||||
|
if ( ! $zone || ! $zone->get_id() ) {
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'error' => 'zone_not_found',
|
||||||
|
'message' => __( 'Shipping zone not found', 'woonoow' ),
|
||||||
|
),
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$zone->delete();
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
\WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' );
|
||||||
|
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => __( 'Shipping zone deleted successfully', 'woonoow' ),
|
||||||
|
),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
return new WP_REST_Response(
|
||||||
|
array(
|
||||||
|
'error' => 'delete_failed',
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
),
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available countries and states for zone regions
|
||||||
|
*/
|
||||||
|
public function get_locations( WP_REST_Request $request ) {
|
||||||
|
$countries_obj = new \WC_Countries();
|
||||||
|
$countries = $countries_obj->get_countries();
|
||||||
|
$states = $countries_obj->get_states();
|
||||||
|
|
||||||
|
$locations = array();
|
||||||
|
|
||||||
|
// Add continents
|
||||||
|
$continents = $countries_obj->get_continents();
|
||||||
|
foreach ( $continents as $code => $continent ) {
|
||||||
|
$locations[] = array(
|
||||||
|
'type' => 'continent',
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $continent['name'],
|
||||||
|
'label' => sprintf( '%s (Continent)', $continent['name'] ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add countries
|
||||||
|
foreach ( $countries as $code => $name ) {
|
||||||
|
$locations[] = array(
|
||||||
|
'type' => 'country',
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'label' => $name,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add states for this country if available
|
||||||
|
if ( isset( $states[ $code ] ) && ! empty( $states[ $code ] ) ) {
|
||||||
|
foreach ( $states[ $code ] as $state_code => $state_name ) {
|
||||||
|
$locations[] = array(
|
||||||
|
'type' => 'state',
|
||||||
|
'code' => $code . ':' . $state_code,
|
||||||
|
'name' => $state_name,
|
||||||
|
'label' => sprintf( '%s — %s', $name, $state_name ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_REST_Response( $locations, 200 );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has permission to manage shipping
|
* Check if user has permission to manage shipping
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user