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",
|
"footerText": "Cannabis-Anbauverein — Sichere Mitgliederverwaltung",
|
||||||
"adminLogin": "Zum Admin-Login"
|
"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": {
|
"api": {
|
||||||
"loading": "Wird geladen...",
|
"loading": "Wird geladen...",
|
||||||
"error": "Fehler beim Laden der Daten.",
|
"error": "Fehler beim Laden der Daten.",
|
||||||
|
|||||||
@@ -293,6 +293,45 @@
|
|||||||
"footerText": "Cannabis cultivation club — Secure member management",
|
"footerText": "Cannabis cultivation club — Secure member management",
|
||||||
"adminLogin": "Go to Admin Login"
|
"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": {
|
"api": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"error": "Failed to load data.",
|
"error": "Failed to load data.",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"@hookform/resolvers": "3.9.1",
|
"@hookform/resolvers": "3.9.1",
|
||||||
"@radix-ui/react-alert-dialog": "1.1.1",
|
"@radix-ui/react-alert-dialog": "1.1.1",
|
||||||
"@radix-ui/react-avatar": "1.1.0",
|
"@radix-ui/react-avatar": "1.1.0",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.4",
|
||||||
"@radix-ui/react-collapsible": "1.1.0",
|
"@radix-ui/react-collapsible": "1.1.0",
|
||||||
"@radix-ui/react-dialog": "1.1.3",
|
"@radix-ui/react-dialog": "1.1.3",
|
||||||
"@radix-ui/react-direction": "1.1.0",
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
|||||||
Generated
+206
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@radix-ui/react-avatar':
|
'@radix-ui/react-avatar':
|
||||||
specifier: 1.1.0
|
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)
|
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':
|
'@radix-ui/react-collapsible':
|
||||||
specifier: 1.1.0
|
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)
|
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':
|
'@radix-ui/primitive@1.1.3':
|
||||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
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':
|
'@radix-ui/react-alert-dialog@1.1.1':
|
||||||
resolution: {integrity: sha512-wmCoJwj7byuVuiLKqDLlX7ClSUU0vd9sdCeM+2Ls+uf13+cpSJoMgwysHq1SGVVkJj5Xn0XWi1NoRCdkMpr6Mw==}
|
resolution: {integrity: sha512-wmCoJwj7byuVuiLKqDLlX7ClSUU0vd9sdCeM+2Ls+uf13+cpSJoMgwysHq1SGVVkJj5Xn0XWi1NoRCdkMpr6Mw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -760,6 +766,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-collapsible@1.1.0':
|
||||||
resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==}
|
resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -813,6 +832,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-context@1.1.0':
|
||||||
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
|
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -840,6 +868,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-dialog@1.1.1':
|
||||||
resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
|
resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1167,6 +1204,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-primitive@2.0.0':
|
||||||
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1219,6 +1269,19 @@ packages:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-roving-focus@1.1.0':
|
||||||
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
|
resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1294,6 +1357,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-toast@1.2.1':
|
||||||
resolution: {integrity: sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==}
|
resolution: {integrity: sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1356,6 +1428,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-use-effect-event@0.0.2':
|
||||||
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
|
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1365,6 +1446,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-use-escape-keydown@1.1.0':
|
||||||
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
|
resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1401,6 +1491,24 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-use-rect@1.1.0':
|
||||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1419,6 +1527,15 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-visually-hidden@1.1.0':
|
||||||
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4084,6 +4201,8 @@ snapshots:
|
|||||||
|
|
||||||
'@radix-ui/primitive@1.1.3': {}
|
'@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.0
|
'@radix-ui/primitive': 1.1.0
|
||||||
@@ -4128,6 +4247,22 @@ snapshots:
|
|||||||
'@types/react': 19.0.12
|
'@types/react': 19.0.12
|
||||||
'@types/react-dom': 19.0.4(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.0
|
'@radix-ui/primitive': 1.1.0
|
||||||
@@ -4174,6 +4309,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@radix-ui/react-context@1.1.0(@types/react@19.0.12)(react@19.1.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.3
|
react: 19.1.3
|
||||||
@@ -4192,6 +4333,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.0
|
'@radix-ui/primitive': 1.1.0
|
||||||
@@ -4532,6 +4679,15 @@ snapshots:
|
|||||||
'@types/react': 19.0.12
|
'@types/react': 19.0.12
|
||||||
'@types/react-dom': 19.0.4(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/react-slot': 1.1.0(@types/react@19.0.12)(react@19.1.3)
|
'@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': 19.0.12
|
||||||
'@types/react-dom': 19.0.4(@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.0
|
'@radix-ui/primitive': 1.1.0
|
||||||
@@ -4639,6 +4804,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@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:
|
dependencies:
|
||||||
'@radix-ui/primitive': 1.1.0
|
'@radix-ui/primitive': 1.1.0
|
||||||
@@ -4706,6 +4878,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.0.12)(react@19.1.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.12)(react@19.1.3)
|
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.12)(react@19.1.3)
|
||||||
@@ -4713,6 +4893,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.12)(react@19.1.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.1.3)
|
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.12)(react@19.1.3)
|
||||||
@@ -4739,6 +4926,18 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@radix-ui/react-use-rect@1.1.0(@types/react@19.0.12)(react@19.1.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/rect': 1.1.0
|
'@radix-ui/rect': 1.1.0
|
||||||
@@ -4753,6 +4952,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.12
|
'@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)':
|
'@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:
|
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)
|
'@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",
|
iconName: "FileText",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Einstellungen",
|
title: "Personal",
|
||||||
href: "/settings",
|
href: "/settings/staff",
|
||||||
iconName: "Settings",
|
iconName: "UserCog",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user