diff --git a/admin-spa/src/components/forms/SchemaField.tsx b/admin-spa/src/components/forms/SchemaField.tsx index 8b2cb8d..70a81a2 100644 --- a/admin-spa/src/components/forms/SchemaField.tsx +++ b/admin-spa/src/components/forms/SchemaField.tsx @@ -16,6 +16,7 @@ export interface FieldSchema { options?: Record; min?: number; max?: number; + disabled?: boolean; } interface SchemaFieldProps { @@ -72,6 +73,7 @@ export function SchemaField({ name, schema, value, onChange, error }: SchemaFiel {value ? 'Enabled' : 'Disabled'} diff --git a/admin-spa/src/components/nav/SubmenuBar.tsx b/admin-spa/src/components/nav/SubmenuBar.tsx index f09b013..2518272 100644 --- a/admin-spa/src/components/nav/SubmenuBar.tsx +++ b/admin-spa/src/components/nav/SubmenuBar.tsx @@ -30,11 +30,6 @@ export default function SubmenuBar({ items = [], fullscreen = false, headerVisib // Only ONE submenu item should be active at a time const isActive = it.path === pathname; - // Debug logging for Dashboard Overview issue - if (it.label === 'Overview' && pathname.includes('dashboard')) { - console.log('Overview check:', { label: it.label, path: it.path, pathname, isActive }); - } - const cls = [ 'ui-ctrl inline-flex items-center gap-2 rounded-md px-2.5 py-1.5 border text-sm whitespace-nowrap', 'focus:outline-none focus:ring-0 focus:shadow-none', diff --git a/admin-spa/src/routes/Products/Attributes.tsx b/admin-spa/src/routes/Products/Attributes.tsx index 66ae644..7151c72 100644 --- a/admin-spa/src/routes/Products/Attributes.tsx +++ b/admin-spa/src/routes/Products/Attributes.tsx @@ -1,11 +1,264 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Plus, Pencil, Trash2, Search } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { api } from '@/lib/api'; +import { toast } from 'sonner'; import { __ } from '@/lib/i18n'; +interface Attribute { + attribute_id: number; + attribute_name: string; + attribute_label: string; + attribute_type: string; + attribute_orderby: string; + attribute_public: number; +} + export default function ProductAttributes() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(''); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingAttribute, setEditingAttribute] = useState(null); + const [formData, setFormData] = useState({ + name: '', + label: '', + type: 'select', + orderby: 'menu_order', + public: 1 + }); + + const { data: attributes = [], isLoading } = useQuery({ + queryKey: ['product-attributes'], + queryFn: async () => { + const response = await fetch(`${api.root()}/products/attributes`, { + headers: { 'X-WP-Nonce': api.nonce() }, + }); + return response.json(); + }, + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => api.post('/products/attributes', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['product-attributes'] }); + toast.success(__('Attribute created successfully')); + handleCloseDialog(); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to create attribute')); + }, + }); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/products/attributes/${id}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['product-attributes'] }); + toast.success(__('Attribute updated successfully')); + handleCloseDialog(); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to update attribute')); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => api.del(`/products/attributes/${id}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['product-attributes'] }); + toast.success(__('Attribute deleted successfully')); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to delete attribute')); + }, + }); + + const handleOpenDialog = (attribute?: Attribute) => { + if (attribute) { + setEditingAttribute(attribute); + setFormData({ + name: attribute.attribute_name, + label: attribute.attribute_label, + type: attribute.attribute_type, + orderby: attribute.attribute_orderby, + public: attribute.attribute_public, + }); + } else { + setEditingAttribute(null); + setFormData({ name: '', label: '', type: 'select', orderby: 'menu_order', public: 1 }); + } + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + setEditingAttribute(null); + setFormData({ name: '', label: '', type: 'select', orderby: 'menu_order', public: 1 }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingAttribute) { + updateMutation.mutate({ id: editingAttribute.attribute_id, data: formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDelete = (id: number) => { + if (confirm(__('Are you sure you want to delete this attribute?'))) { + deleteMutation.mutate(id); + } + }; + + const filteredAttributes = attributes.filter((attr) => + attr.attribute_label.toLowerCase().includes(search.toLowerCase()) || + attr.attribute_name.toLowerCase().includes(search.toLowerCase()) + ); + return ( -
-

{__('Product Attributes')}

-

{__('Coming soon — SPA attributes manager.')}

+
+
+

{__('Product Attributes')}

+ +
+ +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+
+ + {isLoading ? ( +
+

{__('Loading attributes...')}

+
+ ) : filteredAttributes.length === 0 ? ( +
+

{__('No attributes found')}

+
+ ) : ( +
+ + + + + + + + + + + + {filteredAttributes.map((attribute) => ( + + + + + + + + ))} + +
{__('Name')}{__('Slug')}{__('Type')}{__('Order By')}{__('Actions')}
{attribute.attribute_label}{attribute.attribute_name}{attribute.attribute_type}{attribute.attribute_orderby} +
+ + +
+
+
+ )} + + + + + + {editingAttribute ? __('Edit Attribute') : __('Add Attribute')} + + +
+
+ + setFormData({ ...formData, label: e.target.value })} + placeholder={__('e.g., Color, Size')} + required + /> +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder={__('Leave empty to auto-generate')} + /> +
+
+ + +
+
+ + +
+ + + + +
+
+
); } diff --git a/admin-spa/src/routes/Products/Categories.tsx b/admin-spa/src/routes/Products/Categories.tsx index 9253fdb..9365cd5 100644 --- a/admin-spa/src/routes/Products/Categories.tsx +++ b/admin-spa/src/routes/Products/Categories.tsx @@ -1,11 +1,239 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Plus, Pencil, Trash2, Search } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { api } from '@/lib/api'; +import { toast } from 'sonner'; import { __ } from '@/lib/i18n'; +interface Category { + term_id: number; + name: string; + slug: string; + description: string; + count: number; + parent: number; +} + export default function ProductCategories() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(''); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + const [formData, setFormData] = useState({ name: '', slug: '', description: '', parent: 0 }); + + const { data: categories = [], isLoading } = useQuery({ + queryKey: ['product-categories'], + queryFn: async () => { + const response = await fetch(`${api.root()}/products/categories`, { + headers: { 'X-WP-Nonce': api.nonce() }, + }); + return response.json(); + }, + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => api.post('/products/categories', data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['product-categories'] }); + toast.success(__('Category created successfully')); + handleCloseDialog(); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to create category')); + }, + }); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: number; data: any }) => api.put(`/products/categories/${id}`, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['product-categories'] }); + toast.success(__('Category updated successfully')); + handleCloseDialog(); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to update category')); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => api.del(`/products/categories/${id}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['product-categories'] }); + toast.success(__('Category deleted successfully')); + }, + onError: (error: any) => { + toast.error(error?.message || __('Failed to delete category')); + }, + }); + + const handleOpenDialog = (category?: Category) => { + if (category) { + setEditingCategory(category); + setFormData({ + name: category.name, + slug: category.slug, + description: category.description || '', + parent: category.parent || 0, + }); + } else { + setEditingCategory(null); + setFormData({ name: '', slug: '', description: '', parent: 0 }); + } + setDialogOpen(true); + }; + + const handleCloseDialog = () => { + setDialogOpen(false); + setEditingCategory(null); + setFormData({ name: '', slug: '', description: '', parent: 0 }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingCategory) { + updateMutation.mutate({ id: editingCategory.term_id, data: formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDelete = (id: number) => { + if (confirm(__('Are you sure you want to delete this category?'))) { + deleteMutation.mutate(id); + } + }; + + const filteredCategories = categories.filter((cat) => + cat.name.toLowerCase().includes(search.toLowerCase()) + ); + return ( -
-

{__('Product Categories')}

-

{__('Coming soon — SPA categories manager.')}

+
+
+

{__('Product Categories')}

+ +
+ +
+
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+
+ + {isLoading ? ( +
+

{__('Loading categories...')}

+
+ ) : filteredCategories.length === 0 ? ( +
+

{__('No categories found')}

+
+ ) : ( +
+ + + + + + + + + + + + {filteredCategories.map((category) => ( + + + + + + + + ))} + +
{__('Name')}{__('Slug')}{__('Description')}{__('Count')}{__('Actions')}
{category.name}{category.slug} + {category.description || '-'} + {category.count} +
+ + +
+
+
+ )} + + + + + + {editingCategory ? __('Edit Category') : __('Add Category')} + + +
+
+ + setFormData({ ...formData, name: e.target.value })} + required + /> +
+
+ + setFormData({ ...formData, slug: e.target.value })} + placeholder={__('Leave empty to auto-generate')} + /> +
+
+ +