diff --git a/admin-spa/src/components/ui/drawer.tsx b/admin-spa/src/components/ui/drawer.tsx index c17b0cc..8f4fe09 100644 --- a/admin-spa/src/components/ui/drawer.tsx +++ b/admin-spa/src/components/ui/drawer.tsx @@ -26,7 +26,7 @@ const DrawerOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -41,7 +41,7 @@ const DrawerContent = React.forwardRef< (''); const [methodSettings, setMethodSettings] = useState>({}); const [deletingMethod, setDeletingMethod] = useState<{ zoneId: number; instanceId: number; name: string } | null>(null); + const [showAddZone, setShowAddZone] = useState(false); + const [editingZone, setEditingZone] = useState(null); + const [deletingZone, setDeletingZone] = useState(null); const isDesktop = useMediaQuery("(min-width: 768px)"); // 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) { return ( setShowAddZone(true)} + > + + {__('Add Zone')} + + } > {zones.length === 0 ? ( - {__('No shipping zones configured yet.')} + {__('No shipping zones configured yet. Create your first zone to start offering shipping.')} setShowAddZone(true)} > - - {__('Configure in WooCommerce')} - + + {__('Create First Zone')} ) : ( diff --git a/admin-spa/src/routes/Settings/components/SettingsCard.tsx b/admin-spa/src/routes/Settings/components/SettingsCard.tsx index aaf556a..1ae04e0 100644 --- a/admin-spa/src/routes/Settings/components/SettingsCard.tsx +++ b/admin-spa/src/routes/Settings/components/SettingsCard.tsx @@ -6,14 +6,20 @@ interface SettingsCardProps { description?: string; children: React.ReactNode; className?: string; + action?: React.ReactNode; } -export function SettingsCard({ title, description, children, className = '' }: SettingsCardProps) { +export function SettingsCard({ title, description, children, className = '', action }: SettingsCardProps) { return ( - {title} - {description && {description}} + + + {title} + {description && {description}} + + {action && {action}} + {children} diff --git a/includes/Api/ShippingController.php b/includes/Api/ShippingController.php index 873dca1..b584040 100644 --- a/includes/Api/ShippingController.php +++ b/includes/Api/ShippingController.php @@ -30,6 +30,60 @@ class ShippingController extends WP_REST_Controller { 'callback' => array( $this, 'get_zones' ), '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\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 */
- {__('No shipping zones configured yet.')} + {__('No shipping zones configured yet. Create your first zone to start offering shipping.')}