import React, { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { api } from '@/lib/api'; import { SettingsLayout } from './components/SettingsLayout'; import { SettingsCard } from './components/SettingsCard'; import { ToggleField } from './components/ToggleField'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; import { Globe, Truck, MapPin, Edit, Trash2, RefreshCw, Loader2, ExternalLink, Settings, Plus, X, ChevronDown } from 'lucide-react'; import { toast } from 'sonner'; import { __ } from '@/lib/i18n'; import { useMediaQuery } from '@/hooks/use-media-query'; interface ShippingRate { id: string; name: string; price: string; condition?: string; transitTime?: string; } interface ShippingZone { id: string; name: string; regions: string; rates: ShippingRate[]; } export default function ShippingPage() { const queryClient = useQueryClient(); const wcAdminUrl = (window as any).WNW_CONFIG?.wpAdminUrl || '/wp-admin'; const [togglingMethod, setTogglingMethod] = useState(null); const [selectedZone, setSelectedZone] = useState(null); const [showAddMethod, setShowAddMethod] = useState(false); const [showAvailableMethods, setShowAvailableMethods] = useState(false); const [expandedMethod, setExpandedMethod] = useState(''); 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 [regionSearch, setRegionSearch] = useState(''); const isDesktop = useMediaQuery("(min-width: 768px)"); // Fetch shipping zones from WooCommerce const { data: zones = [], isLoading, refetch } = useQuery({ queryKey: ['shipping-zones'], queryFn: () => api.get('/settings/shipping/zones'), staleTime: 5 * 60 * 1000, // 5 minutes }); // Fetch available shipping methods const { data: availableMethods = [] } = useQuery({ queryKey: ['available-shipping-methods'], queryFn: () => api.get('/settings/shipping/methods/available'), enabled: showAvailableMethods, }); // Fetch available locations (countries/states) for zone regions const { data: availableLocations = [] } = useQuery({ queryKey: ['available-locations'], queryFn: () => api.get('/settings/shipping/locations'), enabled: showAddZone || !!editingZone, }); // Sync selectedZone with zones data when it changes useEffect(() => { if (selectedZone && zones && zones.length > 0) { const updatedZone = zones.find((z: any) => z.id === selectedZone.id); if (updatedZone && JSON.stringify(updatedZone) !== JSON.stringify(selectedZone)) { setSelectedZone(updatedZone); } } }, [zones, selectedZone]); // Toggle shipping method mutation const toggleMutation = useMutation({ mutationFn: async ({ zoneId, instanceId, enabled }: { zoneId: number; instanceId: number; enabled: boolean }) => { return api.post(`/settings/shipping/zones/${zoneId}/methods/${instanceId}/toggle`, { enabled }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shipping-zones'] }); toast.success(__('Shipping method updated successfully')); }, onError: (error: any) => { toast.error(error?.message || __('Failed to update shipping method')); }, onSettled: () => { setTogglingMethod(null); }, }); // Add shipping method mutation const addMethodMutation = useMutation({ mutationFn: async ({ zoneId, methodId }: { zoneId: number; methodId: string }) => { return api.post(`/settings/shipping/zones/${zoneId}/methods`, { method_id: methodId }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shipping-zones'] }); toast.success(__('Shipping method added successfully')); setShowAddMethod(false); }, onError: (error: any) => { toast.error(error?.message || __('Failed to add shipping method')); }, }); // Delete shipping method mutation const deleteMethodMutation = useMutation({ mutationFn: async ({ zoneId, instanceId }: { zoneId: number; instanceId: number }) => { return api.del(`/settings/shipping/zones/${zoneId}/methods/${instanceId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['shipping-zones'] }); toast.success(__('Delivery option deleted')); }, onError: (error: any) => { toast.error(error?.message || __('Failed to delete delivery option')); }, }); // Fetch method settings when accordion expands const fetchMethodSettings = async (instanceId: number) => { if (!selectedZone || methodSettings[instanceId]) return; try { const settings = await api.get(`/settings/shipping/zones/${selectedZone.id}/methods/${instanceId}/settings`); setMethodSettings(prev => ({ ...prev, [instanceId]: settings })); } catch (error) { console.error('Failed to fetch method settings:', error); } }; // Update method settings mutation const updateSettingsMutation = useMutation({ mutationFn: async ({ zoneId, instanceId, settings }: { zoneId: number; instanceId: number; settings: any }) => { return api.post(`/settings/shipping/zones/${zoneId}/methods/${instanceId}/settings`, { settings }); }, onSuccess: (_, variables) => { queryClient.invalidateQueries({ queryKey: ['shipping-zones'] }); // Clear cached settings to force refetch setMethodSettings(prev => { const newSettings = { ...prev }; delete newSettings[variables.instanceId]; return newSettings; }); toast.success(__('Settings saved')); setExpandedMethod(''); }, onError: (error: any) => { toast.error(error?.message || __('Failed to save settings')); }, }); const handleToggle = (zoneId: number, instanceId: number, enabled: boolean) => { setTogglingMethod(`${zoneId}-${instanceId}`); toggleMutation.mutate({ zoneId, instanceId, enabled }); }; const handleAddMethod = (methodId: string) => { if (selectedZone) { addMethodMutation.mutate({ zoneId: selectedZone.id, methodId }); } }; const handleDeleteMethod = (instanceId: number, methodName: string) => { if (selectedZone) { setDeletingMethod({ zoneId: selectedZone.id, instanceId, name: methodName }); } }; const confirmDelete = () => { if (deletingMethod) { deleteMethodMutation.mutate({ zoneId: deletingMethod.zoneId, instanceId: deletingMethod.instanceId }); setDeletingMethod(null); } }; // 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 (
); } return ( refetch()} disabled={isLoading} > {__('Refresh')} } > {/* Shipping Zones */} setShowAddZone(true)} > {__('Add Zone')} } > {zones.length === 0 ? (

{__('No shipping zones configured yet. Create your first zone to start offering shipping.')}

) : (
{zones.map((zone: any) => (

{zone.name}

{__('Available to:')} {zone.regions}

{zone.rates.length} {zone.rates.length === 1 ? __('delivery option') : __('delivery options')}

{/* Shipping Rates */}
{zone.rates?.map((rate: any) => (
{rate.transitTime && ( • {rate.transitTime} )} {rate.condition && ( • {rate.condition} )}
handleToggle(zone.id, rate.instance_id, checked)} disabled={togglingMethod === `${zone.id}-${rate.instance_id}`} />
))}
))}
)}
{/* Help Card */}

💡 {__('Shipping tips')}

  • • {__('Offer free shipping for orders above a certain amount to increase average order value')}
  • • {__('Provide multiple shipping options to give customers flexibility')}
  • • {__('Set realistic delivery estimates to manage customer expectations')}
  • • {__('Configure detailed shipping settings in WooCommerce for full control')}
{/* Settings Modal/Drawer */} {selectedZone && ( isDesktop ? ( !open && setSelectedZone(null)}> {selectedZone.name}

{selectedZone.regions}

{/* Add Delivery Option Button */} {!showAvailableMethods ? ( ) : (

{__('Available Delivery Options')}

{availableMethods.map((method: any) => ( ))}
)} {/* Delivery Options Accordion */} { setExpandedMethod(value); if (value) { const instanceId = parseInt(value); fetchMethodSettings(instanceId); } }}> {selectedZone.rates?.map((rate: any) => (
{rate.enabled ? __('On') : __('Off')}
{methodSettings[rate.instance_id] ? (

{__('Name shown to customers at checkout')}

{methodSettings[rate.instance_id].settings?.cost && (
{methodSettings[rate.instance_id].settings.cost.description && (

)}

)} {methodSettings[rate.instance_id].settings?.min_amount && (
{methodSettings[rate.instance_id].settings.min_amount.description && (

)}

)}
) : (
)}
))}
) : ( !open && setSelectedZone(null)}> {selectedZone.name}

{selectedZone.regions}

{/* Add Delivery Option Button */} {!showAvailableMethods ? ( ) : (

{__('Available Delivery Options')}

{availableMethods.map((method: any) => ( ))}
)} {/* Delivery Options Accordion (Mobile) */} { setExpandedMethod(value); if (value) { const instanceId = parseInt(value); fetchMethodSettings(instanceId); } }}> {selectedZone.rates?.map((rate: any) => (
{rate.enabled ? __('On') : __('Off')}
{methodSettings[rate.instance_id] ? (
{methodSettings[rate.instance_id].settings?.cost && (
{methodSettings[rate.instance_id].settings.cost.description && (

)}

)} {methodSettings[rate.instance_id].settings?.min_amount && (
)}
) : (
)}
))}
) )} {/* Delete Method Confirmation Dialog */} setDeletingMethod(null)}> {__('Delete Shipping Method?')} {__('Are you sure you want to delete')} {deletingMethod?.name}? {' '}{__('This action cannot be undone.')} {__('Cancel')} {__('Delete')} {/* Delete Zone Confirmation Dialog */} setDeletingZone(null)}> {__('Delete Shipping Zone?')} {__('Are you sure you want to delete')} {deletingZone?.name}? {' '}{__('All shipping methods in this zone will also be deleted. This action cannot be undone.')} {__('Cancel')} { if (deletingZone) { deleteZoneMutation.mutate(deletingZone.id); } }} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {__('Delete Zone')} {/* Add/Edit Zone Dialog */} { if (!open) { setShowAddZone(false); setEditingZone(null); setRegionSearch(''); } }}> {editingZone ? __('Edit Zone') : __('Add Shipping Zone')}
{ e.preventDefault(); const formData = new FormData(e.currentTarget); const zoneName = formData.get('name') as string; const selectedRegions = formData.getAll('regions').map(code => { const location = availableLocations.find((l: any) => l.code === code); return location ? { code: location.code, type: location.type } : null; }).filter(Boolean); if (editingZone) { updateZoneMutation.mutate({ zoneId: editingZone.id, name: zoneName, regions: selectedRegions, }); } else { createZoneMutation.mutate({ name: zoneName, regions: selectedRegions, }); } }} className="space-y-4">

{__('Select countries, states, or continents for this zone')}

{/* Search Filter */} setRegionSearch(e.target.value)} className="w-full px-3 py-2 border rounded-md mb-2" />
{availableLocations.length === 0 ? (
{__('Loading locations...')}
) : (
{availableLocations .filter((location: any) => location.label.toLowerCase().includes(regionSearch.toLowerCase()) ) .map((location: any) => ( ))} {availableLocations.filter((location: any) => location.label.toLowerCase().includes(regionSearch.toLowerCase()) ).length === 0 && (
{__('No regions found')}
)}
)}
); }