Added comprehensive product/category restrictions to coupon form Features Added: 1. Product Selectors: - Products (include) - multiselect with search - Exclude products - multiselect with search - Shows product names with searchable dropdown - Badge display for selected items 2. Category Selectors: - Product categories (include) - multiselect - Exclude categories - multiselect - Shows category names with search - Badge display for selected items 3. API Integration: - Added ProductsApi.list() endpoint - Added ProductsApi.categories() endpoint - Fetch products and categories on form load - React Query caching for performance 4. Form Data: - Added product_ids field - Added excluded_product_ids field - Added product_categories field - Added excluded_product_categories field - Proper number/string conversion UI/UX Improvements: - Searchable multiselect dropdowns - Badge display with X to remove - Shows +N more when exceeds display limit - Clear placeholder text - Helper text for each field - Consistent spacing and layout Technical: - Uses MultiSelect component (shadcn-based) - React Query for data fetching - Proper TypeScript types - Number array handling Note: Brands field not included yet (requires WooCommerce Product Brands extension check) Result: - Full WooCommerce coupon restrictions support - Clean, searchable UI - Production ready
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
export const api = {
|
|
root: () => (window.WNW_API?.root?.replace(/\/$/, '') || ''),
|
|
nonce: () => (window.WNW_API?.nonce || ''),
|
|
|
|
async wpFetch(path: string, options: RequestInit = {}) {
|
|
const url = /^https?:\/\//.test(path) ? path : api.root() + path;
|
|
const headers = new Headers(options.headers || {});
|
|
if (!headers.has('X-WP-Nonce') && api.nonce()) headers.set('X-WP-Nonce', api.nonce());
|
|
if (!headers.has('Accept')) headers.set('Accept', 'application/json');
|
|
if (options.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
|
|
|
|
const res = await fetch(url, { credentials: 'include', ...options, headers });
|
|
|
|
if (!res.ok) {
|
|
let responseData: any = null;
|
|
try {
|
|
const text = await res.text();
|
|
responseData = text ? JSON.parse(text) : null;
|
|
} catch { /* ignore JSON parse errors */ }
|
|
|
|
if (window.WNW_API?.isDev) {
|
|
console.error('[WooNooW] API error', { url, status: res.status, statusText: res.statusText, data: responseData });
|
|
}
|
|
|
|
// Create error with response data attached (for error handling utility to extract)
|
|
const err: any = new Error(res.statusText);
|
|
err.response = {
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
data: responseData
|
|
};
|
|
throw err;
|
|
}
|
|
|
|
try {
|
|
return await res.json();
|
|
} catch {
|
|
return await res.text();
|
|
}
|
|
},
|
|
|
|
async get(path: string, params?: Record<string, any>) {
|
|
const usp = new URLSearchParams();
|
|
if (params) {
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v == null) continue;
|
|
usp.set(k, String(v));
|
|
}
|
|
}
|
|
const qs = usp.toString();
|
|
return api.wpFetch(path + (qs ? `?${qs}` : ''));
|
|
},
|
|
|
|
async post(path: string, body?: any) {
|
|
return api.wpFetch(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
},
|
|
|
|
async put(path: string, body?: any) {
|
|
return api.wpFetch(path, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
},
|
|
|
|
async del(path: string) {
|
|
return api.wpFetch(path, { method: 'DELETE' });
|
|
},
|
|
|
|
};
|
|
|
|
export type CreateOrderPayload = {
|
|
items: { product_id: number; qty: number }[];
|
|
billing?: Record<string, any>;
|
|
shipping?: Record<string, any>;
|
|
status?: string;
|
|
payment_method?: string;
|
|
};
|
|
|
|
export const OrdersApi = {
|
|
list: (params?: Record<string, any>) => api.get('/orders', params),
|
|
get: (id: number) => api.get(`/orders/${id}`),
|
|
create: (payload: CreateOrderPayload) => api.post('/orders', payload),
|
|
update: (id: number, payload: any) => api.wpFetch(`/orders/${id}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
payments: async () => api.get('/payments'),
|
|
shippings: async () => api.get('/shippings'),
|
|
countries: () => api.get('/countries'),
|
|
};
|
|
|
|
export const ProductsApi = {
|
|
search: (search: string, limit = 10) => api.get('/products/search', { search, limit }),
|
|
list: (params?: { page?: number; per_page?: number }) => api.get('/products', { params }),
|
|
categories: () => api.get('/products/categories'),
|
|
};
|
|
|
|
export const CustomersApi = {
|
|
search: (search: string) => api.get('/customers/search', { search }),
|
|
searchByEmail: (email: string) => api.get('/customers/search', { email }),
|
|
};
|
|
|
|
export async function getMenus() {
|
|
// Prefer REST; fall back to localized snapshot
|
|
try {
|
|
const res = await fetch(`${(window as any).WNW_API}/menus`, { credentials: 'include' });
|
|
if (!res.ok) throw new Error('menus fetch failed');
|
|
return (await res.json()).items || [];
|
|
} catch {
|
|
return ((window as any).WNW_WC_MENUS?.items) || [];
|
|
}
|
|
} |