Merge pull request 'dev' (#11) from dev into main

Reviewed-on: #11
This commit is contained in:
Seazn 2026-02-12 16:36:38 +00:00
commit a36fc051ca
3 changed files with 155 additions and 37 deletions

View File

@ -33,10 +33,12 @@ Last updated: 2026-01-20
• [ ] User Status 1 Feld das wir nicht benutzen
• [ ] Pool mulit user actions (select 5 -> add to pool)
• [x] reset edit templates
• [] "Suspended" status should actually do something
• [] Matrix shit
• [x] "Suspended" status should actually do something
• [] Matrix shit (tiefe dynamisch einstellen)
• [x] Git
• [] Switching status -> confirmation modal
• [] mobile scroll bug with double page on top
• [] search modal unify -> only return userId(s)

View File

@ -53,6 +53,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
const [selectedStatus, setSelectedStatus] = useState<UserStatus>('pending')
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)
const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract')
const [previewState, setPreviewState] = useState({
@ -86,6 +92,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
useEffect(() => {
if (isOpen && userId && token) {
fetchUserDetails()
fetchAllPermissions()
loadContractFiles()
}
}, [isOpen, userId, token])
@ -104,6 +111,7 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
})
setContractFiles({ contract: [], gdpr: [] })
setSelectedFile({})
setPermissionsError(null)
}, [isOpen, userId])
useEffect(() => {
@ -112,6 +120,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
}
}, [userDetails])
useEffect(() => {
if (userDetails?.permissions) {
setSelectedPermissions(userDetails.permissions.map(p => p.name))
}
}, [userDetails])
const fetchUserDetails = async () => {
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) => {
if (!userId || !token || newStatus === selectedStatus) return
@ -794,42 +860,69 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
)}
{/* Permissions */}
{userDetails.permissions.length > 0 && (
<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">
<div className="bg-gray-50 px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<ShieldCheckIcon className="h-5 w-5 text-gray-600" />
Permissions ({userDetails.permissions.length})
Permissions ({selectedPermissions.length})
</h3>
<button
type="button"
onClick={handleSavePermissions}
disabled={permissionsSaving || permissionsLoading}
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"
>
{permissionsSaving ? 'Saving…' : 'Save Permissions'}
</button>
</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">
{userDetails.permissions.map((perm) => (
<div
{allPermissions.map((perm) => {
const checked = selectedPermissions.includes(perm.name)
const disabled = !perm.is_active
return (
<label
key={perm.id}
className={`flex items-center gap-3 p-3 rounded-lg border ${
perm.is_active
? 'bg-green-50 border-green-200'
: 'bg-gray-50 border-gray-200'
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'
}`}
>
{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" />
)}
<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>
</div>
))}
</div>
</div>
</label>
)
})}
</div>
)}
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-3 pt-4">

View File

@ -33,6 +33,9 @@ export const API_ENDPOINTS = {
// Documents
USER_DOCUMENTS: '/api/users/:id/documents',
// Permissions
PERMISSIONS_LIST: '/api/permissions',
// Admin
ADMIN_USERS: '/api/admin/users/:id/full',
ADMIN_USER_DETAILED: '/api/admin/users/:id/detailed',
@ -46,6 +49,7 @@ export const API_ENDPOINTS = {
ADMIN_UPDATE_USER_VERIFICATION: '/api/admin/update-verification/:id',
ADMIN_UPDATE_USER_PROFILE: '/api/admin/update-user-profile/:id',
ADMIN_UPDATE_USER_STATUS: '/api/admin/update-user-status/:id',
ADMIN_UPDATE_USER_PERMISSIONS: '/api/admin/users/:id/permissions',
// Pools (admin)
ADMIN_POOL_MEMBERS: '/api/admin/pools/:id/members',
// Coffee products (admin)
@ -395,6 +399,25 @@ export class AdminAPI {
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)
static async listCoffee(token: string) {
const response = await ApiClient.get(API_ENDPOINTS.ADMIN_COFFEE_LIST, token)