import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/Components/ui/accordion"; import { Button } from "@/Components/ui/button"; import { Checkbox } from "@/Components/ui/checkbox"; import { Input } from "@/Components/ui/input"; import { cn } from "@/lib/utils"; import { useState, useEffect, useMemo } from "react"; import { ChevronsDown, ChevronsUp } from "lucide-react"; export interface Permission { id: number; name: string; } export interface GroupedPermission { key: string; name: string; permissions: Permission[]; } interface PermissionSelectorProps { groupedPermissions: GroupedPermission[]; selectedPermissions: string[]; onChange: (permissions: string[]) => void; } export default function PermissionSelector({ groupedPermissions, selectedPermissions, onChange }: PermissionSelectorProps) { // 翻譯權限後綴 const translateAction = (permissionName: string) => { const parts = permissionName.split('.'); if (parts.length < 2) return permissionName; const action = parts[parts.length - 1]; const map: Record = { 'view': '檢視', 'create': '新增', 'edit': '編輯', 'delete': '刪除', 'adjust': '調整', 'transfer': '調撥', 'count': '盤點', 'safety_stock': '安全庫存設定', 'export': '匯出', 'complete': '完成', 'view_cost': '檢視成本', 'view_logs': '檢視日誌', 'activate': '啟用/停用', 'approve': '核准/退回', 'cancel': '取消', }; const actionText = map[action] || action; // 處理多段式權限 (例如 inventory_count.view) if (parts.length >= 2) { const middleKey = parts[parts.length - 2]; // 如果中間那段有翻譯且不等於動作本身,則顯示為 "標籤: 動作" if (map[middleKey] && middleKey !== action) { return `${map[middleKey]}: ${actionText}`; } } return actionText; }; const togglePermission = (name: string) => { if (selectedPermissions.includes(name)) { onChange(selectedPermissions.filter(p => p !== name)); } else { onChange([...selectedPermissions, name]); } }; const toggleGroup = (groupPermissions: Permission[]) => { const groupNames = groupPermissions.map(p => p.name); const allSelected = groupNames.every(name => selectedPermissions.includes(name)); if (allSelected) { // Unselect all onChange(selectedPermissions.filter(p => !groupNames.includes(p))); } else { // Select all const newPermissions = [...selectedPermissions]; groupNames.forEach(name => { if (!newPermissions.includes(name)) newPermissions.push(name); }); onChange(newPermissions); } }; // State for controlling accordion items const [openItems, setOpenItems] = useState([]); const [searchQuery, setSearchQuery] = useState(""); // Memoize filtered groups to prevent infinite loops in useEffect const filteredGroups = useMemo(() => { return groupedPermissions.map(group => { // If search is empty, return group as is if (!searchQuery.trim()) return group; // Check if group name matches const groupNameMatch = group.name.toLowerCase().includes(searchQuery.toLowerCase()); // Filter permissions that match const matchingPermissions = group.permissions.filter(p => { const translatedName = translateAction(p.name); return translatedName.includes(searchQuery) || p.name.toLowerCase().includes(searchQuery.toLowerCase()); }); // If group name matches, show all permissions. Otherwise show only matching permissions. if (groupNameMatch) { return group; } return { ...group, permissions: matchingPermissions }; }).filter(group => group.permissions.length > 0); }, [groupedPermissions, searchQuery]); const currentDisplayKeys = useMemo(() => filteredGroups.map(g => g.key), [filteredGroups]); const onExpandAll = () => setOpenItems(currentDisplayKeys); const onCollapseAll = () => setOpenItems([]); // Auto-expand groups when searching useEffect(() => { if (searchQuery.trim()) { const filteredKeys = filteredGroups.map(g => g.key); setOpenItems(prev => { const next = new Set([...prev, ...filteredKeys]); return Array.from(next); }); } // removing the 'else' block which forced collapse on empty search // We let the user manually collapse if they want, or we could reset only when search is cleared explicitly? // User behavior: if I finish searching, I might want to see my previous state, but "Expand All" failing was the main issue. // The issue was the effect running on every render resetting state. }, [searchQuery]); // Only run when query changes. We actually depend on filteredGroups result but only when query changes matters most for "auto" trigger. // Actually, correctly: if filteredGroups changes due to search change, we expand. // Better interaction: When search query *changes* and is not empty, we expand matches. return (
setSearchQuery(e.target.value)} className="pl-8" />
已選擇 {selectedPermissions.length} 項
{filteredGroups.length === 0 ? (
沒有符合「{searchQuery}」的權限項目
) : ( filteredGroups.map((group) => { const selectedCount = group.permissions.filter(p => selectedPermissions.includes(p.name)).length; const totalCount = group.permissions.length; const isAllSelected = selectedCount === totalCount; // 將權限分為「基本操作」與「狀態/進階操作」 const statusActions = ['approve', 'cancel', 'complete', 'activate']; const normalPermissions = group.permissions.filter(p => !statusActions.includes(p.name.split('.').pop() || '')); const specialPermissions = group.permissions.filter(p => statusActions.includes(p.name.split('.').pop() || '')); return (
toggleGroup(group.permissions)} className="data-[state=checked]:bg-primary-main" />
0 ? "text-primary-main" : "text-gray-700" )}> {group.name} {selectedCount} / {totalCount}
{/* 基本操作 */} {normalPermissions.length > 0 && (
基本功能權限
{normalPermissions.map((permission) => ( ))}
)} {/* 狀態操作/進階權限 */} {specialPermissions.length > 0 && (
單據狀態與進階操作權限
{specialPermissions.map((permission) => ( ))}
)}
); }) )}
); } function PermissionItem({ permission, selectedPermissions, onToggle, translate }: any) { return (
onToggle(permission.name)} />

{permission.name}

); }