Files
WooNooW/admin-spa/src/routes/Settings/Notifications/ChannelConfig.tsx
dwindown 26eb7cb898 feat: Implement push notification settings backend and UI
##  Push Notification Settings - Fully Functional

### Backend (PHP)

**PushNotificationHandler Updates:**
- Added `SETTINGS_KEY` constant
- `ensure_default_settings()` - Initialize defaults
- `get_default_settings()` - Return default config
- `get_settings()` - Fetch current settings
- `update_settings()` - Save settings

**Default Settings:**
```php
[
  'use_logo' => true,
  'use_product_images' => true,
  'use_gravatar' => false,
  'click_action' => '/wp-admin/admin.php?page=woonoow#/orders',
  'require_interaction' => false,
  'silent' => false,
]
```

**NotificationsController:**
- `GET /notifications/push/settings` - Fetch settings
- `POST /notifications/push/settings` - Update settings
- Permission-protected endpoints

### Frontend (React)

**ChannelConfig Component:**
- Fetches push settings on open
- Real-time state management
- Connected switches and inputs
- Save mutation with loading state
- Toast notifications for success/error
- Disabled state during save

**Settings Available:**
1. **Branding**
   - Use Store Logo
   - Use Product Images
   - Use Customer Gravatar

2. **Behavior**
   - Click Action URL (input)
   - Require Interaction
   - Silent Notifications

### Features

 **Backend Storage** - Settings saved in wp_options
 **REST API** - GET and POST endpoints
 **Frontend UI** - Full CRUD interface
 **State Management** - React Query integration
 **Loading States** - Skeleton and button states
 **Error Handling** - Toast notifications
 **Default Values** - Sensible defaults

---

**Next: Email channel toggle** 📧
2025-11-11 15:15:02 +07:00

282 lines
10 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { ExternalLink, Loader2 } from 'lucide-react';
import { toast } from 'sonner';
import { __ } from '@/lib/i18n';
interface ChannelConfigProps {
open: boolean;
onClose: () => void;
channelId: string;
channelLabel: string;
}
export default function ChannelConfig({ open, onClose, channelId, channelLabel }: ChannelConfigProps) {
// Email configuration - redirect to WooCommerce
if (channelId === 'email') {
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{__('Email Configuration')}</DialogTitle>
<DialogDescription>
{__('Email settings are managed by WooCommerce')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">
{__(
'Email notifications are powered by WooCommerce. You can configure SMTP settings, email templates, and sender information in the WooCommerce settings.'
)}
</p>
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
<h4 className="font-medium text-sm">{__('Available Settings')}</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> {__('SMTP Configuration')}</li>
<li> {__('Email Templates')}</li>
<li> {__('Sender Name & Email')}</li>
<li> {__('Email Headers & Footers')}</li>
</ul>
</div>
<Button
className="w-full"
onClick={() => {
window.open(
`${(window as any).WNW_CONFIG?.wpAdminUrl || '/wp-admin'}/admin.php?page=wc-settings&tab=email`,
'_blank'
);
onClose();
}}
>
<ExternalLink className="h-4 w-4 mr-2" />
{__('Open WooCommerce Email Settings')}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
// Push notification configuration
if (channelId === 'push') {
const queryClient = useQueryClient();
const [settings, setSettings] = useState({
use_logo: true,
use_product_images: true,
use_gravatar: false,
click_action: '/wp-admin/admin.php?page=woonoow#/orders',
require_interaction: false,
silent: false,
});
// Fetch push settings
const { data: pushSettings, isLoading } = useQuery({
queryKey: ['push-settings'],
queryFn: () => api.get('/notifications/push/settings'),
enabled: open,
});
// Update local state when data is fetched
useEffect(() => {
if (pushSettings) {
setSettings(pushSettings);
}
}, [pushSettings]);
// Save settings mutation
const saveMutation = useMutation({
mutationFn: async () => {
return api.post('/notifications/push/settings', settings);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['push-settings'] });
toast.success(__('Push notification settings saved'));
onClose();
},
onError: (error: any) => {
toast.error(error?.message || __('Failed to save settings'));
},
});
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{__('Push Notification Configuration')}</DialogTitle>
<DialogDescription>
{__('Configure how push notifications appear and behave')}
</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-6 py-4">
{/* Branding */}
<div className="space-y-3">
<Label>{__('Notification Branding')}</Label>
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="use-logo">{__('Use Store Logo')}</Label>
<p className="text-xs text-muted-foreground">
{__('Display your store logo in push notifications')}
</p>
</div>
<Switch
id="use-logo"
checked={settings.use_logo}
onCheckedChange={(checked) => setSettings({...settings, use_logo: checked})}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="use-product-image">{__('Use Product Images')}</Label>
<p className="text-xs text-muted-foreground">
{__('Show product images in order notifications')}
</p>
</div>
<Switch
id="use-product-image"
checked={settings.use_product_images}
onCheckedChange={(checked) => setSettings({...settings, use_product_images: checked})}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="use-gravatar">{__('Use Customer Gravatar')}</Label>
<p className="text-xs text-muted-foreground">
{__('Display customer avatar when available')}
</p>
</div>
<Switch
id="use-gravatar"
checked={settings.use_gravatar}
onCheckedChange={(checked) => setSettings({...settings, use_gravatar: checked})}
/>
</div>
</div>
</div>
{/* Behavior */}
<div className="space-y-3">
<Label>{__('Notification Behavior')}</Label>
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="click-action">{__('Click Action URL')}</Label>
<Input
id="click-action"
placeholder={__('https://yourstore.com/orders')}
value={settings.click_action}
onChange={(e) => setSettings({...settings, click_action: e.target.value})}
/>
<p className="text-xs text-muted-foreground">
{__('Where users are redirected when clicking the notification')}
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="require-interaction">{__('Require Interaction')}</Label>
<p className="text-xs text-muted-foreground">
{__('Notification stays until user dismisses it')}
</p>
</div>
<Switch
id="require-interaction"
checked={settings.require_interaction}
onCheckedChange={(checked) => setSettings({...settings, require_interaction: checked})}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="silent">{__('Silent Notifications')}</Label>
<p className="text-xs text-muted-foreground">
{__('Disable notification sound')}
</p>
</div>
<Switch
id="silent"
checked={settings.silent}
onCheckedChange={(checked) => setSettings({...settings, silent: checked})}
/>
</div>
</div>
</div>
<div className="bg-muted/50 rounded-lg p-4">
<p className="text-xs text-muted-foreground">
💡 {__('Note: These settings will be saved and applied to all push notifications. Individual templates can override the icon and image.')}
</p>
</div>
</div>
)
}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={onClose} disabled={saveMutation.isPending}>
{__('Cancel')}
</Button>
<Button onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending || isLoading}>
{saveMutation.isPending ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{__('Saving...')}
</>
) : (
__('Save Configuration')
)}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
// Generic addon channel configuration
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{channelLabel} {__('Configuration')}</DialogTitle>
<DialogDescription>
{__('Configure')} {channelLabel} {__('settings')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">
{__('Configuration for this channel is provided by the addon.')}
</p>
</div>
<div className="flex justify-end">
<Button variant="outline" onClick={onClose}>
{__('Close')}
</Button>
</div>
</DialogContent>
</Dialog>
);
}