feat: Tax route fix + Local Pickup + Email/Notifications settings
## 1. Fixed Tax Settings Route ✅ - Changed /settings/taxes → /settings/tax in nav tree - Now matches App.tsx route - Tax page now loads correctly ## 2. Advanced Local Pickup ✅ Frontend (LocalPickup.tsx): - Add/edit/delete pickup locations - Enable/disable locations - Full address fields (street, city, state, postcode) - Phone number and business hours - Clean modal UI for adding locations Backend (PickupLocationsController.php): - GET /settings/pickup-locations - POST /settings/pickup-locations (create) - POST /settings/pickup-locations/:id (update) - DELETE /settings/pickup-locations/:id - POST /settings/pickup-locations/:id/toggle - Stores in wp_options as array ## 3. Email/Notifications Settings ✅ Frontend (Notifications.tsx): - List all WooCommerce emails - Separate customer vs admin emails - Enable/disable toggle for each email - Show from name/email - Link to WooCommerce for advanced config Backend (EmailController.php): - GET /settings/emails - List all emails - POST /settings/emails/:id/toggle - Enable/disable - Uses WC()->mailer()->get_emails() - Auto-detects recipient type (customer/admin) ## Features: ✅ Simple, non-tech-savvy UI ✅ All CRUD operations ✅ Real-time updates ✅ Links to WooCommerce for advanced settings ✅ Mobile responsive Next: Test all settings pages
This commit is contained in:
374
admin-spa/src/routes/Settings/LocalPickup.tsx
Normal file
374
admin-spa/src/routes/Settings/LocalPickup.tsx
Normal file
@@ -0,0 +1,374 @@
|
||||
import React, { useState } 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 { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { MapPin, Plus, Trash2, RefreshCw, Edit } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { __ } from '@/lib/i18n';
|
||||
|
||||
interface PickupLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
phone?: string;
|
||||
hours?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export default function LocalPickupSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingLocation, setEditingLocation] = useState<PickupLocation | null>(null);
|
||||
|
||||
// Fetch pickup locations
|
||||
const { data: locations = [], isLoading, refetch } = useQuery({
|
||||
queryKey: ['pickup-locations'],
|
||||
queryFn: () => api.get('/settings/pickup-locations'),
|
||||
});
|
||||
|
||||
// Save location mutation
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: async (location: Partial<PickupLocation>) => {
|
||||
if (location.id) {
|
||||
return api.post(`/settings/pickup-locations/${location.id}`, location);
|
||||
}
|
||||
return api.post('/settings/pickup-locations', location);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pickup-locations'] });
|
||||
toast.success(__('Pickup location saved'));
|
||||
setShowDialog(false);
|
||||
setEditingLocation(null);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || __('Failed to save location'));
|
||||
},
|
||||
});
|
||||
|
||||
// Delete location mutation
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
return api.del(`/settings/pickup-locations/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pickup-locations'] });
|
||||
toast.success(__('Pickup location deleted'));
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast.error(error?.message || __('Failed to delete location'));
|
||||
},
|
||||
});
|
||||
|
||||
// Toggle location mutation
|
||||
const toggleMutation = useMutation({
|
||||
mutationFn: async ({ id, enabled }: { id: string; enabled: boolean }) => {
|
||||
return api.post(`/settings/pickup-locations/${id}/toggle`, { enabled });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['pickup-locations'] });
|
||||
toast.success(__('Location updated'));
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
const location: Partial<PickupLocation> = {
|
||||
id: editingLocation?.id,
|
||||
name: formData.get('name') as string,
|
||||
address: formData.get('address') as string,
|
||||
city: formData.get('city') as string,
|
||||
state: formData.get('state') as string,
|
||||
postcode: formData.get('postcode') as string,
|
||||
phone: formData.get('phone') as string,
|
||||
hours: formData.get('hours') as string,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
saveMutation.mutate(location);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Local Pickup Locations')}
|
||||
description={__('Manage pickup locations for local pickup orders')}
|
||||
>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsLayout
|
||||
title={__('Local Pickup Locations')}
|
||||
description={__('Manage pickup locations for local pickup orders')}
|
||||
action={
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-2" />
|
||||
{__('Refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingLocation(null);
|
||||
setShowDialog(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{__('Add Location')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Info Card */}
|
||||
<SettingsCard
|
||||
title={__('About Local Pickup')}
|
||||
description={__('Allow customers to pick up orders from your physical locations')}
|
||||
>
|
||||
<div className="text-sm text-muted-foreground space-y-2">
|
||||
<p>
|
||||
{__('Add multiple pickup locations where customers can collect their orders. Each location can have its own address, phone number, and business hours.')}
|
||||
</p>
|
||||
<p>
|
||||
{__('Customers will see available pickup locations during checkout and can choose their preferred location.')}
|
||||
</p>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Pickup Locations List */}
|
||||
<SettingsCard
|
||||
title={__('Pickup Locations')}
|
||||
description={locations.length === 0
|
||||
? __('No pickup locations configured yet')
|
||||
: __(`${locations.length} location(s) configured`)}
|
||||
>
|
||||
{locations.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<MapPin className="h-12 w-12 mx-auto text-muted-foreground mb-3" />
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{__('Add your first pickup location to get started')}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setEditingLocation(null);
|
||||
setShowDialog(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{__('Add Location')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{locations.map((location: PickupLocation) => (
|
||||
<div
|
||||
key={location.id}
|
||||
className="rounded-lg border p-4 hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<MapPin className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="font-medium">{location.name}</h3>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
location.enabled
|
||||
? 'bg-green-100 text-green-700'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}>
|
||||
{location.enabled ? __('Active') : __('Inactive')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<p>{location.address}</p>
|
||||
<p>{location.city}, {location.state} {location.postcode}</p>
|
||||
{location.phone && <p>📞 {location.phone}</p>}
|
||||
{location.hours && <p>🕐 {location.hours}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleMutation.mutate({
|
||||
id: location.id,
|
||||
enabled: !location.enabled
|
||||
})}
|
||||
disabled={toggleMutation.isPending}
|
||||
>
|
||||
{location.enabled ? __('Disable') : __('Enable')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingLocation(location);
|
||||
setShowDialog(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (confirm(__('Are you sure you want to delete this location?'))) {
|
||||
deleteMutation.mutate(location.id);
|
||||
}
|
||||
}}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</SettingsCard>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Location Dialog */}
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingLocation ? __('Edit Pickup Location') : __('Add Pickup Location')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('Location Name')} *
|
||||
</label>
|
||||
<input
|
||||
name="name"
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.name}
|
||||
placeholder={__('e.g., Main Store, Warehouse, Downtown Branch')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('Street Address')} *
|
||||
</label>
|
||||
<input
|
||||
name="address"
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.address}
|
||||
placeholder={__('123 Main Street')}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('City')} *
|
||||
</label>
|
||||
<input
|
||||
name="city"
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.city}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('State/Province')} *
|
||||
</label>
|
||||
<input
|
||||
name="state"
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.state}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('Postcode')} *
|
||||
</label>
|
||||
<input
|
||||
name="postcode"
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.postcode}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('Phone Number')}
|
||||
</label>
|
||||
<input
|
||||
name="phone"
|
||||
type="tel"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.phone}
|
||||
placeholder={__('(123) 456-7890')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-1.5 block">
|
||||
{__('Business Hours')}
|
||||
</label>
|
||||
<input
|
||||
name="hours"
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-md"
|
||||
defaultValue={editingLocation?.hours}
|
||||
placeholder={__('Mon-Fri: 9AM-5PM, Sat: 10AM-2PM')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowDialog(false);
|
||||
setEditingLocation(null);
|
||||
}}
|
||||
>
|
||||
{__('Cancel')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={saveMutation.isPending}>
|
||||
{saveMutation.isPending ? __('Saving...') : __('Save Location')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user