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:
@@ -293,6 +293,45 @@
|
||||
"footerText": "Cannabis-Anbauverein — Sichere Mitgliederverwaltung",
|
||||
"adminLogin": "Zum Admin-Login"
|
||||
},
|
||||
"staff": {
|
||||
"title": "Personal",
|
||||
"invite": "Neues Mitglied einladen",
|
||||
"name": "Name",
|
||||
"email": "E-Mail",
|
||||
"role": "Rolle",
|
||||
"permissions": "Berechtigungen",
|
||||
"status": "Status",
|
||||
"actions": "Aktionen",
|
||||
"active": "Aktiv",
|
||||
"revoked": "Widerrufen",
|
||||
"invited": "Eingeladen",
|
||||
"editPermissions": "Berechtigungen bearbeiten",
|
||||
"revokeAccess": "Zugang widerrufen",
|
||||
"revokeConfirm": "Zugang für {name} wirklich widerrufen? Der Mitarbeiter kann sich nicht mehr anmelden.",
|
||||
"revokeSuccess": "Zugang widerrufen.",
|
||||
"inviteTitle": "Mitarbeiter einladen",
|
||||
"inviteDesc": "Neues Teammitglied per E-Mail einladen und Berechtigungen zuweisen.",
|
||||
"inviteEmail": "E-Mail-Adresse",
|
||||
"inviteRole": "Rollenvorlage",
|
||||
"roleAusgabe": "Ausgabe",
|
||||
"roleLager": "Lager",
|
||||
"roleVorstand": "Vorstand",
|
||||
"roleCustom": "Benutzerdefiniert",
|
||||
"inviteSend": "Einladung senden",
|
||||
"inviteSuccess": "Einladung an {email} gesendet.",
|
||||
"permRecordDistribution": "Ausgabe erfassen",
|
||||
"permViewMemberList": "Mitgliederliste einsehen",
|
||||
"permViewMemberQuota": "Kontingent einsehen",
|
||||
"permAddMember": "Mitglieder anlegen",
|
||||
"permViewStock": "Lager einsehen",
|
||||
"permRecordStockIn": "Wareneingang",
|
||||
"permViewComplianceReport": "Berichte einsehen",
|
||||
"permManageGrowCalendar": "Anbaukalender verwalten",
|
||||
"savePermissions": "Speichern",
|
||||
"permissionsSaved": "Berechtigungen aktualisiert.",
|
||||
"cancel": "Abbrechen",
|
||||
"noStaff": "Noch keine Mitarbeiter vorhanden."
|
||||
},
|
||||
"api": {
|
||||
"loading": "Wird geladen...",
|
||||
"error": "Fehler beim Laden der Daten.",
|
||||
|
||||
@@ -293,6 +293,45 @@
|
||||
"footerText": "Cannabis cultivation club — Secure member management",
|
||||
"adminLogin": "Go to Admin Login"
|
||||
},
|
||||
"staff": {
|
||||
"title": "Staff",
|
||||
"invite": "Invite New Member",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"role": "Role",
|
||||
"permissions": "Permissions",
|
||||
"status": "Status",
|
||||
"actions": "Actions",
|
||||
"active": "Active",
|
||||
"revoked": "Revoked",
|
||||
"invited": "Invited",
|
||||
"editPermissions": "Edit Permissions",
|
||||
"revokeAccess": "Revoke Access",
|
||||
"revokeConfirm": "Really revoke access for {name}? The staff member will no longer be able to sign in.",
|
||||
"revokeSuccess": "Access revoked.",
|
||||
"inviteTitle": "Invite Staff Member",
|
||||
"inviteDesc": "Invite a new team member via email and assign permissions.",
|
||||
"inviteEmail": "Email address",
|
||||
"inviteRole": "Role template",
|
||||
"roleAusgabe": "Distribution",
|
||||
"roleLager": "Stock",
|
||||
"roleVorstand": "Board",
|
||||
"roleCustom": "Custom",
|
||||
"inviteSend": "Send Invitation",
|
||||
"inviteSuccess": "Invitation sent to {email}.",
|
||||
"permRecordDistribution": "Record Distribution",
|
||||
"permViewMemberList": "View Member List",
|
||||
"permViewMemberQuota": "View Quota",
|
||||
"permAddMember": "Add Members",
|
||||
"permViewStock": "View Stock",
|
||||
"permRecordStockIn": "Record Stock In",
|
||||
"permViewComplianceReport": "View Reports",
|
||||
"permManageGrowCalendar": "Manage Grow Calendar",
|
||||
"savePermissions": "Save",
|
||||
"permissionsSaved": "Permissions updated.",
|
||||
"cancel": "Cancel",
|
||||
"noStaff": "No staff members yet."
|
||||
},
|
||||
"api": {
|
||||
"loading": "Loading...",
|
||||
"error": "Failed to load data.",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@radix-ui/react-alert-dialog": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.3.4",
|
||||
"@radix-ui/react-collapsible": "1.1.0",
|
||||
"@radix-ui/react-dialog": "1.1.3",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
|
||||
Generated
+206
@@ -20,6 +20,9 @@ importers:
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.3.4
|
||||
version: 1.3.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)
|
||||
'@radix-ui/react-collapsible':
|
||||
specifier: 1.1.0
|
||||
version: 1.1.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)
|
||||
@@ -708,6 +711,9 @@ packages:
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
'@radix-ui/primitive@1.1.4':
|
||||
resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==}
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.1':
|
||||
resolution: {integrity: sha512-wmCoJwj7byuVuiLKqDLlX7ClSUU0vd9sdCeM+2Ls+uf13+cpSJoMgwysHq1SGVVkJj5Xn0XWi1NoRCdkMpr6Mw==}
|
||||
peerDependencies:
|
||||
@@ -760,6 +766,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-checkbox@1.3.4':
|
||||
resolution: {integrity: sha512-m3JmIOAX5ZzZ6VPjxEU2dbTOhoHi0nT5riwcDwe8idocsWf4a5DXJLDtZ6LfJwMBx7W+A2b7kp2TgPEKtaiF6A==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collapsible@1.1.0':
|
||||
resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==}
|
||||
peerDependencies:
|
||||
@@ -813,6 +832,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.3':
|
||||
resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.0':
|
||||
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
|
||||
peerDependencies:
|
||||
@@ -840,6 +868,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.4':
|
||||
resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dialog@1.1.1':
|
||||
resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
|
||||
peerDependencies:
|
||||
@@ -1167,6 +1204,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-presence@1.1.6':
|
||||
resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.0.0':
|
||||
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
||||
peerDependencies:
|
||||
@@ -1219,6 +1269,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.5':
|
||||
resolution: {integrity: sha512-zifXeB8Y88qCYx8PLZ5oQb32KwZub+s925mMoZsBBq9KUQqWKkREubTfs6ASjRPPBe7Jt9O8OHH89+95VG+grA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.0':
|
||||
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
|
||||
peerDependencies:
|
||||
@@ -1294,6 +1357,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.5':
|
||||
resolution: {integrity: sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-toast@1.2.1':
|
||||
resolution: {integrity: sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==}
|
||||
peerDependencies:
|
||||
@@ -1356,6 +1428,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.2.3':
|
||||
resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.2':
|
||||
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
|
||||
peerDependencies:
|
||||
@@ -1365,6 +1446,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.3':
|
||||
resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.1.0':
|
||||
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
|
||||
peerDependencies:
|
||||
@@ -1401,6 +1491,24 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.2':
|
||||
resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.2':
|
||||
resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.0':
|
||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||
peerDependencies:
|
||||
@@ -1419,6 +1527,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-size@1.1.2':
|
||||
resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.1.0':
|
||||
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
||||
peerDependencies:
|
||||
@@ -4084,6 +4201,8 @@ snapshots:
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/primitive@1.1.4': {}
|
||||
|
||||
'@radix-ui/react-alert-dialog@1.1.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
@@ -4128,6 +4247,22 @@ snapshots:
|
||||
'@types/react': 19.0.12
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||
|
||||
'@radix-ui/react-checkbox@1.3.4(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.4
|
||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.0.12)(react@19.1.3)
|
||||
'@radix-ui/react-context': 1.1.4(@types/react@19.0.12)(react@19.1.3)
|
||||
'@radix-ui/react-presence': 1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)
|
||||
'@radix-ui/react-primitive': 2.1.5(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.0.12)(react@19.1.3)
|
||||
'@radix-ui/react-use-previous': 1.1.2(@types/react@19.0.12)(react@19.1.3)
|
||||
'@radix-ui/react-use-size': 1.1.2(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
react-dom: 19.1.3(react@19.1.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||
|
||||
'@radix-ui/react-collapsible@1.1.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
@@ -4174,6 +4309,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.3(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-context@1.1.0(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
react: 19.1.3
|
||||
@@ -4192,6 +4333,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-context@1.1.4(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-dialog@1.1.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
@@ -4532,6 +4679,15 @@ snapshots:
|
||||
'@types/react': 19.0.12
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||
|
||||
'@radix-ui/react-presence@1.1.6(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
react-dom: 19.1.3(react@19.1.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||
|
||||
'@radix-ui/react-primitive@2.0.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.1.0(@types/react@19.0.12)(react@19.1.3)
|
||||
@@ -4568,6 +4724,15 @@ snapshots:
|
||||
'@types/react': 19.0.12
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||
|
||||
'@radix-ui/react-primitive@2.1.5(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.5(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
react-dom: 19.1.3(react@19.1.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
'@types/react-dom': 19.0.4(@types/react@19.0.12)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
@@ -4639,6 +4804,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-slot@1.2.5(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-toast@1.2.1(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
@@ -4706,6 +4878,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.0.12)(react@19.1.3)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.12)(react@19.1.3)
|
||||
@@ -4713,6 +4893,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-effect-event@0.0.3(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.1.3)
|
||||
@@ -4739,6 +4926,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-previous@1.1.2(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.0
|
||||
@@ -4753,6 +4952,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-use-size@1.1.2(@types/react@19.0.12)(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.0.12)(react@19.1.3)
|
||||
react: 19.1.3
|
||||
optionalDependencies:
|
||||
'@types/react': 19.0.12
|
||||
|
||||
'@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.3(react@19.1.3))(react@19.1.3)
|
||||
|
||||
@@ -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