- Add WishlistController with full CRUD API - Create wishlist page in My Account - Add heart icon to all product card layouts (always visible) - Implement useWishlist hook for state management - Add wishlist toggle in admin Settings > Customer - Fix wishlist menu visibility based on admin settings - Fix double navigation in wishlist page - Fix variable product navigation to use React Router - Add TypeScript type casting fix for addresses
267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { __ } from '@/lib/i18n';
|
|
import { Crown, Info } from 'lucide-react';
|
|
import { SettingsLayout } from './components/SettingsLayout';
|
|
import { SettingsCard } from './components/SettingsCard';
|
|
import { ToggleField } from './components/ToggleField';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
|
|
|
interface CustomerSettings {
|
|
auto_register_members: boolean;
|
|
multiple_addresses_enabled: boolean;
|
|
wishlist_enabled: boolean;
|
|
vip_min_spent: number;
|
|
vip_min_orders: number;
|
|
vip_timeframe: 'all' | '30' | '90' | '365';
|
|
vip_require_both: boolean;
|
|
vip_exclude_refunded: boolean;
|
|
}
|
|
|
|
export default function CustomersSettings() {
|
|
const [settings, setSettings] = useState<CustomerSettings>({
|
|
auto_register_members: false,
|
|
multiple_addresses_enabled: true,
|
|
wishlist_enabled: true,
|
|
vip_min_spent: 1000,
|
|
vip_min_orders: 10,
|
|
vip_timeframe: 'all',
|
|
vip_require_both: true,
|
|
vip_exclude_refunded: true,
|
|
});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [message, setMessage] = useState('');
|
|
const store = getStoreCurrency();
|
|
|
|
useEffect(() => {
|
|
fetchSettings();
|
|
}, []);
|
|
|
|
const fetchSettings = async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
const response = await fetch(
|
|
`${(window as any).WNW_CONFIG?.restUrl || ''}/store/customer-settings`,
|
|
{
|
|
credentials: 'include',
|
|
headers: {
|
|
'X-WP-Nonce': (window as any).WNW_CONFIG?.nonce || (window as any).wpApiSettings?.nonce || '',
|
|
},
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('Failed to fetch');
|
|
const data = await response.json();
|
|
setSettings(data);
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
setMessage('Failed to load settings');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
setIsSaving(true);
|
|
setMessage('');
|
|
const response = await fetch(
|
|
`${(window as any).WNW_CONFIG?.restUrl || ''}/store/customer-settings`,
|
|
{
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-WP-Nonce': (window as any).WNW_CONFIG?.nonce || (window as any).wpApiSettings?.nonce || '',
|
|
},
|
|
body: JSON.stringify(settings),
|
|
}
|
|
);
|
|
|
|
if (!response.ok) throw new Error('Failed to save');
|
|
const data = await response.json();
|
|
setMessage(data.message || 'Settings saved successfully');
|
|
if (data.settings) setSettings(data.settings);
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
setMessage('Failed to save settings');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<SettingsLayout
|
|
title={__('Customer Settings')}
|
|
description={__('Configure VIP customer qualification')}
|
|
isLoading={true}
|
|
>
|
|
<div className="animate-pulse h-64 bg-muted rounded-lg"></div>
|
|
</SettingsLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SettingsLayout
|
|
title={__('Customer Settings')}
|
|
description={__('Configure VIP customer qualification criteria')}
|
|
onSave={handleSave}
|
|
saveLabel={__('Save Changes')}
|
|
>
|
|
{message && (
|
|
<div className={`p-4 rounded-lg ${message.includes('success') ? 'bg-green-50 text-green-900' : 'bg-red-50 text-red-900'}`}>
|
|
{message}
|
|
</div>
|
|
)}
|
|
|
|
<SettingsCard
|
|
title={__('General')}
|
|
description={__('General customer settings')}
|
|
>
|
|
<div className="space-y-6">
|
|
<ToggleField
|
|
id="auto_register_members"
|
|
label={__('Auto-register customers as site members')}
|
|
description={__('Automatically create WordPress user accounts for new customers when orders are created. Customers will receive login credentials via email and can track their orders.')}
|
|
checked={settings.auto_register_members}
|
|
onCheckedChange={(checked) => setSettings({ ...settings, auto_register_members: checked })}
|
|
/>
|
|
|
|
<ToggleField
|
|
id="multiple_addresses_enabled"
|
|
label={__('Enable multiple saved addresses')}
|
|
description={__('Allow customers to save multiple billing and shipping addresses in their account. Customers can select from saved addresses during checkout for faster ordering.')}
|
|
checked={settings.multiple_addresses_enabled}
|
|
onCheckedChange={(checked) => setSettings({ ...settings, multiple_addresses_enabled: checked })}
|
|
/>
|
|
|
|
<ToggleField
|
|
id="wishlist_enabled"
|
|
label={__('Enable wishlist')}
|
|
description={__('Allow customers to save products to their wishlist for later purchase. Customers can add products to wishlist from product cards and manage them in their account.')}
|
|
checked={settings.wishlist_enabled}
|
|
onCheckedChange={(checked) => setSettings({ ...settings, wishlist_enabled: checked })}
|
|
/>
|
|
</div>
|
|
</SettingsCard>
|
|
|
|
<SettingsCard
|
|
title={__('VIP Customer Qualification')}
|
|
description={__('Define criteria for identifying VIP customers')}
|
|
>
|
|
<div className="space-y-6">
|
|
<div className="flex gap-3 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-900 rounded-lg">
|
|
<Info className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
|
<div className="text-sm text-blue-900 dark:text-blue-100">
|
|
<p className="font-medium mb-1">{__('What are VIP customers?')}</p>
|
|
<p className="text-blue-700 dark:text-blue-300">
|
|
{__('VIP customers are high-value customers who meet your qualification criteria.')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vip_min_spent">{__('Minimum Total Spent')}</Label>
|
|
<Input
|
|
id="vip_min_spent"
|
|
type="number"
|
|
value={settings.vip_min_spent}
|
|
onChange={(e) => setSettings({ ...settings, vip_min_spent: parseFloat(e.target.value) || 0 })}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
{__('Minimum total amount a customer must spend')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vip_min_orders">{__('Minimum Order Count')}</Label>
|
|
<Input
|
|
id="vip_min_orders"
|
|
type="number"
|
|
value={settings.vip_min_orders}
|
|
onChange={(e) => setSettings({ ...settings, vip_min_orders: parseInt(e.target.value) || 0 })}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
{__('Minimum number of orders a customer must place')}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="vip_timeframe">{__('Timeframe')}</Label>
|
|
<Select
|
|
value={settings.vip_timeframe}
|
|
onValueChange={(value: string) => setSettings({ ...settings, vip_timeframe: value as CustomerSettings['vip_timeframe'] })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="all">{__('All time')}</SelectItem>
|
|
<SelectItem value="30">{__('Last 30 days')}</SelectItem>
|
|
<SelectItem value="90">{__('Last 90 days')}</SelectItem>
|
|
<SelectItem value="365">{__('Last year')}</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-sm text-muted-foreground">
|
|
{__('Time period to calculate totals')}
|
|
</p>
|
|
</div>
|
|
|
|
<ToggleField
|
|
id="vip_require_both"
|
|
label={__('Require both criteria')}
|
|
checked={settings.vip_require_both}
|
|
onCheckedChange={(checked) => setSettings({ ...settings, vip_require_both: checked })}
|
|
/>
|
|
<p className="text-sm text-muted-foreground -mt-4">
|
|
{settings.vip_require_both
|
|
? __('Customer must meet BOTH criteria')
|
|
: __('Customer must meet EITHER criterion')}
|
|
</p>
|
|
|
|
<ToggleField
|
|
id="vip_exclude_refunded"
|
|
label={__('Exclude refunded orders')}
|
|
checked={settings.vip_exclude_refunded}
|
|
onCheckedChange={(checked) => setSettings({ ...settings, vip_exclude_refunded: checked })}
|
|
/>
|
|
<p className="text-sm text-muted-foreground -mt-4">
|
|
{__('Do not count refunded orders')}
|
|
</p>
|
|
|
|
<div className="pt-4 border-t">
|
|
<h3 className="font-medium mb-3 flex items-center gap-2">
|
|
<Crown className="w-4 h-4 text-yellow-500" />
|
|
{__('Preview')}
|
|
</h3>
|
|
<div className="text-sm text-muted-foreground">
|
|
<p>
|
|
{settings.vip_require_both ? __('Customer needs') : __('Customer needs')}{' '}
|
|
<span className="font-semibold text-foreground">
|
|
{formatMoney(settings.vip_min_spent, {
|
|
currency: store.currency,
|
|
symbol: store.symbol,
|
|
thousandSep: store.thousand_sep,
|
|
decimalSep: store.decimal_sep,
|
|
decimals: 0,
|
|
preferSymbol: true,
|
|
})}
|
|
</span>{' '}
|
|
{settings.vip_require_both ? __('AND') : __('OR')}{' '}
|
|
<span className="font-semibold text-foreground">
|
|
{settings.vip_min_orders} {__('orders')}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</SettingsCard>
|
|
</SettingsLayout>
|
|
);
|
|
}
|