Files
WooNooW/admin-spa/src/routes/Settings/Customers.tsx
Dwindi Ramadhana 0b08ddefa1 feat: implement wishlist feature with admin toggle
- 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
2025-12-26 01:44:15 +07:00

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>
);
}