diff --git a/src/features/admin/components/pages/RoleEditPage.js b/src/features/admin/components/pages/RoleEditPage.js index 86229a3..b9dee39 100644 --- a/src/features/admin/components/pages/RoleEditPage.js +++ b/src/features/admin/components/pages/RoleEditPage.js @@ -58,16 +58,6 @@ const RoleEditPage = ({ roleId }) => { ); }; - const toggleGroup = (group) => { - const groupKeys = PERMISSION_GROUPS[group].map(p => p.key); - const allSelected = groupKeys.every(k => selectedPerms.includes(k)); - if (allSelected) { - setSelectedPerms(prev => prev.filter(k => !groupKeys.includes(k))); - } else { - setSelectedPerms(prev => [...new Set([...prev, ...groupKeys])]); - } - }; - const handleSubmit = async (e) => { e.preventDefault(); if (!name.trim()) { @@ -178,53 +168,24 @@ const RoleEditPage = ({ roleId }) => {

Permissions

- {Object.entries(PERMISSION_GROUPS).map(([group, perms]) => { - const groupKeys = perms.map(p => p.key); - const allSelected = groupKeys.every(k => selectedPerms.includes(k)); - const someSelected = groupKeys.some(k => selectedPerms.includes(k)); - - return ( -
- - -
- {perms.map((perm) => ( - togglePerm(perm.key)} - label={perm.name} - description={perm.key} - /> - ))} -
+ {Object.entries(PERMISSION_GROUPS).map(([group, perms]) => ( +
+

+ {group} +

+
+ {perms.map((perm) => ( + togglePerm(perm.key)} + label={perm.name} + description={perm.key} + /> + ))}
- ); - })} +
+ ))}
diff --git a/src/features/admin/components/pages/UserEditPage.js b/src/features/admin/components/pages/UserEditPage.js index 2054bb7..1f7867d 100644 --- a/src/features/admin/components/pages/UserEditPage.js +++ b/src/features/admin/components/pages/UserEditPage.js @@ -2,7 +2,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; -import { Button, Card, Input, Select, Loading, Switch } from '@zen/core/shared/components'; +import { Button, Card, Input, Select, Loading, TagInput } from '@zen/core/shared/components'; import { useToast } from '@zen/core/toast'; const UserEditPage = ({ userId }) => { @@ -80,12 +80,6 @@ const UserEditPage = ({ userId }) => { } }; - const toggleRole = (roleId) => { - setSelectedRoleIds(prev => - prev.includes(roleId) ? prev.filter(id => id !== roleId) : [...prev, roleId] - ); - }; - const validateForm = () => { const newErrors = {}; if (!formData.name || !formData.name.trim()) { @@ -146,6 +140,13 @@ const UserEditPage = ({ userId }) => { } }; + const roleOptions = allRoles.map(r => ({ + value: r.id, + label: r.name, + color: r.color || '#6b7280', + description: r.description || undefined, + })); + if (loading) { return (
@@ -219,33 +220,16 @@ const UserEditPage = ({ userId }) => { -
+

Rôles

-

Activez les rôles à attribuer à cet utilisateur.

- {allRoles.length === 0 ? ( -

Aucun rôle disponible

- ) : ( -
- {allRoles.map((role) => ( - toggleRole(role.id)} - label={ - - - {role.name} - - } - description={role.description || undefined} - /> - ))} -
- )} +
diff --git a/src/shared/components/TagInput.js b/src/shared/components/TagInput.js new file mode 100644 index 0000000..d567b82 --- /dev/null +++ b/src/shared/components/TagInput.js @@ -0,0 +1,168 @@ +'use client'; + +import { useState, useRef, useEffect } from 'react'; + +const hexToRgb = (hex) => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result + ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } + : null; +}; + +const Pill = ({ option, onRemove }) => { + const rgb = option.color ? hexToRgb(option.color) : null; + const pillStyle = rgb + ? { backgroundColor: `rgba(${rgb.r},${rgb.g},${rgb.b},0.15)`, borderColor: `rgba(${rgb.r},${rgb.g},${rgb.b},0.4)`, color: option.color } + : {}; + const fallbackClass = !rgb + ? 'bg-neutral-100 dark:bg-neutral-700 border-neutral-200 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300' + : ''; + + return ( + + {option.color && ( + + )} + {option.label} + + + ); +}; + +const TagInput = ({ + options = [], + value = [], + onChange, + label, + description, + placeholder = 'Ajouter...', + error, +}) => { + const [inputValue, setInputValue] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const containerRef = useRef(null); + const inputRef = useRef(null); + + const selectedOptions = value.map(v => options.find(o => o.value === v)).filter(Boolean); + const filtered = options.filter(o => + !value.includes(o.value) && + o.label.toLowerCase().includes(inputValue.toLowerCase()) + ); + + useEffect(() => { + const handleClickOutside = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const selectOption = (option) => { + onChange([...value, option.value]); + setInputValue(''); + inputRef.current?.focus(); + }; + + const removeOption = (val) => { + onChange(value.filter(v => v !== val)); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Backspace' && inputValue === '' && value.length > 0) { + onChange(value.slice(0, -1)); + } + if (e.key === 'Escape') { + setIsOpen(false); + } + }; + + return ( +
+ {label && ( + + )} + +
{ inputRef.current?.focus(); setIsOpen(true); }} + > + {selectedOptions.map(opt => ( + removeOption(opt.value)} /> + ))} + + { setInputValue(e.target.value); setIsOpen(true); }} + onFocus={() => setIsOpen(true)} + onKeyDown={handleKeyDown} + placeholder={value.length === 0 ? placeholder : ''} + className="flex-1 min-w-[80px] bg-transparent outline-none text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 text-sm" + /> + + {isOpen && filtered.length > 0 && ( +
+ {filtered.map(option => ( + + ))} +
+ )} + + {isOpen && filtered.length === 0 && inputValue && ( +
+

Aucun résultat

+
+ )} +
+ + {description && !error && ( +

{description}

+ )} + {error && ( +

{error}

+ )} +
+ ); +}; + +export default TagInput; diff --git a/src/shared/components/index.js b/src/shared/components/index.js index b95cf2e..6cf585c 100644 --- a/src/shared/components/index.js +++ b/src/shared/components/index.js @@ -26,3 +26,4 @@ export { default as PasswordStrengthIndicator } from './PasswordStrengthIndicato export { default as FilterTabs } from './FilterTabs'; export { default as Breadcrumb } from './Breadcrumb'; export { default as Switch } from './Switch'; +export { default as TagInput } from './TagInput';