diff --git a/cannamanage-frontend/messages/de.json b/cannamanage-frontend/messages/de.json index 7923ac6..27576b1 100644 --- a/cannamanage-frontend/messages/de.json +++ b/cannamanage-frontend/messages/de.json @@ -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.", diff --git a/cannamanage-frontend/messages/en.json b/cannamanage-frontend/messages/en.json index 8ffe64b..936fd31 100644 --- a/cannamanage-frontend/messages/en.json +++ b/cannamanage-frontend/messages/en.json @@ -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.", diff --git a/cannamanage-frontend/package.json b/cannamanage-frontend/package.json index 09c103f..62f4658 100644 --- a/cannamanage-frontend/package.json +++ b/cannamanage-frontend/package.json @@ -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", diff --git a/cannamanage-frontend/pnpm-lock.yaml b/cannamanage-frontend/pnpm-lock.yaml index 6ec7e99..748c8eb 100644 --- a/cannamanage-frontend/pnpm-lock.yaml +++ b/cannamanage-frontend/pnpm-lock.yaml @@ -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) diff --git a/cannamanage-frontend/src/app/(dashboard-layout)/settings/layout.tsx b/cannamanage-frontend/src/app/(dashboard-layout)/settings/layout.tsx new file mode 100644 index 0000000..94f1e5c --- /dev/null +++ b/cannamanage-frontend/src/app/(dashboard-layout)/settings/layout.tsx @@ -0,0 +1,7 @@ +export default function SettingsLayout({ + children, +}: { + children: React.ReactNode +}) { + return <>{children} +} diff --git a/cannamanage-frontend/src/app/(dashboard-layout)/settings/staff/page.tsx b/cannamanage-frontend/src/app/(dashboard-layout)/settings/staff/page.tsx new file mode 100644 index 0000000..dc7edc3 --- /dev/null +++ b/cannamanage-frontend/src/app/(dashboard-layout)/settings/staff/page.tsx @@ -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 = { + 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 +}) { + if (status === "ACTIVE") { + return ( + + {t("active")} + + ) + } + if (status === "REVOKED") { + return ( + + {t("revoked")} + + ) + } + return ( + + {t("invited")} + + ) +} + +// --- Permission Checkboxes --- + +function PermissionCheckboxes({ + selected, + onChange, + t, +}: { + selected: string[] + onChange: (perms: string[]) => void + t: ReturnType +}) { + const permLabels: Record = { + 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 ( +
+ {ALL_PERMISSIONS.map((perm) => ( +
+ toggle(perm)} + /> + +
+ ))} +
+ ) +} + +// --- Invite Staff Sheet --- + +function InviteStaffSheet({ + open, + onOpenChange, + t, +}: { + open: boolean + onOpenChange: (open: boolean) => void + t: ReturnType +}) { + const { toast } = useToast() + const inviteMutation = useInviteStaffMutation() + const [email, setEmail] = useState("") + const [displayName, setDisplayName] = useState("") + const [roleTemplate, setRoleTemplate] = useState("custom") + const [permissions, setPermissions] = useState([]) + + const handleRoleChange = (e: React.ChangeEvent) => { + 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 ( + + + + {t("inviteTitle")} + {t("inviteDesc")} + +
+
+ + setDisplayName(e.target.value)} + placeholder="Max Mustermann" + /> +
+
+ + setEmail(e.target.value)} + placeholder="max@example.de" + /> +
+
+ + +
+
+ + +
+ +
+
+
+ ) +} + +// --- Edit Permissions Dialog --- + +function EditPermissionsDialog({ + staffMember, + open, + onOpenChange, + t, +}: { + staffMember: StaffMember | null + open: boolean + onOpenChange: (open: boolean) => void + t: ReturnType +}) { + const { toast } = useToast() + const [permissions, setPermissions] = useState( + 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 ( + + + + {t("editPermissions")} + + {staffMember?.displayName} — {staffMember?.email} + + +
+ +
+ + + + +
+
+ ) +} + +// --- Revoke Confirmation --- + +function RevokeConfirmDialog({ + staffMember, + open, + onOpenChange, + t, +}: { + staffMember: StaffMember | null + open: boolean + onOpenChange: (open: boolean) => void + t: ReturnType +}) { + const { toast } = useToast() + const revokeMutation = useRevokeStaffMutation() + + const handleRevoke = () => { + if (!staffMember) return + revokeMutation.mutate(staffMember.id, { + onSuccess: () => { + toast({ description: t("revokeSuccess") }) + onOpenChange(false) + }, + }) + } + + return ( + + + + {t("revokeAccess")} + + {t("revokeConfirm", { name: staffMember?.displayName ?? "" })} + + + + {t("cancel")} + + {t("revokeAccess")} + + + + + ) +} + +// --- Main Page --- + +export default function StaffPage() { + const t = useTranslations("staff") + const { data: staffData, isLoading } = useStaffListQuery() + + const [inviteOpen, setInviteOpen] = useState(false) + const [editTarget, setEditTarget] = useState(null) + const [revokeTarget, setRevokeTarget] = useState(null) + + // Fallback to mock data when API unavailable + const staff: StaffMember[] = staffData ?? mockStaffAccounts + + const permLabel = (perm: string): string => { + const labels: Record = { + 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 ( +
+ {/* Header */} +
+
+ +

{t("title")}

+
+ +
+ + {/* Table */} + {isLoading ? ( + + ) : ( +
+ + + + {t("name")} + {t("email")} + {t("role")} + {t("permissions")} + {t("status")} + {t("actions")} + + + + {staff.map((member) => ( + + + {member.displayName} + + + {member.email} + + + {member.role} + + +
+ {member.permissions.slice(0, 3).map((perm) => ( + + {permLabel(perm)} + + ))} + {member.permissions.length > 3 && ( + + +{member.permissions.length - 3} + + )} +
+
+ + + + + {member.status === "ACTIVE" && ( +
+ + +
+ )} +
+
+ ))} + {staff.length === 0 && ( + + + {t("noStaff")} + + + )} +
+
+
+ )} + + {/* Dialogs */} + + { + if (!open) setEditTarget(null) + }} + t={t} + /> + { + if (!open) setRevokeTarget(null) + }} + t={t} + /> +
+ ) +} diff --git a/cannamanage-frontend/src/components/ui/checkbox.tsx b/cannamanage-frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..4972102 --- /dev/null +++ b/cannamanage-frontend/src/components/ui/checkbox.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/cannamanage-frontend/src/data/mock/staff.ts b/cannamanage-frontend/src/data/mock/staff.ts new file mode 100644 index 0000000..4e78ab8 --- /dev/null +++ b/cannamanage-frontend/src/data/mock/staff.ts @@ -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", + }, +] diff --git a/cannamanage-frontend/src/data/navigations.ts b/cannamanage-frontend/src/data/navigations.ts index c1226ee..dbab4cc 100644 --- a/cannamanage-frontend/src/data/navigations.ts +++ b/cannamanage-frontend/src/data/navigations.ts @@ -30,9 +30,9 @@ export const navigationsData: NavigationType[] = [ iconName: "FileText", }, { - title: "Einstellungen", - href: "/settings", - iconName: "Settings", + title: "Personal", + href: "/settings/staff", + iconName: "UserCog", }, ], },