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 • [ ] User Status 1 Feld das wir nicht benutzen
• [ ] Pool mulit user actions (select 5 -> add to pool) • [ ] Pool mulit user actions (select 5 -> add to pool)
• [x] reset edit templates • [x] reset edit templates
• [] "Suspended" status should actually do something • [x] "Suspended" status should actually do something
• [] Matrix shit • [] Matrix shit (tiefe dynamisch einstellen)
• [x] Git • [x] Git
• [] Switching status -> confirmation modal
• [] mobile scroll bug with double page on top • [] 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 [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">

View File

@ -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)