Add permissions management functionality in UserDetailModal and API
This commit is contained in:
parent
3face61dc5
commit
6864375021
@ -53,6 +53,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
const [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
|
const [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
|
||||||
const token = useAuthStore(state => state.accessToken)
|
const token = useAuthStore(state => state.accessToken)
|
||||||
|
|
||||||
|
const [allPermissions, setAllPermissions] = useState<Array<{ id: number; name: string; description?: string; is_active: boolean }>>([])
|
||||||
|
const [permissionsLoading, setPermissionsLoading] = useState(false)
|
||||||
|
const [permissionsError, setPermissionsError] = useState<string | null>(null)
|
||||||
|
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([])
|
||||||
|
const [permissionsSaving, setPermissionsSaving] = useState(false)
|
||||||
|
|
||||||
// Contract preview state (lazy-loaded, per contract type)
|
// Contract preview state (lazy-loaded, per contract type)
|
||||||
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
|
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
|
||||||
const [previewState, setPreviewState] = useState({
|
const [previewState, setPreviewState] = useState({
|
||||||
@ -86,6 +92,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && userId && token) {
|
if (isOpen && userId && token) {
|
||||||
fetchUserDetails()
|
fetchUserDetails()
|
||||||
|
fetchAllPermissions()
|
||||||
loadContractFiles()
|
loadContractFiles()
|
||||||
}
|
}
|
||||||
}, [isOpen, userId, token])
|
}, [isOpen, userId, token])
|
||||||
@ -104,6 +111,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
})
|
})
|
||||||
setContractFiles({ contract: [], gdpr: [] })
|
setContractFiles({ contract: [], gdpr: [] })
|
||||||
setSelectedFile({})
|
setSelectedFile({})
|
||||||
|
setPermissionsError(null)
|
||||||
}, [isOpen, userId])
|
}, [isOpen, userId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -112,6 +120,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
}
|
}
|
||||||
}, [userDetails])
|
}, [userDetails])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userDetails?.permissions) {
|
||||||
|
setSelectedPermissions(userDetails.permissions.map(p => p.name))
|
||||||
|
}
|
||||||
|
}, [userDetails])
|
||||||
|
|
||||||
const fetchUserDetails = async () => {
|
const fetchUserDetails = async () => {
|
||||||
if (!userId || !token) return
|
if (!userId || !token) return
|
||||||
|
|
||||||
@ -134,6 +148,58 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchAllPermissions = async () => {
|
||||||
|
if (!token) return
|
||||||
|
setPermissionsLoading(true)
|
||||||
|
setPermissionsError(null)
|
||||||
|
try {
|
||||||
|
const response = await AdminAPI.getPermissions(token)
|
||||||
|
if (response.success) {
|
||||||
|
setAllPermissions(response.permissions || [])
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to fetch permissions')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch permissions'
|
||||||
|
setPermissionsError(errorMessage)
|
||||||
|
console.error('UserDetailModal.fetchAllPermissions error:', err)
|
||||||
|
} finally {
|
||||||
|
setPermissionsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePermission = (permName: string) => {
|
||||||
|
setSelectedPermissions(prev => {
|
||||||
|
if (prev.includes(permName)) {
|
||||||
|
return prev.filter(p => p !== permName)
|
||||||
|
}
|
||||||
|
return [...prev, permName]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSavePermissions = async () => {
|
||||||
|
if (!userId || !token) return
|
||||||
|
setPermissionsSaving(true)
|
||||||
|
setPermissionsError(null)
|
||||||
|
try {
|
||||||
|
const response = await AdminAPI.updateUserPermissions(token, userId, selectedPermissions)
|
||||||
|
if (response.success) {
|
||||||
|
await fetchUserDetails()
|
||||||
|
if (onUserUpdated) {
|
||||||
|
onUserUpdated()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to update permissions')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err instanceof Error ? err.message : 'Failed to update permissions'
|
||||||
|
setPermissionsError(errorMessage)
|
||||||
|
console.error('UserDetailModal.handleSavePermissions error:', err)
|
||||||
|
} finally {
|
||||||
|
setPermissionsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleStatusChange = async (newStatus: UserStatus) => {
|
const handleStatusChange = async (newStatus: UserStatus) => {
|
||||||
if (!userId || !token || newStatus === selectedStatus) return
|
if (!userId || !token || newStatus === selectedStatus) return
|
||||||
|
|
||||||
@ -794,42 +860,69 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Permissions */}
|
{/* Permissions */}
|
||||||
{userDetails.permissions.length > 0 && (
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||||
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200">
|
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
|
<ShieldCheckIcon className="h-5 w-5 text-gray-600" />
|
||||||
<ShieldCheckIcon className="h-5 w-5 text-gray-600" />
|
Permissions ({selectedPermissions.length})
|
||||||
Permissions ({userDetails.permissions.length})
|
</h3>
|
||||||
</h3>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
<div className="px-6 py-5">
|
onClick={handleSavePermissions}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
disabled={permissionsSaving || permissionsLoading}
|
||||||
{userDetails.permissions.map((perm) => (
|
className="inline-flex items-center justify-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm disabled:opacity-60"
|
||||||
<div
|
>
|
||||||
key={perm.id}
|
{permissionsSaving ? 'Saving…' : 'Save Permissions'}
|
||||||
className={`flex items-center gap-3 p-3 rounded-lg border ${
|
</button>
|
||||||
perm.is_active
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-gray-50 border-gray-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{perm.is_active ? (
|
|
||||||
<CheckCircleIcon className="h-5 w-5 text-green-600 flex-shrink-0" />
|
|
||||||
) : (
|
|
||||||
<XCircleIcon className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<div className="text-sm font-medium text-gray-900">{perm.name}</div>
|
|
||||||
{perm.description && (
|
|
||||||
<div className="text-xs text-gray-500 mt-0.5">{perm.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="px-6 py-5">
|
||||||
|
{permissionsError && (
|
||||||
|
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 mb-4">
|
||||||
|
{permissionsError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{permissionsLoading ? (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<div className="h-4 w-4 border-2 border-gray-400 border-b-transparent rounded-full animate-spin" />
|
||||||
|
Loading permissions…
|
||||||
|
</div>
|
||||||
|
) : allPermissions.length === 0 ? (
|
||||||
|
<div className="text-sm text-gray-500">No permissions available.</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{allPermissions.map((perm) => {
|
||||||
|
const checked = selectedPermissions.includes(perm.name)
|
||||||
|
const disabled = !perm.is_active
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={perm.id}
|
||||||
|
className={`flex items-start gap-3 p-3 rounded-lg border cursor-pointer ${
|
||||||
|
disabled ? 'bg-gray-50 border-gray-200 opacity-70 cursor-not-allowed' : checked ? 'bg-green-50 border-green-200' : 'bg-white border-gray-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled || permissionsSaving}
|
||||||
|
onChange={() => togglePermission(perm.name)}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-900">{perm.name}</div>
|
||||||
|
{perm.description && (
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">{perm.description}</div>
|
||||||
|
)}
|
||||||
|
{!perm.is_active && (
|
||||||
|
<div className="text-xs text-gray-400 mt-0.5">Inactive</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
<div className="flex justify-end gap-3 pt-4">
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
|||||||
@ -32,6 +32,9 @@ export const API_ENDPOINTS = {
|
|||||||
|
|
||||||
// Documents
|
// Documents
|
||||||
USER_DOCUMENTS: '/api/users/:id/documents',
|
USER_DOCUMENTS: '/api/users/:id/documents',
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
PERMISSIONS_LIST: '/api/permissions',
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
ADMIN_USERS: '/api/admin/users/:id/full',
|
ADMIN_USERS: '/api/admin/users/:id/full',
|
||||||
@ -46,6 +49,7 @@ export const API_ENDPOINTS = {
|
|||||||
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
|
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
|
||||||
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
|
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
|
||||||
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
|
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
|
||||||
|
ADMIN_UPDATE_USER_PERMISSIONS: '/api/admin/users/:id/permissions',
|
||||||
// Pools (admin)
|
// Pools (admin)
|
||||||
ADMIN_POOL_MEMBERS: '/api/admin/pools/:id/members',
|
ADMIN_POOL_MEMBERS: '/api/admin/pools/:id/members',
|
||||||
// Coffee products (admin)
|
// Coffee products (admin)
|
||||||
@ -395,6 +399,25 @@ export class AdminAPI {
|
|||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async getPermissions(token: string) {
|
||||||
|
const response = await ApiClient.get(API_ENDPOINTS.PERMISSIONS_LIST, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ message: 'Failed to fetch permissions' }))
|
||||||
|
throw new Error(error.message || 'Failed to fetch permissions')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
static async updateUserPermissions(token: string, userId: string, permissions: string[]) {
|
||||||
|
const endpoint = API_ENDPOINTS.ADMIN_UPDATE_USER_PERMISSIONS.replace(':id', userId)
|
||||||
|
const response = await ApiClient.put(endpoint, { permissions }, token)
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ message: 'Failed to update user permissions' }))
|
||||||
|
throw new Error(error.message || 'Failed to update user permissions')
|
||||||
|
}
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
// Coffee products (admin)
|
// Coffee products (admin)
|
||||||
static async listCoffee(token: string) {
|
static async listCoffee(token: string) {
|
||||||
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_COFFEE_LIST, token)
|
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_COFFEE_LIST, token)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user