diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index dbc175e..c6f93fb 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -197,6 +197,8 @@ import SettingsStore from '@/routes/Settings/Store'; import SettingsPayments from '@/routes/Settings/Payments'; import SettingsShipping from '@/routes/Settings/Shipping'; import SettingsTax from '@/routes/Settings/Tax'; +import SettingsLocalPickup from '@/routes/Settings/LocalPickup'; +import SettingsNotifications from '@/routes/Settings/Notifications'; import MorePage from '@/routes/More'; // Addon Route Component - Dynamically loads addon components @@ -428,9 +430,10 @@ function AppRoutes() { } /> } /> } /> + } /> } /> } /> - } /> + } /> } /> {/* Dynamic Addon Routes */} diff --git a/admin-spa/src/nav/tree.ts b/admin-spa/src/nav/tree.ts index ff3f9cb..58c59b0 100644 --- a/admin-spa/src/nav/tree.ts +++ b/admin-spa/src/nav/tree.ts @@ -111,7 +111,7 @@ function getStaticFallbackTree(): MainNode[] { { label: 'Store Details', mode: 'spa' as const, path: '/settings/store' }, { label: 'Payments', mode: 'spa' as const, path: '/settings/payments' }, { label: 'Shipping & Delivery', mode: 'spa' as const, path: '/settings/shipping' }, - { label: 'Taxes', mode: 'spa' as const, path: '/settings/taxes' }, + { label: 'Tax', mode: 'spa' as const, path: '/settings/tax' }, { label: 'Checkout', mode: 'spa' as const, path: '/settings/checkout' }, { label: 'Customer Accounts', mode: 'spa' as const, path: '/settings/customers' }, { label: 'Notifications', mode: 'spa' as const, path: '/settings/notifications' }, diff --git a/admin-spa/src/routes/Settings/LocalPickup.tsx b/admin-spa/src/routes/Settings/LocalPickup.tsx new file mode 100644 index 0000000..69afb32 --- /dev/null +++ b/admin-spa/src/routes/Settings/LocalPickup.tsx @@ -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(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) => { + 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) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + const location: Partial = { + 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 ( + +
+ +
+
+ ); + } + + return ( + + + + + } + > +
+ {/* Info Card */} + +
+

+ {__('Add multiple pickup locations where customers can collect their orders. Each location can have its own address, phone number, and business hours.')} +

+

+ {__('Customers will see available pickup locations during checkout and can choose their preferred location.')} +

+
+
+ + {/* Pickup Locations List */} + + {locations.length === 0 ? ( +
+ +

+ {__('Add your first pickup location to get started')} +

+ +
+ ) : ( +
+ {locations.map((location: PickupLocation) => ( +
+
+
+
+ +

{location.name}

+ + {location.enabled ? __('Active') : __('Inactive')} + +
+ +
+

{location.address}

+

{location.city}, {location.state} {location.postcode}

+ {location.phone &&

📞 {location.phone}

} + {location.hours &&

🕐 {location.hours}

} +
+
+ +
+ + + +
+
+
+ ))} +
+ )} +
+
+ + {/* Add/Edit Location Dialog */} + + + + + {editingLocation ? __('Edit Pickup Location') : __('Add Pickup Location')} + + + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/admin-spa/src/routes/Settings/Notifications.tsx b/admin-spa/src/routes/Settings/Notifications.tsx new file mode 100644 index 0000000..29f8bff --- /dev/null +++ b/admin-spa/src/routes/Settings/Notifications.tsx @@ -0,0 +1,228 @@ +import React 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 { ExternalLink, RefreshCw, Mail } from 'lucide-react'; +import { toast } from 'sonner'; +import { __ } from '@/lib/i18n'; + +export default function NotificationsSettings() { + const queryClient = useQueryClient(); + const wcAdminUrl = (window as any).WNW_CONFIG?.wpAdminUrl || '/wp-admin'; + + // Fetch email settings + const { data: settings, isLoading, refetch } = useQuery({ + queryKey: ['email-settings'], + queryFn: () => api.get('/settings/emails'), + }); + + // Toggle email mutation + const toggleMutation = useMutation({ + mutationFn: async ({ emailId, enabled }: { emailId: string; enabled: boolean }) => { + return api.post(`/settings/emails/${emailId}/toggle`, { enabled }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['email-settings'] }); + toast.success(__('Email settings updated')); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to update email settings')); + }, + }); + + if (isLoading) { + return ( + +
+ +
+
+ ); + } + + const customerEmails = settings?.emails?.filter((e: any) => e.recipient === 'customer') || []; + const adminEmails = settings?.emails?.filter((e: any) => e.recipient === 'admin') || []; + + return ( + refetch()} + > + + {__('Refresh')} + + } + > +
+ {/* Customer Emails */} + +
+ {customerEmails.map((email: any) => ( +
+
+
+ +

{email.title}

+
+

{email.description}

+
+
+ toggleMutation.mutate({ + emailId: email.id, + enabled: checked + })} + disabled={toggleMutation.isPending} + /> + +
+
+ ))} +
+
+ + {/* Admin Emails */} + +
+ {adminEmails.map((email: any) => ( +
+
+
+ +

{email.title}

+
+

{email.description}

+
+
+ toggleMutation.mutate({ + emailId: email.id, + enabled: checked + })} + disabled={toggleMutation.isPending} + /> + +
+
+ ))} +
+
+ + {/* Email Sender Settings */} + +
+
+
+

{__('From Name')}

+

+ {settings?.from_name || __('Not configured')} +

+
+ +
+ +
+
+

{__('From Email')}

+

+ {settings?.from_email || __('Not configured')} +

+
+ +
+
+
+ + {/* Advanced Settings Link */} +
+

+ {__('For email templates, styling, and advanced configuration, use the WooCommerce settings page')} +

+ +
+
+
+ ); +} diff --git a/includes/Api/EmailController.php b/includes/Api/EmailController.php new file mode 100644 index 0000000..f7a1026 --- /dev/null +++ b/includes/Api/EmailController.php @@ -0,0 +1,144 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_settings' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + + // Toggle email + register_rest_route( + $namespace, + '/settings/emails/(?P[a-zA-Z0-9_-]+)/toggle', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'toggle_email' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + } + + /** + * Check permission + */ + public function check_permission() { + return current_user_can( 'manage_woocommerce' ); + } + + /** + * Get email settings + */ + public function get_settings( WP_REST_Request $request ) { + try { + $email_settings = array(); + $emails = \WC()->mailer()->get_emails(); + + foreach ( $emails as $email ) { + $recipient = 'customer'; + + // Determine recipient type + if ( strpos( $email->id, 'admin' ) !== false || + strpos( $email->id, 'new_order' ) !== false || + strpos( $email->id, 'cancelled_order' ) !== false || + strpos( $email->id, 'failed_order' ) !== false ) { + $recipient = 'admin'; + } + + $email_settings[] = array( + 'id' => $email->id, + 'title' => $email->get_title(), + 'description' => $email->get_description(), + 'enabled' => $email->is_enabled() ? 'yes' : 'no', + 'recipient' => $recipient, + ); + } + + $settings = array( + 'emails' => $email_settings, + 'from_name' => get_option( 'woocommerce_email_from_name' ), + 'from_email' => get_option( 'woocommerce_email_from_address' ), + ); + + return new WP_REST_Response( $settings, 200 ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( + 'error' => 'fetch_failed', + 'message' => $e->getMessage(), + ), + 500 + ); + } + } + + /** + * Toggle email + */ + public function toggle_email( WP_REST_Request $request ) { + try { + $email_id = $request->get_param( 'email_id' ); + $enabled = rest_sanitize_boolean( $request->get_param( 'enabled' ) ); + $emails = \WC()->mailer()->get_emails(); + + if ( ! isset( $emails[ $email_id ] ) ) { + return new WP_REST_Response( + array( + 'error' => 'not_found', + 'message' => __( 'Email not found', 'woonoow' ), + ), + 404 + ); + } + + $email = $emails[ $email_id ]; + + // Update email settings + $email->update_option( 'enabled', $enabled ? 'yes' : 'no' ); + + return new WP_REST_Response( + array( + 'success' => true, + 'enabled' => $enabled, + 'message' => $enabled + ? __( 'Email enabled', 'woonoow' ) + : __( 'Email disabled', 'woonoow' ), + ), + 200 + ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( + 'error' => 'toggle_failed', + 'message' => $e->getMessage(), + ), + 500 + ); + } + } +} diff --git a/includes/Api/PickupLocationsController.php b/includes/Api/PickupLocationsController.php new file mode 100644 index 0000000..568dfc0 --- /dev/null +++ b/includes/Api/PickupLocationsController.php @@ -0,0 +1,248 @@ + WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_locations' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_location' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + + // Update/Delete specific location + register_rest_route( + $namespace, + '/settings/pickup-locations/(?P[a-zA-Z0-9_-]+)', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_location' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_location' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + + // Toggle location + register_rest_route( + $namespace, + '/settings/pickup-locations/(?P[a-zA-Z0-9_-]+)/toggle', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'toggle_location' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + } + + /** + * Check permission + */ + public function check_permission() { + return current_user_can( 'manage_woocommerce' ); + } + + /** + * Get all pickup locations + */ + public function get_locations( WP_REST_Request $request ) { + $locations = get_option( 'woonoow_pickup_locations', array() ); + return new WP_REST_Response( array_values( $locations ), 200 ); + } + + /** + * Create pickup location + */ + public function create_location( WP_REST_Request $request ) { + try { + $locations = get_option( 'woonoow_pickup_locations', array() ); + + $id = uniqid( 'loc_' ); + $location = array( + 'id' => $id, + 'name' => sanitize_text_field( $request->get_param( 'name' ) ), + 'address' => sanitize_text_field( $request->get_param( 'address' ) ), + 'city' => sanitize_text_field( $request->get_param( 'city' ) ), + 'state' => sanitize_text_field( $request->get_param( 'state' ) ), + 'postcode' => sanitize_text_field( $request->get_param( 'postcode' ) ), + 'phone' => sanitize_text_field( $request->get_param( 'phone' ) ), + 'hours' => sanitize_text_field( $request->get_param( 'hours' ) ), + 'enabled' => true, + ); + + $locations[ $id ] = $location; + update_option( 'woonoow_pickup_locations', $locations ); + + return new WP_REST_Response( $location, 201 ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( + 'error' => 'create_failed', + 'message' => $e->getMessage(), + ), + 500 + ); + } + } + + /** + * Update pickup location + */ + public function update_location( WP_REST_Request $request ) { + try { + $id = $request->get_param( 'id' ); + $locations = get_option( 'woonoow_pickup_locations', array() ); + + if ( ! isset( $locations[ $id ] ) ) { + return new WP_REST_Response( + array( + 'error' => 'not_found', + 'message' => __( 'Location not found', 'woonoow' ), + ), + 404 + ); + } + + $location = array( + 'id' => $id, + 'name' => sanitize_text_field( $request->get_param( 'name' ) ), + 'address' => sanitize_text_field( $request->get_param( 'address' ) ), + 'city' => sanitize_text_field( $request->get_param( 'city' ) ), + 'state' => sanitize_text_field( $request->get_param( 'state' ) ), + 'postcode' => sanitize_text_field( $request->get_param( 'postcode' ) ), + 'phone' => sanitize_text_field( $request->get_param( 'phone' ) ), + 'hours' => sanitize_text_field( $request->get_param( 'hours' ) ), + 'enabled' => $locations[ $id ]['enabled'], // Preserve enabled status + ); + + $locations[ $id ] = $location; + update_option( 'woonoow_pickup_locations', $locations ); + + return new WP_REST_Response( $location, 200 ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( + 'error' => 'update_failed', + 'message' => $e->getMessage(), + ), + 500 + ); + } + } + + /** + * Delete pickup location + */ + public function delete_location( WP_REST_Request $request ) { + try { + $id = $request->get_param( 'id' ); + $locations = get_option( 'woonoow_pickup_locations', array() ); + + if ( ! isset( $locations[ $id ] ) ) { + return new WP_REST_Response( + array( + 'error' => 'not_found', + 'message' => __( 'Location not found', 'woonoow' ), + ), + 404 + ); + } + + unset( $locations[ $id ] ); + update_option( 'woonoow_pickup_locations', $locations ); + + return new WP_REST_Response( + array( + 'success' => true, + 'message' => __( 'Location deleted', 'woonoow' ), + ), + 200 + ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( + 'error' => 'delete_failed', + 'message' => $e->getMessage(), + ), + 500 + ); + } + } + + /** + * Toggle pickup location + */ + public function toggle_location( WP_REST_Request $request ) { + try { + $id = $request->get_param( 'id' ); + $enabled = rest_sanitize_boolean( $request->get_param( 'enabled' ) ); + $locations = get_option( 'woonoow_pickup_locations', array() ); + + if ( ! isset( $locations[ $id ] ) ) { + return new WP_REST_Response( + array( + 'error' => 'not_found', + 'message' => __( 'Location not found', 'woonoow' ), + ), + 404 + ); + } + + $locations[ $id ]['enabled'] = $enabled; + update_option( 'woonoow_pickup_locations', $locations ); + + return new WP_REST_Response( + array( + 'success' => true, + 'enabled' => $enabled, + 'message' => $enabled + ? __( 'Location enabled', 'woonoow' ) + : __( 'Location disabled', 'woonoow' ), + ), + 200 + ); + } catch ( \Exception $e ) { + return new WP_REST_Response( + array( + 'error' => 'toggle_failed', + 'message' => $e->getMessage(), + ), + 500 + ); + } + } +} diff --git a/includes/Api/Routes.php b/includes/Api/Routes.php index defa7c7..b3c2f55 100644 --- a/includes/Api/Routes.php +++ b/includes/Api/Routes.php @@ -11,6 +11,8 @@ use WooNooW\API\PaymentsController; use WooNooW\API\StoreController; use WooNooW\Api\ShippingController; use WooNooW\Api\TaxController; +use WooNooW\Api\PickupLocationsController; +use WooNooW\Api\EmailController; class Routes { public static function init() { @@ -59,6 +61,14 @@ class Routes { // Tax controller $tax_controller = new TaxController(); $tax_controller->register_routes(); + + // Pickup locations controller + $pickup_controller = new PickupLocationsController(); + $pickup_controller->register_routes(); + + // Email controller + $email_controller = new EmailController(); + $email_controller->register_routes(); }); } }