Files
WooNooW/admin-spa/src/routes/Settings/Shipping.tsx
dwindown d2350852ef 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
2025-11-10 08:24:25 +07:00

789 lines
37 KiB
TypeScript

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<string | null>(null);
const [selectedZone, setSelectedZone] = useState<any | null>(null);
const [showAddMethod, setShowAddMethod] = useState(false);
const [expandedMethod, setExpandedMethod] = useState<string>('');
const [methodSettings, setMethodSettings] = useState<Record<string, any>>({});
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)");
// 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: showAddMethod,
});
// 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 (
<SettingsLayout
title={__('Shipping & Delivery')}
description={__('Manage how you ship products to customers')}
>
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
</SettingsLayout>
);
}
return (
<SettingsLayout
title={__('Shipping & Delivery')}
description={__('Manage how you ship products to customers')}
action={
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
disabled={isLoading}
>
<RefreshCw className="h-4 w-4 mr-2" />
{__('Refresh')}
</Button>
}
>
{/* Shipping Zones */}
<SettingsCard
title={__('Shipping Zones')}
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 ? (
<div className="text-center py-8">
<p className="text-sm text-muted-foreground mb-4">
{__('No shipping zones configured yet. Create your first zone to start offering shipping.')}
</p>
<Button
onClick={() => setShowAddZone(true)}
>
<Plus className="h-4 w-4 mr-2" />
{__('Create First Zone')}
</Button>
</div>
) : (
<div className="space-y-4">
{zones.map((zone: any) => (
<div
key={zone.id}
className="border rounded-lg p-3 md:p-4 hover:border-primary/50 transition-colors"
>
<div className="flex items-start justify-between gap-3 mb-3 md:mb-4">
<div className="flex items-start gap-2 md:gap-3 flex-1">
<div className="p-1.5 md:p-2 bg-primary/10 rounded-lg text-primary flex-shrink-0">
<Globe className="h-4 w-4 md:h-5 md:w-5" />
</div>
<div className="min-w-0 flex-1">
<h3 className="font-semibold text-base md:text-lg">{zone.name}</h3>
<p className="text-xs md:text-sm text-muted-foreground truncate">
{zone.regions}
</p>
<p className="text-xs md:text-sm text-muted-foreground">
{zone.rates.length} {zone.rates.length === 1 ? 'method' : 'methods'}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedZone(zone)}
>
<Edit className="h-4 w-4" />
</Button>
</div>
{/* Shipping Rates */}
<div className="pl-0 md:pl-11 space-y-2">
{zone.rates?.map((rate: any) => (
<div
key={rate.id}
className="flex items-center justify-between gap-2 py-2 px-2 md:px-3 bg-muted/50 rounded-md"
>
<div className="flex items-center gap-1.5 md:gap-2 flex-1 min-w-0">
<Truck className="h-3.5 w-3.5 md:h-4 md:w-4 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<span
className="text-xs md:text-sm font-medium line-clamp-1"
dangerouslySetInnerHTML={{ __html: rate.name }}
/>
{rate.transitTime && (
<span className="text-xs text-muted-foreground ml-2">
{rate.transitTime}
</span>
)}
{rate.condition && (
<span className="text-xs text-muted-foreground ml-2">
{rate.condition}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 md:gap-3 flex-shrink-0">
<span
className="text-xs md:text-sm font-semibold whitespace-nowrap"
dangerouslySetInnerHTML={{ __html: rate.price }}
/>
<ToggleField
id={`${zone.id}-${rate.instance_id}`}
label=""
checked={rate.enabled}
onCheckedChange={(checked) => handleToggle(zone.id, rate.instance_id, checked)}
disabled={togglingMethod === `${zone.id}-${rate.instance_id}`}
/>
</div>
</div>
))}
</div>
</div>
))}
<Button
variant="outline"
className="w-full"
asChild
>
<a href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=shipping`} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4 mr-2" />
{__('Manage Zones in WooCommerce')}
</a>
</Button>
</div>
)}
</SettingsCard>
{/* Help Card */}
<div className="bg-muted/50 border rounded-lg p-4">
<p className="text-sm font-medium mb-2">💡 {__('Shipping tips')}</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> {__('Offer free shipping for orders above a certain amount to increase average order value')}</li>
<li> {__('Provide multiple shipping options to give customers flexibility')}</li>
<li> {__('Set realistic delivery estimates to manage customer expectations')}</li>
<li> {__('Configure detailed shipping settings in WooCommerce for full control')}</li>
</ul>
</div>
{/* Settings Modal/Drawer */}
{selectedZone && (
isDesktop ? (
<Dialog open={!!selectedZone} onOpenChange={(open) => !open && setSelectedZone(null)}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col p-0">
<DialogHeader className="px-6 py-4 border-b">
<DialogTitle>{selectedZone.name}</DialogTitle>
<p className="text-sm text-muted-foreground mt-1">
{selectedZone.regions}
</p>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6 min-h-0">
<div className="space-y-4">
{/* Add Delivery Option Button */}
<Button
variant="outline"
className="w-full"
onClick={() => setShowAddMethod(true)}
>
<Plus className="h-4 w-4 mr-2" />
{__('Add Delivery Option')}
</Button>
{/* Delivery Options Accordion */}
<Accordion type="single" collapsible value={expandedMethod} onValueChange={(value) => {
setExpandedMethod(value);
if (value) {
const instanceId = parseInt(value);
fetchMethodSettings(instanceId);
}
}}>
{selectedZone.rates?.map((rate: any) => (
<AccordionItem key={rate.id} value={String(rate.instance_id)} className="border rounded-lg mb-2">
<AccordionTrigger className="hover:no-underline px-4">
<div className="flex items-center gap-3 flex-1">
<Truck className="h-4 w-4 text-muted-foreground" />
<div className="flex-1 text-left">
<div className="font-medium" dangerouslySetInnerHTML={{ __html: rate.name }} />
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1">
<span className="font-semibold" dangerouslySetInnerHTML={{ __html: rate.price }} />
<span className={`text-xs px-2 py-0.5 rounded-full ${
rate.enabled
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-600'
}`}>
{rate.enabled ? __('On') : __('Off')}
</span>
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="px-4 pb-4">
{methodSettings[rate.instance_id] ? (
<div className="space-y-4 pt-2">
<div>
<label className="text-sm font-medium mb-2 block">
{__('Display Name')}
</label>
<input
className="w-full px-3 py-2 border rounded-md"
defaultValue={methodSettings[rate.instance_id].settings?.title?.value || ''}
id={`method-title-${rate.instance_id}`}
/>
<p className="text-xs text-muted-foreground mt-1">
{__('Name shown to customers at checkout')}
</p>
</div>
{methodSettings[rate.instance_id].settings?.cost && (
<div>
<label className="text-sm font-medium mb-2 block">
{methodSettings[rate.instance_id].settings.cost.title || __('Cost')}
</label>
<input
className="w-full px-3 py-2 border rounded-md"
defaultValue={methodSettings[rate.instance_id].settings.cost.value || ''}
id={`method-cost-${rate.instance_id}`}
placeholder={methodSettings[rate.instance_id].settings.cost.placeholder || '0'}
/>
{methodSettings[rate.instance_id].settings.cost.description && (
<p
className="text-xs text-muted-foreground mt-1"
dangerouslySetInnerHTML={{ __html: methodSettings[rate.instance_id].settings.cost.description }}
/>
)}
</div>
)}
{methodSettings[rate.instance_id].settings?.min_amount && (
<div>
<label className="text-sm font-medium mb-2 block">
{methodSettings[rate.instance_id].settings.min_amount.title || __('Minimum Order')}
</label>
<input
className="w-full px-3 py-2 border rounded-md"
defaultValue={methodSettings[rate.instance_id].settings.min_amount.value || ''}
id={`method-min-amount-${rate.instance_id}`}
placeholder={methodSettings[rate.instance_id].settings.min_amount.placeholder || '0'}
/>
{methodSettings[rate.instance_id].settings.min_amount.description && (
<p
className="text-xs text-muted-foreground mt-1"
dangerouslySetInnerHTML={{ __html: methodSettings[rate.instance_id].settings.min_amount.description }}
/>
)}
</div>
)}
<div className="flex justify-between gap-2 pt-2">
<Button
variant="destructive"
size="sm"
onClick={() => {
handleDeleteMethod(rate.instance_id, rate.title);
setExpandedMethod('');
}}
disabled={deleteMethodMutation.isPending}
>
<Trash2 className="h-4 w-4 mr-1" />
{__('Remove')}
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setExpandedMethod('')}
>
{__('Cancel')}
</Button>
<Button
size="sm"
onClick={() => {
const settings: any = {};
const titleInput = document.getElementById(`method-title-${rate.instance_id}`) as HTMLInputElement;
const costInput = document.getElementById(`method-cost-${rate.instance_id}`) as HTMLInputElement;
const minAmountInput = document.getElementById(`method-min-amount-${rate.instance_id}`) as HTMLInputElement;
if (titleInput) settings.title = titleInput.value;
if (costInput) settings.cost = costInput.value;
if (minAmountInput) settings.min_amount = minAmountInput.value;
updateSettingsMutation.mutate({
zoneId: selectedZone.id,
instanceId: rate.instance_id,
settings
});
}}
disabled={updateSettingsMutation.isPending}
>
{updateSettingsMutation.isPending ? __('Saving...') : __('Save')}
</Button>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
<div className="px-6 py-4 border-t flex justify-between gap-3">
<Button
variant="outline"
asChild
>
<a href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=shipping&zone_id=${selectedZone.id}`} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4 mr-2" />
{__('Edit in WooCommerce')}
</a>
</Button>
<Button onClick={() => setSelectedZone(null)}>
{__('Done')}
</Button>
</div>
</DialogContent>
</Dialog>
) : (
<Drawer open={!!selectedZone} onOpenChange={(open) => !open && setSelectedZone(null)}>
<DrawerContent className="max-h-[90vh] flex flex-col">
<DrawerHeader className="border-b">
<DrawerTitle>{selectedZone.name}</DrawerTitle>
<p className="text-sm text-muted-foreground mt-1">
{selectedZone.regions}
</p>
</DrawerHeader>
<div className="flex-1 overflow-y-auto px-4 py-4 min-h-0">
<div className="space-y-3">
{/* Add Delivery Option Button */}
<Button
variant="outline"
className="w-full"
onClick={() => setShowAddMethod(true)}
>
<Plus className="h-4 w-4 mr-2" />
{__('Add Delivery Option')}
</Button>
{/* Delivery Options Accordion (Mobile) */}
<Accordion type="single" collapsible value={expandedMethod} onValueChange={(value) => {
setExpandedMethod(value);
if (value) {
const instanceId = parseInt(value);
fetchMethodSettings(instanceId);
}
}}>
{selectedZone.rates?.map((rate: any) => (
<AccordionItem key={rate.id} value={String(rate.instance_id)} className="border rounded-lg mb-2">
<AccordionTrigger className="hover:no-underline px-3 py-2">
<div className="flex items-center gap-2 flex-1 min-w-0">
<Truck className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
<div className="flex-1 text-left min-w-0">
<div className="font-medium text-sm line-clamp-1" dangerouslySetInnerHTML={{ __html: rate.name }} />
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
<span className="font-semibold" dangerouslySetInnerHTML={{ __html: rate.price }} />
<span className={`px-1.5 py-0.5 rounded-full whitespace-nowrap ${
rate.enabled
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-600'
}`}>
{rate.enabled ? __('On') : __('Off')}
</span>
</div>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="px-3 pb-3">
{methodSettings[rate.instance_id] ? (
<div className="space-y-3 pt-2">
<div>
<label className="text-xs font-medium mb-1.5 block">
{__('Display Name')}
</label>
<input
className="w-full px-2.5 py-1.5 text-sm border rounded-md"
defaultValue={methodSettings[rate.instance_id].settings?.title?.value || ''}
id={`method-title-mobile-${rate.instance_id}`}
/>
</div>
{methodSettings[rate.instance_id].settings?.cost && (
<div>
<label className="text-xs font-medium mb-1.5 block">
{methodSettings[rate.instance_id].settings.cost.title || __('Cost')}
</label>
<input
className="w-full px-2.5 py-1.5 text-sm border rounded-md"
defaultValue={methodSettings[rate.instance_id].settings.cost.value || ''}
id={`method-cost-mobile-${rate.instance_id}`}
placeholder={methodSettings[rate.instance_id].settings.cost.placeholder || '0'}
/>
{methodSettings[rate.instance_id].settings.cost.description && (
<p
className="text-xs text-muted-foreground mt-1"
dangerouslySetInnerHTML={{ __html: methodSettings[rate.instance_id].settings.cost.description }}
/>
)}
</div>
)}
{methodSettings[rate.instance_id].settings?.min_amount && (
<div>
<label className="text-xs font-medium mb-1.5 block">
{methodSettings[rate.instance_id].settings.min_amount.title || __('Minimum Order')}
</label>
<input
className="w-full px-2.5 py-1.5 text-sm border rounded-md"
defaultValue={methodSettings[rate.instance_id].settings.min_amount.value || ''}
id={`method-min-amount-mobile-${rate.instance_id}`}
placeholder={methodSettings[rate.instance_id].settings.min_amount.placeholder || '0'}
/>
</div>
)}
<div className="flex justify-between gap-2 pt-1">
<Button
variant="destructive"
size="sm"
onClick={() => {
handleDeleteMethod(rate.instance_id, rate.title);
setExpandedMethod('');
}}
disabled={deleteMethodMutation.isPending}
>
<Trash2 className="h-3 w-3 mr-1" />
{__('Remove')}
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setExpandedMethod('')}
>
{__('Cancel')}
</Button>
<Button
size="sm"
onClick={() => {
const settings: any = {};
const titleInput = document.getElementById(`method-title-mobile-${rate.instance_id}`) as HTMLInputElement;
const costInput = document.getElementById(`method-cost-mobile-${rate.instance_id}`) as HTMLInputElement;
const minAmountInput = document.getElementById(`method-min-amount-mobile-${rate.instance_id}`) as HTMLInputElement;
if (titleInput) settings.title = titleInput.value;
if (costInput) settings.cost = costInput.value;
if (minAmountInput) settings.min_amount = minAmountInput.value;
updateSettingsMutation.mutate({
zoneId: selectedZone.id,
instanceId: rate.instance_id,
settings
});
}}
disabled={updateSettingsMutation.isPending}
>
{updateSettingsMutation.isPending ? __('Saving...') : __('Save')}
</Button>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center py-3">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
</div>
<div className="px-4 py-4 border-t flex flex-col gap-2">
<Button
variant="outline"
asChild
className="w-full"
>
<a href={`${wcAdminUrl}/admin.php?page=wc-settings&tab=shipping&zone_id=${selectedZone.id}`} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4 mr-2" />
{__('Edit in WooCommerce')}
</a>
</Button>
<Button onClick={() => setSelectedZone(null)} className="w-full">
{__('Done')}
</Button>
</div>
</DrawerContent>
</Drawer>
)
)}
{/* Add Delivery Option Dialog */}
<Dialog open={showAddMethod} onOpenChange={setShowAddMethod}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{__('Add Delivery Option')}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">
{__('Select a delivery option to add:')}</p>
<div className="space-y-2">
{availableMethods.map((method: any) => (
<button
key={method.id}
onClick={() => handleAddMethod(method.id)}
disabled={addMethodMutation.isPending}
className="w-full text-left p-4 border rounded-lg hover:border-primary hover:bg-accent transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<div className="font-medium">{method.title}</div>
{method.description && (
<div className="text-sm text-muted-foreground mt-1">
{method.description}
</div>
)}
</button>
))}
</div>
</div>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<AlertDialog open={!!deletingMethod} onOpenChange={() => setDeletingMethod(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{__('Delete Shipping Method?')}</AlertDialogTitle>
<AlertDialogDescription>
{__('Are you sure you want to delete')} <strong>{deletingMethod?.name}</strong>?
{' '}{__('This action cannot be undone.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{__('Cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{__('Delete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</SettingsLayout>
);
}