Files
TREK/client/src/components/Admin/PermissionsPanel.tsx
2026-03-31 23:36:15 +02:00

170 lines
6.2 KiB
TypeScript

import React, { useEffect, useState, useMemo } from 'react'
import { adminApi } from '../../api/client'
import { useTranslation } from '../../i18n'
import { usePermissionsStore, PermissionLevel } from '../../store/permissionsStore'
import { useToast } from '../shared/Toast'
import { Save, Loader2, RotateCcw } from 'lucide-react'
import CustomSelect from '../shared/CustomSelect'
interface PermissionEntry {
key: string
level: PermissionLevel
defaultLevel: PermissionLevel
allowedLevels: PermissionLevel[]
}
const LEVEL_LABELS: Record<string, string> = {
admin: 'perm.level.admin',
trip_owner: 'perm.level.tripOwner',
trip_member: 'perm.level.tripMember',
everybody: 'perm.level.everybody',
}
const CATEGORIES = [
{ id: 'trip', keys: ['trip_create', 'trip_edit', 'trip_delete', 'trip_archive', 'trip_cover_upload'] },
{ id: 'members', keys: ['member_manage'] },
{ id: 'files', keys: ['file_upload', 'file_edit', 'file_delete'] },
{ id: 'content', keys: ['place_edit', 'day_edit', 'reservation_edit'] },
{ id: 'extras', keys: ['budget_edit', 'packing_edit', 'collab_edit', 'share_manage'] },
]
export default function PermissionsPanel(): React.ReactElement {
const { t } = useTranslation()
const toast = useToast()
const [entries, setEntries] = useState<PermissionEntry[]>([])
const [values, setValues] = useState<Record<string, PermissionLevel>>({})
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [dirty, setDirty] = useState(false)
useEffect(() => {
loadPermissions()
}, [])
const loadPermissions = async () => {
setLoading(true)
try {
const data = await adminApi.getPermissions()
setEntries(data.permissions)
const vals: Record<string, PermissionLevel> = {}
for (const p of data.permissions) vals[p.key] = p.level
setValues(vals)
setDirty(false)
} catch {
toast.error(t('common.error'))
} finally {
setLoading(false)
}
}
const handleChange = (key: string, level: PermissionLevel) => {
setValues(prev => ({ ...prev, [key]: level }))
setDirty(true)
}
const handleSave = async () => {
setSaving(true)
try {
const data = await adminApi.updatePermissions(values)
if (data.permissions) {
usePermissionsStore.getState().setPermissions(data.permissions)
}
setDirty(false)
toast.success(t('perm.saved'))
} catch {
toast.error(t('common.error'))
} finally {
setSaving(false)
}
}
const handleReset = () => {
const defaults: Record<string, PermissionLevel> = {}
for (const p of entries) defaults[p.key] = p.defaultLevel
setValues(defaults)
setDirty(true)
}
const entryMap = useMemo(() => new Map(entries.map(e => [e.key, e])), [entries])
if (loading) {
return (
<div className="p-8 text-center">
<div className="w-8 h-8 border-2 border-slate-200 border-t-slate-900 rounded-full animate-spin mx-auto" />
</div>
)
}
return (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="px-6 py-4 border-b border-slate-100 flex items-center justify-between">
<div>
<h2 className="font-semibold text-slate-900">{t('perm.title')}</h2>
<p className="text-xs text-slate-400 mt-0.5">{t('perm.subtitle')}</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleReset}
disabled={saving}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-40 transition-colors"
>
<RotateCcw className="w-3.5 h-3.5" />
{t('perm.resetDefaults')}
</button>
<button
onClick={handleSave}
disabled={saving || !dirty}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-slate-900 text-white rounded-lg hover:bg-slate-700 disabled:bg-slate-400 transition-colors"
>
{saving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
{t('common.save')}
</button>
</div>
</div>
<div className="divide-y divide-slate-100">
{CATEGORIES.map(cat => (
<div key={cat.id} className="px-6 py-4">
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3">
{t(`perm.cat.${cat.id}`)}
</h3>
<div className="space-y-3">
{cat.keys.map(key => {
const entry = entryMap.get(key)
if (!entry) return null
const currentLevel = values[key] || entry.defaultLevel
const isDefault = currentLevel === entry.defaultLevel
return (
<div key={key} className="flex items-center justify-between gap-4">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-700">{t(`perm.action.${key}`)}</p>
<p className="text-xs text-slate-400 mt-0.5">{t(`perm.actionHint.${key}`)}</p>
</div>
<div className="flex items-center gap-2">
{!isDefault && (
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-amber-100 text-amber-700">
{t('perm.customized')}
</span>
)}
<CustomSelect
value={currentLevel}
onChange={(val) => handleChange(key, val as PermissionLevel)}
options={entry.allowedLevels.map(l => ({
value: l,
label: t(LEVEL_LABELS[l] || l),
}))}
/>
</div>
</div>
)
})}
</div>
</div>
))}
</div>
</div>
</div>
)
}