386 lines
16 KiB
TypeScript
386 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import clsx from 'clsx';
|
|
import {
|
|
X,
|
|
Shield,
|
|
User,
|
|
Users,
|
|
Plus,
|
|
Trash2,
|
|
Lock,
|
|
Eye,
|
|
Pencil,
|
|
ChevronDown,
|
|
Calendar,
|
|
Crown,
|
|
} from 'lucide-react';
|
|
import {
|
|
useEntityPermissions,
|
|
useCreateEntityPermission,
|
|
useUpdateEntityPermission,
|
|
useDeleteEntityPermission,
|
|
useEntityAccess,
|
|
} from '@/api/entityPermissionHooks';
|
|
import { useUsers } from '@/api/users';
|
|
import { useGroups } from '@/api/groups';
|
|
import type { EntityPermission, PermissionLevel } from '@/api/entityPermissions';
|
|
|
|
interface ShareDialogProps {
|
|
entityType: string;
|
|
entityId: string;
|
|
entityName: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
const PERM_LEVELS: { value: PermissionLevel; label: string; icon: React.ComponentType<{ className?: string; strokeWidth?: number }> | any; desc: string }[] = [
|
|
{ value: 'read', label: 'Lesen', icon: Eye, desc: 'Inhalt ansehen' },
|
|
{ value: 'write', label: 'Schreiben', icon: Pencil, desc: 'Inhalt bearbeiten, neue hinzufügen' },
|
|
{ value: 'admin', label: 'Admin', icon: Shield, desc: 'Bearbeiten + Löschen + Rechte verwalten' },
|
|
{ value: 'delete', label: 'Löschen', icon: Trash2, desc: 'Vollzugriff inkl. Löschen' },
|
|
];
|
|
|
|
function permIcon(level: string) {
|
|
const p = PERM_LEVELS.find((l) => l.value === level);
|
|
return p ? p.icon : Eye;
|
|
}
|
|
|
|
function permLabel(level: string) {
|
|
const p = PERM_LEVELS.find((l) => l.value === level);
|
|
return p ? p.label : level;
|
|
}
|
|
|
|
export function ShareDialog({ entityType, entityId, entityName, onClose }: ShareDialogProps) {
|
|
const { data: permData, isLoading } = useEntityPermissions(entityType, entityId);
|
|
const { data: accessData } = useEntityAccess(entityType, entityId);
|
|
const { data: usersData } = useUsers(1, 100);
|
|
const { data: groupsData } = useGroups();
|
|
const createMut = useCreateEntityPermission();
|
|
const updateMut = useUpdateEntityPermission();
|
|
const deleteMut = useDeleteEntityPermission();
|
|
|
|
const [showAdd, setShowAdd] = useState(false);
|
|
const [addType, setAddType] = useState<'user' | 'group'>('user');
|
|
const [addPrincipalId, setAddPrincipalId] = useState('');
|
|
const [addLevel, setAddLevel] = useState<PermissionLevel>('read');
|
|
const [addExpiresAt, setAddExpiresAt] = useState('');
|
|
|
|
const permissions = permData?.items ?? [];
|
|
const users = usersData?.items ?? [];
|
|
const groups = groupsData?.items ?? [];
|
|
const ownerName = accessData?.owner_name ?? null;
|
|
const ownerEmail = accessData?.owner_email ?? null;
|
|
|
|
const handleAdd = () => {
|
|
if (!addPrincipalId) return;
|
|
createMut.mutate(
|
|
{
|
|
entityType,
|
|
entityId,
|
|
data: {
|
|
user_id: addType === 'user' ? addPrincipalId : undefined,
|
|
group_id: addType === 'group' ? addPrincipalId : undefined,
|
|
permission_level: addLevel,
|
|
expires_at: addExpiresAt || null,
|
|
},
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
setShowAdd(false);
|
|
setAddPrincipalId('');
|
|
setAddLevel('read');
|
|
setAddExpiresAt('');
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
const handleUpdate = (perm: EntityPermission, newLevel: string) => {
|
|
updateMut.mutate({
|
|
entityType,
|
|
entityId,
|
|
permId: perm.id,
|
|
data: { permission_level: newLevel as PermissionLevel },
|
|
});
|
|
};
|
|
|
|
const handleUpdateExpiry = (perm: EntityPermission, expiresAt: string) => {
|
|
updateMut.mutate({
|
|
entityType,
|
|
entityId,
|
|
permId: perm.id,
|
|
data: { expires_at: expiresAt || null },
|
|
});
|
|
};
|
|
|
|
const handleDelete = (perm: EntityPermission) => {
|
|
if (!confirm(`Berechtigung für ${perm.user_name || perm.group_name || 'diesen Eintrag'} entfernen?`)) return;
|
|
deleteMut.mutate({ entityType, entityId, permId: perm.id });
|
|
};
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
<div
|
|
className="bg-white rounded-xl shadow-2xl w-full max-w-lg max-h-[80vh] flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-5 py-4 border-b border-secondary-200">
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="w-5 h-5 text-primary-600" strokeWidth={2} />
|
|
<h2 className="text-lg font-semibold text-secondary-800">Freigabe: {entityName}</h2>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-secondary-400 hover:text-secondary-600 p-1 rounded-md hover:bg-secondary-100"
|
|
aria-label="Schließen"
|
|
>
|
|
<X className="w-5 h-5" strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
{/* Info banner */}
|
|
<div className="mb-4 p-3 bg-primary-50 border border-primary-200 rounded-lg text-sm text-primary-700">
|
|
<p className="font-medium mb-1">Element teilen</p>
|
|
<p className="text-primary-600">
|
|
Gewähre Benutzern oder Gruppen Zugriff auf dieses Element. Die Berechtigungsstufe bestimmt,
|
|
welche Aktionen durchgeführt werden können.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Owner info */}
|
|
{ownerName && (
|
|
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm">
|
|
<div className="flex items-center gap-2 text-amber-700 font-medium mb-1">
|
|
<Crown className="w-4 h-4" strokeWidth={2} />
|
|
<span>Besitzer</span>
|
|
</div>
|
|
<p className="text-amber-600">
|
|
{ownerName}{ownerEmail ? ` (${ownerEmail})` : ''}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Existing permissions */}
|
|
{isLoading ? (
|
|
<div className="text-sm text-secondary-400 py-4 text-center">Laden…</div>
|
|
) : permissions.length === 0 ? (
|
|
<div className="text-sm text-secondary-400 py-4 text-center">
|
|
Noch keine Berechtigungen vergeben. Dieses Element ist nur für den Besitzer sichtbar.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{permissions.map((perm) => {
|
|
const Icon = perm.user_id ? User : Users;
|
|
const name = perm.user_name || perm.group_name || 'Unbekannt';
|
|
const email = perm.user_email;
|
|
const PermIcon = permIcon(perm.permission_level);
|
|
const isExpired = perm.expires_at ? new Date(perm.expires_at) < new Date() : false;
|
|
return (
|
|
<div
|
|
key={perm.id}
|
|
className={clsx(
|
|
'flex items-center gap-3 p-3 border rounded-lg',
|
|
isExpired
|
|
? 'border-red-200 bg-red-50'
|
|
: 'border-secondary-200 hover:bg-secondary-50'
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4 text-secondary-400 flex-shrink-0" strokeWidth={2} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm font-medium text-secondary-700 truncate">
|
|
{name}
|
|
{isExpired && (
|
|
<span className="ml-2 text-xs text-red-500 font-normal">(abgelaufen)</span>
|
|
)}
|
|
</div>
|
|
{email && (
|
|
<div className="text-xs text-secondary-400 truncate">{email}</div>
|
|
)}
|
|
{/* Expiry date */}
|
|
<div className="flex items-center gap-1 mt-1">
|
|
<Calendar className="w-3 h-3 text-secondary-400" strokeWidth={2} />
|
|
<input
|
|
type="date"
|
|
value={perm.expires_at ? perm.expires_at.split('T')[0] : ''}
|
|
onChange={(e) => handleUpdateExpiry(perm, e.target.value || '')}
|
|
className="text-xs border border-secondary-200 rounded px-1 py-0.5 bg-transparent focus:outline-none focus:ring-1 focus:ring-primary-500 text-secondary-500"
|
|
title="Ablaufdatum setzen"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* Permission level selector */}
|
|
<div className="relative">
|
|
<select
|
|
value={perm.permission_level}
|
|
onChange={(e) => handleUpdate(perm, e.target.value)}
|
|
className="appearance-none pl-8 pr-7 py-1.5 text-sm border border-secondary-200 rounded-md bg-white cursor-pointer hover:border-secondary-300 focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
>
|
|
{PERM_LEVELS.map((l) => (
|
|
<option key={l.value} value={l.value}>{l.label}</option>
|
|
))}
|
|
</select>
|
|
<PermIcon className="w-3.5 h-3.5 text-secondary-400 absolute left-2 top-1/2 -translate-y-1/2 pointer-events-none" strokeWidth={2} />
|
|
<ChevronDown className="w-3.5 h-3.5 text-secondary-400 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none" strokeWidth={2} />
|
|
</div>
|
|
{/* Delete */}
|
|
<button
|
|
onClick={() => handleDelete(perm)}
|
|
className="text-secondary-400 hover:text-red-600 p-1.5 rounded-md hover:bg-red-50 flex-shrink-0"
|
|
title="Entfernen"
|
|
aria-label="Berechtigung entfernen"
|
|
>
|
|
<Trash2 className="w-4 h-4" strokeWidth={2} />
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{/* Add new permission */}
|
|
{showAdd ? (
|
|
<div className="mt-4 p-4 border-2 border-primary-200 rounded-lg bg-primary-50/50">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Plus className="w-4 h-4 text-primary-600" strokeWidth={2} />
|
|
<span className="text-sm font-medium text-secondary-700">Neue Berechtigung</span>
|
|
</div>
|
|
|
|
{/* Type toggle */}
|
|
<div className="flex gap-2 mb-3">
|
|
<button
|
|
onClick={() => {
|
|
setAddType('user');
|
|
setAddPrincipalId('');
|
|
}}
|
|
className={clsx(
|
|
'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md border',
|
|
addType === 'user'
|
|
? 'bg-primary-600 text-white border-primary-600'
|
|
: 'bg-white text-secondary-600 border-secondary-200 hover:border-secondary-300'
|
|
)}
|
|
>
|
|
<User className="w-3.5 h-3.5" strokeWidth={2} />
|
|
Benutzer
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setAddType('group');
|
|
setAddPrincipalId('');
|
|
}}
|
|
className={clsx(
|
|
'flex items-center gap-1.5 px-3 py-1.5 text-sm rounded-md border',
|
|
addType === 'group'
|
|
? 'bg-primary-600 text-white border-primary-600'
|
|
: 'bg-white text-secondary-600 border-secondary-200 hover:border-secondary-300'
|
|
)}
|
|
>
|
|
<Users className="w-3.5 h-3.5" strokeWidth={2} />
|
|
Gruppe
|
|
</button>
|
|
</div>
|
|
|
|
{/* Principal select */}
|
|
<select
|
|
value={addPrincipalId}
|
|
onChange={(e) => setAddPrincipalId(e.target.value)}
|
|
className="w-full px-3 py-2 text-sm border border-secondary-200 rounded-md mb-3 bg-white cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
>
|
|
<option value="">
|
|
{addType === 'user' ? 'Benutzer auswählen…' : 'Gruppe auswählen…'}
|
|
</option>
|
|
{addType === 'user'
|
|
? users.map((u) => (
|
|
<option key={u.id} value={u.id}>
|
|
{u.name} ({u.email})
|
|
</option>
|
|
))
|
|
: groups.map((g) => (
|
|
<option key={g.id} value={g.id}>
|
|
{g.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
{/* Permission level */}
|
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
|
{PERM_LEVELS.map((l) => (
|
|
<button
|
|
key={l.value}
|
|
onClick={() => setAddLevel(l.value)}
|
|
className={clsx(
|
|
'flex items-start gap-2 p-2.5 text-left rounded-md border text-sm',
|
|
addLevel === l.value
|
|
? 'bg-primary-50 border-primary-400 text-primary-700'
|
|
: 'bg-white border-secondary-200 text-secondary-600 hover:border-secondary-300'
|
|
)}
|
|
>
|
|
<l.icon className="w-4 h-4 flex-shrink-0 mt-0.5" strokeWidth={2} />
|
|
<div>
|
|
<div className="font-medium">{l.label}</div>
|
|
<div className="text-xs text-secondary-400">{l.desc}</div>
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Expiry date */}
|
|
<div className="mb-3">
|
|
<label className="flex items-center gap-2 text-sm text-secondary-600 mb-1">
|
|
<Calendar className="w-4 h-4" strokeWidth={2} />
|
|
Ablaufdatum (optional)
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={addExpiresAt}
|
|
onChange={(e) => setAddExpiresAt(e.target.value)}
|
|
className="w-full px-3 py-2 text-sm border border-secondary-200 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-primary-500"
|
|
min={new Date().toISOString().split('T')[0]}
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
onClick={() => setShowAdd(false)}
|
|
className="px-3 py-1.5 text-sm text-secondary-600 hover:bg-secondary-100 rounded-md"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleAdd}
|
|
disabled={!addPrincipalId || createMut.isPending}
|
|
className="px-4 py-1.5 text-sm bg-primary-600 text-white rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{createMut.isPending ? 'Speichern…' : 'Hinzufügen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => setShowAdd(true)}
|
|
className="mt-4 w-full flex items-center justify-center gap-2 py-2.5 text-sm text-primary-600 border-2 border-dashed border-primary-200 rounded-lg hover:bg-primary-50 hover:border-primary-300 transition-colors"
|
|
>
|
|
<Plus className="w-4 h-4" strokeWidth={2} />
|
|
Berechtigung hinzufügen
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="px-5 py-3 border-t border-secondary-200 flex justify-end">
|
|
<button
|
|
onClick={onClose}
|
|
className="px-4 py-2 text-sm text-secondary-600 hover:bg-secondary-100 rounded-md"
|
|
>
|
|
Schließen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
}
|