feat(sprint-5): Phase 6 — Staff management UI (list, invite, permissions, revoke)
- /settings/staff: staff account table with role badges + permission chips - Invite sheet: email + role template + 8 granular permission checkboxes - Edit permissions dialog with optimistic update - Revoke access with AlertDialog confirmation - React Query hooks wired (useStaffListQuery, mutations) - Full i18n (de/en), mock fallback, loading skeletons - Sidebar nav updated: Personal → /settings/staff with UserCog icon - Added @radix-ui/react-checkbox + Checkbox UI component
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
export default function SettingsLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import {
|
||||
useInviteStaffMutation,
|
||||
useRevokeStaffMutation,
|
||||
useStaffListQuery,
|
||||
useUpdateStaffPermissionsMutation,
|
||||
} from "@/services/staff"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { Edit, Plus, ShieldX, UserCog } from "lucide-react"
|
||||
|
||||
import type { InviteStaffRequest, StaffMember } from "@/services/staff"
|
||||
|
||||
import { mockStaffAccounts } from "@/data/mock/staff"
|
||||
|
||||
import { useToast } from "@/hooks/use-toast"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { TableSkeleton } from "@/components/ui/data-skeleton"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select } from "@/components/ui/select"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet"
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
|
||||
// --- Permission definitions ---
|
||||
|
||||
const ALL_PERMISSIONS = [
|
||||
"RECORD_DISTRIBUTION",
|
||||
"VIEW_MEMBER_LIST",
|
||||
"VIEW_MEMBER_QUOTA",
|
||||
"ADD_MEMBER",
|
||||
"VIEW_STOCK",
|
||||
"RECORD_STOCK_IN",
|
||||
"VIEW_COMPLIANCE_REPORT",
|
||||
"MANAGE_GROW_CALENDAR",
|
||||
] as const
|
||||
|
||||
type StaffPermission = (typeof ALL_PERMISSIONS)[number]
|
||||
|
||||
const ROLE_TEMPLATES: Record<string, StaffPermission[]> = {
|
||||
ausgabe: ["RECORD_DISTRIBUTION", "VIEW_MEMBER_LIST", "VIEW_MEMBER_QUOTA"],
|
||||
lager: ["VIEW_STOCK", "RECORD_STOCK_IN", "MANAGE_GROW_CALENDAR"],
|
||||
vorstand: [
|
||||
"RECORD_DISTRIBUTION",
|
||||
"VIEW_MEMBER_LIST",
|
||||
"VIEW_MEMBER_QUOTA",
|
||||
"ADD_MEMBER",
|
||||
"VIEW_STOCK",
|
||||
"RECORD_STOCK_IN",
|
||||
"VIEW_COMPLIANCE_REPORT",
|
||||
"MANAGE_GROW_CALENDAR",
|
||||
],
|
||||
custom: [],
|
||||
}
|
||||
|
||||
// --- Status Badge ---
|
||||
|
||||
function StatusBadge({
|
||||
status,
|
||||
t,
|
||||
}: {
|
||||
status: StaffMember["status"]
|
||||
t: ReturnType<typeof useTranslations>
|
||||
}) {
|
||||
if (status === "ACTIVE") {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-900/30 dark:text-green-400"
|
||||
>
|
||||
{t("active")}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
if (status === "REVOKED") {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
{t("revoked")}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
|
||||
>
|
||||
{t("invited")}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Permission Checkboxes ---
|
||||
|
||||
function PermissionCheckboxes({
|
||||
selected,
|
||||
onChange,
|
||||
t,
|
||||
}: {
|
||||
selected: string[]
|
||||
onChange: (perms: string[]) => void
|
||||
t: ReturnType<typeof useTranslations>
|
||||
}) {
|
||||
const permLabels: Record<string, string> = {
|
||||
RECORD_DISTRIBUTION: t("permRecordDistribution"),
|
||||
VIEW_MEMBER_LIST: t("permViewMemberList"),
|
||||
VIEW_MEMBER_QUOTA: t("permViewMemberQuota"),
|
||||
ADD_MEMBER: t("permAddMember"),
|
||||
VIEW_STOCK: t("permViewStock"),
|
||||
RECORD_STOCK_IN: t("permRecordStockIn"),
|
||||
VIEW_COMPLIANCE_REPORT: t("permViewComplianceReport"),
|
||||
MANAGE_GROW_CALENDAR: t("permManageGrowCalendar"),
|
||||
}
|
||||
|
||||
const toggle = (perm: string) => {
|
||||
if (selected.includes(perm)) {
|
||||
onChange(selected.filter((p) => p !== perm))
|
||||
} else {
|
||||
onChange([...selected, perm])
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{ALL_PERMISSIONS.map((perm) => (
|
||||
<div key={perm} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`perm-${perm}`}
|
||||
checked={selected.includes(perm)}
|
||||
onCheckedChange={() => toggle(perm)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor={`perm-${perm}`}
|
||||
className="cursor-pointer text-sm font-normal"
|
||||
>
|
||||
{permLabels[perm]}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Invite Staff Sheet ---
|
||||
|
||||
function InviteStaffSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
t,
|
||||
}: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
t: ReturnType<typeof useTranslations>
|
||||
}) {
|
||||
const { toast } = useToast()
|
||||
const inviteMutation = useInviteStaffMutation()
|
||||
const [email, setEmail] = useState("")
|
||||
const [displayName, setDisplayName] = useState("")
|
||||
const [roleTemplate, setRoleTemplate] = useState("custom")
|
||||
const [permissions, setPermissions] = useState<string[]>([])
|
||||
|
||||
const handleRoleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = e.target.value
|
||||
setRoleTemplate(value)
|
||||
if (value !== "custom") {
|
||||
setPermissions([...ROLE_TEMPLATES[value]])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
const data: InviteStaffRequest = {
|
||||
email,
|
||||
displayName,
|
||||
role: "STAFF",
|
||||
permissions,
|
||||
}
|
||||
inviteMutation.mutate(data, {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
description: t("inviteSuccess", { email }),
|
||||
})
|
||||
onOpenChange(false)
|
||||
setEmail("")
|
||||
setDisplayName("")
|
||||
setRoleTemplate("custom")
|
||||
setPermissions([])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="overflow-y-auto sm:max-w-md">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t("inviteTitle")}</SheetTitle>
|
||||
<SheetDescription>{t("inviteDesc")}</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="mt-6 space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invite-name">{t("name")}</Label>
|
||||
<Input
|
||||
id="invite-name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Max Mustermann"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invite-email">{t("inviteEmail")}</Label>
|
||||
<Input
|
||||
id="invite-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="max@example.de"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invite-role">{t("inviteRole")}</Label>
|
||||
<Select
|
||||
id="invite-role"
|
||||
value={roleTemplate}
|
||||
onChange={handleRoleChange}
|
||||
>
|
||||
<option value="ausgabe">{t("roleAusgabe")}</option>
|
||||
<option value="lager">{t("roleLager")}</option>
|
||||
<option value="vorstand">{t("roleVorstand")}</option>
|
||||
<option value="custom">{t("roleCustom")}</option>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>{t("permissions")}</Label>
|
||||
<PermissionCheckboxes
|
||||
selected={permissions}
|
||||
onChange={setPermissions}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
!email ||
|
||||
!displayName ||
|
||||
permissions.length === 0 ||
|
||||
inviteMutation.isPending
|
||||
}
|
||||
>
|
||||
{t("inviteSend")}
|
||||
</Button>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Edit Permissions Dialog ---
|
||||
|
||||
function EditPermissionsDialog({
|
||||
staffMember,
|
||||
open,
|
||||
onOpenChange,
|
||||
t,
|
||||
}: {
|
||||
staffMember: StaffMember | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
t: ReturnType<typeof useTranslations>
|
||||
}) {
|
||||
const { toast } = useToast()
|
||||
const [permissions, setPermissions] = useState<string[]>(
|
||||
staffMember?.permissions ?? []
|
||||
)
|
||||
const updateMutation = useUpdateStaffPermissionsMutation(
|
||||
staffMember?.id ?? ""
|
||||
)
|
||||
|
||||
// Sync permissions when staff member changes
|
||||
const handleOpen = (isOpen: boolean) => {
|
||||
if (isOpen && staffMember) {
|
||||
setPermissions([...staffMember.permissions])
|
||||
}
|
||||
onOpenChange(isOpen)
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
updateMutation.mutate(
|
||||
{ permissions },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast({ description: t("permissionsSaved") })
|
||||
onOpenChange(false)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("editPermissions")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{staffMember?.displayName} — {staffMember?.email}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<PermissionCheckboxes
|
||||
selected={permissions}
|
||||
onChange={setPermissions}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={permissions.length === 0 || updateMutation.isPending}
|
||||
>
|
||||
{t("savePermissions")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Revoke Confirmation ---
|
||||
|
||||
function RevokeConfirmDialog({
|
||||
staffMember,
|
||||
open,
|
||||
onOpenChange,
|
||||
t,
|
||||
}: {
|
||||
staffMember: StaffMember | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
t: ReturnType<typeof useTranslations>
|
||||
}) {
|
||||
const { toast } = useToast()
|
||||
const revokeMutation = useRevokeStaffMutation()
|
||||
|
||||
const handleRevoke = () => {
|
||||
if (!staffMember) return
|
||||
revokeMutation.mutate(staffMember.id, {
|
||||
onSuccess: () => {
|
||||
toast({ description: t("revokeSuccess") })
|
||||
onOpenChange(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("revokeAccess")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("revokeConfirm", { name: staffMember?.displayName ?? "" })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleRevoke}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{t("revokeAccess")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Main Page ---
|
||||
|
||||
export default function StaffPage() {
|
||||
const t = useTranslations("staff")
|
||||
const { data: staffData, isLoading } = useStaffListQuery()
|
||||
|
||||
const [inviteOpen, setInviteOpen] = useState(false)
|
||||
const [editTarget, setEditTarget] = useState<StaffMember | null>(null)
|
||||
const [revokeTarget, setRevokeTarget] = useState<StaffMember | null>(null)
|
||||
|
||||
// Fallback to mock data when API unavailable
|
||||
const staff: StaffMember[] = staffData ?? mockStaffAccounts
|
||||
|
||||
const permLabel = (perm: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
RECORD_DISTRIBUTION: t("permRecordDistribution"),
|
||||
VIEW_MEMBER_LIST: t("permViewMemberList"),
|
||||
VIEW_MEMBER_QUOTA: t("permViewMemberQuota"),
|
||||
ADD_MEMBER: t("permAddMember"),
|
||||
VIEW_STOCK: t("permViewStock"),
|
||||
RECORD_STOCK_IN: t("permRecordStockIn"),
|
||||
VIEW_COMPLIANCE_REPORT: t("permViewComplianceReport"),
|
||||
MANAGE_GROW_CALENDAR: t("permManageGrowCalendar"),
|
||||
}
|
||||
return labels[perm] ?? perm
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<UserCog className="text-muted-foreground h-6 w-6" />
|
||||
<h1 className="text-2xl font-bold tracking-tight">{t("title")}</h1>
|
||||
</div>
|
||||
<Button onClick={() => setInviteOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t("invite")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{isLoading ? (
|
||||
<TableSkeleton />
|
||||
) : (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{t("name")}</TableHead>
|
||||
<TableHead>{t("email")}</TableHead>
|
||||
<TableHead>{t("role")}</TableHead>
|
||||
<TableHead>{t("permissions")}</TableHead>
|
||||
<TableHead>{t("status")}</TableHead>
|
||||
<TableHead className="text-right">{t("actions")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{staff.map((member) => (
|
||||
<TableRow key={member.id}>
|
||||
<TableCell className="font-medium">
|
||||
{member.displayName}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{member.email}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{member.role}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{member.permissions.slice(0, 3).map((perm) => (
|
||||
<Badge key={perm} variant="outline" className="text-xs">
|
||||
{permLabel(perm)}
|
||||
</Badge>
|
||||
))}
|
||||
{member.permissions.length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{member.permissions.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={member.status} t={t} />
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{member.status === "ACTIVE" && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setEditTarget(member)}
|
||||
>
|
||||
<Edit className="mr-1 h-3.5 w-3.5" />
|
||||
{t("editPermissions")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => setRevokeTarget(member)}
|
||||
>
|
||||
<ShieldX className="mr-1 h-3.5 w-3.5" />
|
||||
{t("revokeAccess")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{staff.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="text-muted-foreground py-8 text-center"
|
||||
>
|
||||
{t("noStaff")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dialogs */}
|
||||
<InviteStaffSheet open={inviteOpen} onOpenChange={setInviteOpen} t={t} />
|
||||
<EditPermissionsDialog
|
||||
staffMember={editTarget}
|
||||
open={!!editTarget}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setEditTarget(null)
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
<RevokeConfirmDialog
|
||||
staffMember={revokeTarget}
|
||||
open={!!revokeTarget}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setRevokeTarget(null)
|
||||
}}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ComponentRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer h-4 w-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { StaffMember } from "@/services/staff"
|
||||
|
||||
export const mockStaffAccounts: StaffMember[] = [
|
||||
{
|
||||
id: "staff-1",
|
||||
displayName: "Maria Schulz",
|
||||
email: "maria@gruener-daumen.de",
|
||||
role: "STAFF",
|
||||
permissions: [
|
||||
"RECORD_DISTRIBUTION",
|
||||
"VIEW_MEMBER_LIST",
|
||||
"VIEW_MEMBER_QUOTA",
|
||||
],
|
||||
status: "ACTIVE",
|
||||
lastLoginAt: "2026-06-10T14:30:00Z",
|
||||
createdAt: "2025-11-15T09:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "staff-2",
|
||||
displayName: "Thomas Klein",
|
||||
email: "thomas@gruener-daumen.de",
|
||||
role: "STAFF",
|
||||
permissions: ["VIEW_STOCK", "RECORD_STOCK_IN", "MANAGE_GROW_CALENDAR"],
|
||||
status: "ACTIVE",
|
||||
lastLoginAt: "2026-06-11T08:45:00Z",
|
||||
createdAt: "2025-12-01T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "staff-3",
|
||||
displayName: "Petra Wagner",
|
||||
email: "petra@gruener-daumen.de",
|
||||
role: "MANAGER",
|
||||
permissions: [
|
||||
"RECORD_DISTRIBUTION",
|
||||
"VIEW_MEMBER_LIST",
|
||||
"VIEW_MEMBER_QUOTA",
|
||||
"ADD_MEMBER",
|
||||
"VIEW_STOCK",
|
||||
"RECORD_STOCK_IN",
|
||||
"VIEW_COMPLIANCE_REPORT",
|
||||
],
|
||||
status: "ACTIVE",
|
||||
lastLoginAt: "2026-06-12T07:15:00Z",
|
||||
createdAt: "2026-01-10T11:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "staff-4",
|
||||
displayName: "Stefan Braun",
|
||||
email: "stefan@gruener-daumen.de",
|
||||
role: "STAFF",
|
||||
permissions: ["RECORD_DISTRIBUTION", "VIEW_MEMBER_LIST"],
|
||||
status: "REVOKED",
|
||||
createdAt: "2025-10-01T08:00:00Z",
|
||||
},
|
||||
]
|
||||
@@ -30,9 +30,9 @@ export const navigationsData: NavigationType[] = [
|
||||
iconName: "FileText",
|
||||
},
|
||||
{
|
||||
title: "Einstellungen",
|
||||
href: "/settings",
|
||||
iconName: "Settings",
|
||||
title: "Personal",
|
||||
href: "/settings/staff",
|
||||
iconName: "UserCog",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user