bananas-commerce-admin 0.21.2 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/esm/Admin.js +35 -9
  2. package/dist/esm/Admin.js.map +1 -1
  3. package/dist/esm/extensions/pim/components/ProductRow.js +16 -0
  4. package/dist/esm/extensions/pim/components/ProductRow.js.map +1 -1
  5. package/dist/esm/extensions/pim/pages/product/list.js +1 -0
  6. package/dist/esm/extensions/pim/pages/product/list.js.map +1 -1
  7. package/dist/esm/extensions/user/components/InviteUserModal.js +106 -0
  8. package/dist/esm/extensions/user/components/InviteUserModal.js.map +1 -0
  9. package/dist/esm/extensions/user/index.js +30 -0
  10. package/dist/esm/extensions/user/index.js.map +1 -0
  11. package/dist/esm/extensions/user/pages/staff/detail.js +143 -0
  12. package/dist/esm/extensions/user/pages/staff/detail.js.map +1 -0
  13. package/dist/esm/extensions/user/pages/staff/list.js +61 -0
  14. package/dist/esm/extensions/user/pages/staff/list.js.map +1 -0
  15. package/dist/esm/extensions/user/types/staff.js +2 -0
  16. package/dist/esm/extensions/user/types/staff.js.map +1 -0
  17. package/dist/esm/index.js +1 -0
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/pages/AcceptInvitePage.js +125 -0
  20. package/dist/esm/pages/AcceptInvitePage.js.map +1 -0
  21. package/dist/esm/pages/ResetPasswordPage.js +125 -0
  22. package/dist/esm/pages/ResetPasswordPage.js.map +1 -0
  23. package/dist/types/extensions/pim/types/product.d.ts +1 -0
  24. package/dist/types/extensions/user/components/InviteUserModal.d.ts +9 -0
  25. package/dist/types/extensions/user/index.d.ts +4 -0
  26. package/dist/types/extensions/user/pages/staff/detail.d.ts +4 -0
  27. package/dist/types/extensions/user/pages/staff/list.d.ts +4 -0
  28. package/dist/types/extensions/user/types/staff.d.ts +24 -0
  29. package/dist/types/index.d.ts +1 -0
  30. package/dist/types/pages/AcceptInvitePage.d.ts +9 -0
  31. package/dist/types/pages/ResetPasswordPage.d.ts +9 -0
  32. package/package.json +1 -1
  33. package/src/Admin.tsx +53 -26
  34. package/src/extensions/pim/components/ProductRow.tsx +32 -0
  35. package/src/extensions/pim/pages/product/list.tsx +1 -0
  36. package/src/extensions/pim/types/product.ts +1 -0
  37. package/src/extensions/user/components/InviteUserModal.tsx +179 -0
  38. package/src/extensions/user/index.tsx +51 -0
  39. package/src/extensions/user/pages/staff/detail.tsx +262 -0
  40. package/src/extensions/user/pages/staff/list.tsx +100 -0
  41. package/src/extensions/user/types/staff.ts +27 -0
  42. package/src/index.ts +1 -0
  43. package/src/pages/AcceptInvitePage.tsx +232 -0
  44. package/src/pages/ResetPasswordPage.tsx +236 -0
@@ -0,0 +1,179 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ import {
4
+ Autocomplete,
5
+ Box,
6
+ Button,
7
+ Chip,
8
+ Dialog,
9
+ DialogActions,
10
+ DialogContent,
11
+ DialogTitle,
12
+ TextField,
13
+ } from "@mui/material";
14
+
15
+ import { useSnackbar } from "notistack";
16
+
17
+ import { useApi } from "../../../contexts/ApiContext";
18
+ import { useI18n } from "../../../contexts/I18nContext";
19
+ import { PageProps } from "../../../types";
20
+ import { GroupOption, StaffOptions } from "../types/staff";
21
+
22
+ export interface InviteUserModalProps {
23
+ open: boolean;
24
+ setOpen: (open: boolean) => void;
25
+ refresh: PageProps["refresh"];
26
+ }
27
+
28
+ export const InviteUserModal: React.FC<InviteUserModalProps> = ({ open, setOpen, refresh }) => {
29
+ const { t } = useI18n();
30
+ const api = useApi();
31
+ const { enqueueSnackbar } = useSnackbar();
32
+
33
+ const [email, setEmail] = useState("");
34
+ const [selectedGroups, setSelectedGroups] = useState<GroupOption[]>([]);
35
+ const [availableGroups, setAvailableGroups] = useState<GroupOption[]>([]);
36
+ const [loading, setLoading] = useState(false);
37
+ const [emailError, setEmailError] = useState("");
38
+
39
+ useEffect(() => {
40
+ const fetchOptions = async () => {
41
+ const response = await api.operations["user.staff:options"].call({});
42
+ if (response.ok) {
43
+ const options: StaffOptions = await response.json();
44
+ setAvailableGroups(options.groups);
45
+ }
46
+ };
47
+ if (open) {
48
+ fetchOptions();
49
+ }
50
+ }, [api, open]);
51
+
52
+ const handleClose = () => {
53
+ setEmail("");
54
+ setSelectedGroups([]);
55
+ setEmailError("");
56
+ setOpen(false);
57
+ };
58
+
59
+ const validateEmail = (email: string): boolean => {
60
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
61
+ return emailRegex.test(email);
62
+ };
63
+
64
+ const handleSubmit = async () => {
65
+ // Validate email
66
+ if (!email) {
67
+ setEmailError(t("Email is required"));
68
+ return;
69
+ }
70
+
71
+ if (!validateEmail(email)) {
72
+ setEmailError(t("Please enter a valid email address"));
73
+ return;
74
+ }
75
+
76
+ if (selectedGroups.length === 0) {
77
+ enqueueSnackbar(t("Please select at least one group"), { variant: "error" });
78
+ return;
79
+ }
80
+
81
+ setLoading(true);
82
+ setEmailError("");
83
+
84
+ try {
85
+ const response = await api.operations["user.staff:invite"].call({
86
+ body: {
87
+ email,
88
+ groups: selectedGroups.map((g) => g.id),
89
+ },
90
+ });
91
+
92
+ if (response.ok) {
93
+ const data = await response.json();
94
+ enqueueSnackbar(data.message || t("Invite sent successfully"), { variant: "success" });
95
+ handleClose();
96
+ refresh();
97
+ } else {
98
+ const errorData = await response.json();
99
+ if (errorData.detail && Array.isArray(errorData.detail)) {
100
+ const emailErrors = errorData.detail.filter((err: { loc?: string[]; msg?: string }) =>
101
+ err.loc?.includes("email"),
102
+ );
103
+ if (emailErrors.length > 0) {
104
+ setEmailError(emailErrors[0].msg);
105
+ } else {
106
+ enqueueSnackbar(errorData.detail[0]?.msg || t("Failed to send invite"), {
107
+ variant: "error",
108
+ });
109
+ }
110
+ } else {
111
+ enqueueSnackbar(t("Failed to send invite"), { variant: "error" });
112
+ }
113
+ }
114
+ } catch (error) {
115
+ console.error("[INVITE_USER]", error);
116
+ enqueueSnackbar(t("Failed to send invite"), { variant: "error" });
117
+ } finally {
118
+ setLoading(false);
119
+ }
120
+ };
121
+
122
+ return (
123
+ <Dialog open={open} sx={{ "& .MuiPaper-rounded": { borderRadius: 3 } }} onClose={handleClose}>
124
+ <DialogTitle sx={{ p: 3 }}>{t("Invite Staff User")}</DialogTitle>
125
+ <DialogContent sx={{ p: 3, minWidth: 500 }}>
126
+ <Box display="flex" flexDirection="column" gap={3}>
127
+ <TextField
128
+ fullWidth
129
+ error={!!emailError}
130
+ helperText={emailError || t("Enter the email address of the user to invite")}
131
+ label={t("Email")}
132
+ type="email"
133
+ value={email}
134
+ onChange={(e) => {
135
+ setEmail(e.target.value);
136
+ setEmailError("");
137
+ }}
138
+ />
139
+
140
+ <Autocomplete
141
+ fullWidth
142
+ multiple
143
+ getOptionLabel={(option) => option.name}
144
+ isOptionEqualToValue={(option, value) => option.id === value.id}
145
+ options={availableGroups}
146
+ renderInput={(params) => (
147
+ <TextField
148
+ {...params}
149
+ helperText={t("Select one or more groups for the user")}
150
+ label={t("Groups")}
151
+ />
152
+ )}
153
+ renderTags={(value, getTagProps) =>
154
+ value.map((option, index) => (
155
+ <Chip
156
+ {...getTagProps({ index })}
157
+ key={option.id}
158
+ label={option.name}
159
+ size="small"
160
+ />
161
+ ))
162
+ }
163
+ size="small"
164
+ value={selectedGroups}
165
+ onChange={(_, newValue) => setSelectedGroups(newValue)}
166
+ />
167
+ </Box>
168
+ </DialogContent>
169
+ <DialogActions sx={{ px: 3, py: 3 }}>
170
+ <Button onClick={handleClose}>{t("Cancel")}</Button>
171
+ <Button disabled={loading} variant="contained" onClick={handleSubmit}>
172
+ {loading ? t("Sending...") : t("Send Invite")}
173
+ </Button>
174
+ </DialogActions>
175
+ </Dialog>
176
+ );
177
+ };
178
+
179
+ export default InviteUserModal;
@@ -0,0 +1,51 @@
1
+ import PeopleIcon from "@mui/icons-material/People";
2
+
3
+ import { OpenAPI } from "openapi-types";
4
+
5
+ import { RouterExtension } from "../../router/Router";
6
+ import { NavigationOverrides, PageComponent } from "../../types";
7
+
8
+ const routes: Record<
9
+ string,
10
+ Record<
11
+ string,
12
+ {
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ page: () => PageComponent<any> | Promise<PageComponent<any>>;
15
+ request?: OpenAPI.Request;
16
+ defaultRequest?: OpenAPI.Request;
17
+ offline?: boolean;
18
+ }
19
+ >
20
+ > = {
21
+ staff: {
22
+ list: { page: async () => (await import("./pages/staff/list")).default },
23
+ detail: {
24
+ page: async () => (await import("./pages/staff/detail")).default,
25
+ },
26
+ },
27
+ };
28
+
29
+ export const router: RouterExtension = {
30
+ app: "user",
31
+ pages: (route) => {
32
+ const { page, ...hit } = routes[route.view]?.[route.action] ?? {};
33
+
34
+ if (page != null) {
35
+ return {
36
+ page: page(),
37
+ ...hit,
38
+ };
39
+ }
40
+
41
+ return undefined;
42
+ },
43
+ };
44
+
45
+ export const navigation: NavigationOverrides = {
46
+ "user.staff:list": {
47
+ icon: PeopleIcon,
48
+ title: "Staff",
49
+ permission: "user.view_user",
50
+ },
51
+ } as const;
@@ -0,0 +1,262 @@
1
+ import React, { useEffect, useState } from "react";
2
+
3
+ import { Autocomplete, Button, Chip, Stack, TextField } from "@mui/material";
4
+
5
+ import { DateTime } from "luxon";
6
+ import { useSnackbar } from "notistack";
7
+
8
+ import ActionBar from "../../../../components/ActionBar";
9
+ import Card from "../../../../components/Card";
10
+ import CardActions from "../../../../components/Card/CardActions";
11
+ import CardCancelButton from "../../../../components/Card/CardCancelButton";
12
+ import CardContent from "../../../../components/Card/CardContent";
13
+ import CardFieldSwitch from "../../../../components/Card/CardFieldSwitch";
14
+ import CardFieldText from "../../../../components/Card/CardFieldText";
15
+ import CardHeader from "../../../../components/Card/CardHeader";
16
+ import CardRow from "../../../../components/Card/CardRow";
17
+ import CardSaveButton from "../../../../components/Card/CardSaveButton";
18
+ import { Header } from "../../../../components/Header";
19
+ import { Page } from "../../../../components/Page";
20
+ import { TitleBar } from "../../../../components/TitleBar";
21
+ import Content, {
22
+ ContentWrapperWithActionBar,
23
+ LeftColumn,
24
+ RightColumn,
25
+ } from "../../../../containers/Content";
26
+ import { useApi } from "../../../../contexts/ApiContext";
27
+ import { useCardContext } from "../../../../contexts/CardContext";
28
+ import { useI18n } from "../../../../contexts/I18nContext";
29
+ import { useUser } from "../../../../contexts/UserContext";
30
+ import { PageComponent } from "../../../../types";
31
+ import { hasPermission } from "../../../../util/has_permission";
32
+ import { GroupOption, StaffDetail, StaffOptions } from "../../types/staff";
33
+
34
+ interface StaffUpdateValues {
35
+ is_active: string;
36
+ groups: string;
37
+ }
38
+
39
+ const StaffDetailPage: PageComponent<StaffDetail> = ({ data }) => {
40
+ const { t } = useI18n();
41
+ const api = useApi();
42
+ const { user: currentUser } = useUser();
43
+ const { enqueueSnackbar } = useSnackbar();
44
+ const [user, setUser] = useState<StaffDetail>(data);
45
+ const [availableGroups, setAvailableGroups] = useState<GroupOption[]>([]);
46
+ const [isResettingPassword, setIsResettingPassword] = useState(false);
47
+
48
+ const formatDateTime = (dateString: string | null) => {
49
+ if (!dateString) return t("Never");
50
+ return DateTime.fromISO(dateString as string).toISODate() ?? undefined;
51
+ };
52
+
53
+ useEffect(() => {
54
+ const fetchOptions = async () => {
55
+ const response = await api.operations["user.staff:options"].call({});
56
+ if (response.ok) {
57
+ const options: StaffOptions = await response.json();
58
+ setAvailableGroups(options.groups);
59
+ }
60
+ };
61
+ fetchOptions();
62
+ }, [api]);
63
+
64
+ const handleSubmit = async (values: StaffUpdateValues) => {
65
+ const response = await api.operations["user.staff:update"].call({
66
+ params: { id: user.id },
67
+ body: {
68
+ is_active: values.is_active === "on",
69
+ groups: values.groups
70
+ ? values.groups
71
+ .split(",")
72
+ .map((id) => parseInt(id.trim(), 10))
73
+ .filter((id) => !isNaN(id))
74
+ : [],
75
+ },
76
+ });
77
+
78
+ if (response.ok) {
79
+ const updatedUser = await response.json();
80
+ setUser(updatedUser);
81
+ return t("User updated successfully");
82
+ }
83
+
84
+ throw new Error("Failed to update user");
85
+ };
86
+
87
+ const handlePasswordReset = async () => {
88
+ setIsResettingPassword(true);
89
+
90
+ try {
91
+ const response = await api.operations["user.staff:password-reset-request"].call({
92
+ body: {
93
+ email: user.email,
94
+ },
95
+ });
96
+
97
+ if (response.ok) {
98
+ const data = await response.json();
99
+ enqueueSnackbar(data.message || t("Password reset link sent successfully"), {
100
+ variant: "success",
101
+ });
102
+ } else {
103
+ const errorData = await response.json();
104
+ if (errorData.detail && Array.isArray(errorData.detail)) {
105
+ enqueueSnackbar(errorData.detail[0]?.msg || t("Failed to send password reset link"), {
106
+ variant: "error",
107
+ });
108
+ } else {
109
+ enqueueSnackbar(t("Failed to send password reset link"), { variant: "error" });
110
+ }
111
+ }
112
+ } catch (error) {
113
+ console.error("[PASSWORD_RESET]", error);
114
+ enqueueSnackbar(t("Failed to send password reset link"), { variant: "error" });
115
+ } finally {
116
+ setIsResettingPassword(false);
117
+ }
118
+ };
119
+
120
+ const GroupsField = () => {
121
+ const { isEditing } = useCardContext();
122
+ const [selectedGroups, setSelectedGroups] = useState<GroupOption[]>(
123
+ availableGroups.filter((g) => user.groups.includes(g.name)),
124
+ );
125
+
126
+ if (isEditing) {
127
+ return (
128
+ <>
129
+ <Autocomplete
130
+ fullWidth
131
+ multiple
132
+ getOptionLabel={(option) => option.name}
133
+ isOptionEqualToValue={(option, value) => option.id === value.id}
134
+ options={availableGroups}
135
+ renderInput={(params) => (
136
+ <TextField {...params} helperText={t("Select user groups")} label={t("Groups")} />
137
+ )}
138
+ renderTags={(value, getTagProps) =>
139
+ value.map((option, index) => (
140
+ <Chip
141
+ {...getTagProps({ index })}
142
+ key={option.id}
143
+ label={option.name}
144
+ size="small"
145
+ />
146
+ ))
147
+ }
148
+ size="small"
149
+ value={selectedGroups}
150
+ onChange={(_, newValue) => setSelectedGroups(newValue)}
151
+ />
152
+ <input name="groups" type="hidden" value={selectedGroups.map((g) => g.id).join(", ")} />
153
+ </>
154
+ );
155
+ }
156
+
157
+ return (
158
+ <Stack direction="column" spacing={1}>
159
+ <strong>{t("Groups")}</strong>
160
+ <Stack direction="row" flexWrap="wrap" gap={1}>
161
+ {user.groups.length > 0 ? (
162
+ user.groups.map((group) => <Chip key={group} label={group} size="small" />)
163
+ ) : (
164
+ <span style={{ fontSize: "0.875rem", color: "#666" }}>{t("No groups")}</span>
165
+ )}
166
+ </Stack>
167
+ </Stack>
168
+ );
169
+ };
170
+
171
+ return (
172
+ <Page>
173
+ <Header>
174
+ <TitleBar title={user.email} />
175
+ </Header>
176
+
177
+ <ContentWrapperWithActionBar>
178
+ <Content layout="fixedWidth">
179
+ <LeftColumn>
180
+ <Card isEditable={false}>
181
+ <CardHeader title={t("User Details")} />
182
+
183
+ <CardContent>
184
+ <CardRow>
185
+ <CardFieldText
186
+ formName="email"
187
+ isEditable={false}
188
+ label={t("Email")}
189
+ value={user.email}
190
+ />
191
+ </CardRow>
192
+ <CardRow>
193
+ <CardFieldText
194
+ formName="last_login"
195
+ isEditable={false}
196
+ label={t("Last Login")}
197
+ value={formatDateTime(user.last_login)}
198
+ />
199
+ </CardRow>
200
+ <CardRow>
201
+ <CardFieldText
202
+ formName="date_created"
203
+ isEditable={false}
204
+ label={t("Date Joined")}
205
+ value={formatDateTime(user.date_created)}
206
+ />
207
+ </CardRow>
208
+ </CardContent>
209
+ </Card>
210
+ </LeftColumn>
211
+
212
+ <RightColumn>
213
+ <Card<StaffUpdateValues>
214
+ isCompact={true}
215
+ isEditable={hasPermission(currentUser, "user.change_user")}
216
+ onSubmit={handleSubmit}
217
+ >
218
+ <CardHeader title={t("Permissions")} />
219
+
220
+ <CardContent>
221
+ <CardRow>
222
+ <CardFieldSwitch
223
+ formName="is_active"
224
+ helperText={t(
225
+ "Indicates whether the user should be considered active. Uncheck this instead of deleting accounts.",
226
+ )}
227
+ label={t("Active")}
228
+ value={user.is_active}
229
+ />
230
+ </CardRow>
231
+
232
+ <CardRow>
233
+ <GroupsField />
234
+ </CardRow>
235
+ </CardContent>
236
+
237
+ <CardActions>
238
+ <CardCancelButton />
239
+ <CardSaveButton />
240
+ </CardActions>
241
+ </Card>
242
+ </RightColumn>
243
+ </Content>
244
+
245
+ {hasPermission(currentUser, "user.change_user") && (
246
+ <ActionBar>
247
+ <Button
248
+ color="primary"
249
+ disabled={isResettingPassword}
250
+ variant="contained"
251
+ onClick={handlePasswordReset}
252
+ >
253
+ {isResettingPassword ? t("Sending...") : t("Reset Password")}
254
+ </Button>
255
+ </ActionBar>
256
+ )}
257
+ </ContentWrapperWithActionBar>
258
+ </Page>
259
+ );
260
+ };
261
+
262
+ export default StaffDetailPage;
@@ -0,0 +1,100 @@
1
+ import React, { useState } from "react";
2
+
3
+ import CheckCircleIcon from "@mui/icons-material/CheckCircle";
4
+ import { Button, TableBody } from "@mui/material";
5
+
6
+ import ActionBar from "../../../../components/ActionBar";
7
+ import { Header } from "../../../../components/Header";
8
+ import { Page } from "../../../../components/Page";
9
+ import SearchBar from "../../../../components/SearchBar";
10
+ import { Table } from "../../../../components/Table";
11
+ import { TableCell } from "../../../../components/Table/TableCell";
12
+ import TableHead from "../../../../components/Table/TableHead";
13
+ import TableHeading from "../../../../components/Table/TableHeading";
14
+ import { NavigatingTableRow } from "../../../../components/Table/TableRow";
15
+ import TableCard from "../../../../components/TableCard";
16
+ import { TitleBar } from "../../../../components/TitleBar";
17
+ import Content, { ContentWrapperWithActionBar } from "../../../../containers/Content";
18
+ import { useI18n } from "../../../../contexts/I18nContext";
19
+ import { useRouter } from "../../../../contexts/RouterContext";
20
+ import { useUser } from "../../../../contexts/UserContext";
21
+ import { LimitOffset, PageComponent } from "../../../../types";
22
+ import { hasPermission } from "../../../../util/has_permission";
23
+ import InviteUserModal from "../../components/InviteUserModal";
24
+ import { StaffList } from "../../types/staff";
25
+
26
+ const StaffListPage: PageComponent<LimitOffset<StaffList>> = ({ data, refresh }) => {
27
+ const { t } = useI18n();
28
+ const { navigate } = useRouter();
29
+ const { user } = useUser();
30
+ const [inviteModalOpen, setInviteModalOpen] = useState(false);
31
+
32
+ return (
33
+ <Page>
34
+ <Header>
35
+ <TitleBar title="Staff">
36
+ <SearchBar
37
+ defaultValue={new URLSearchParams(window.location.search).get("search") ?? ""}
38
+ placeholder={t("Search users…")}
39
+ onSubmit={(input) => {
40
+ if (input === "") {
41
+ navigate("user.staff:list", {
42
+ replace: true,
43
+ });
44
+ } else {
45
+ navigate("user.staff:list", {
46
+ query: {
47
+ search: input,
48
+ },
49
+ replace: true,
50
+ });
51
+ }
52
+ }}
53
+ />
54
+ </TitleBar>
55
+ </Header>
56
+
57
+ <ContentWrapperWithActionBar>
58
+ <Content layout="fullWidth">
59
+ <TableCard>
60
+ <Table pagination count={data?.count}>
61
+ <TableHead>
62
+ <TableHeading>{t("Email")}</TableHeading>
63
+ <TableHeading>{t("Permissions")}</TableHeading>
64
+ <TableHeading>{t("Active")}</TableHeading>
65
+ </TableHead>
66
+
67
+ <TableBody>
68
+ {data?.results.map((user) => (
69
+ <NavigatingTableRow
70
+ key={user.id}
71
+ route="user.staff:detail"
72
+ routeParams={{ id: user.id }}
73
+ >
74
+ <TableCell>{user.email}</TableCell>
75
+ <TableCell>{user.groups.join(", ")}</TableCell>
76
+ <TableCell>
77
+ {user.is_active ? <CheckCircleIcon color="success" fontSize="small" /> : null}
78
+ </TableCell>
79
+ </NavigatingTableRow>
80
+ ))}
81
+ </TableBody>
82
+ </Table>
83
+ </TableCard>
84
+ </Content>
85
+
86
+ {hasPermission(user, "user.add_user") && (
87
+ <ActionBar>
88
+ <Button color="primary" variant="contained" onClick={() => setInviteModalOpen(true)}>
89
+ {t("Invite")}
90
+ </Button>
91
+ </ActionBar>
92
+ )}
93
+ </ContentWrapperWithActionBar>
94
+
95
+ <InviteUserModal open={inviteModalOpen} refresh={refresh} setOpen={setInviteModalOpen} />
96
+ </Page>
97
+ );
98
+ };
99
+
100
+ export default StaffListPage;
@@ -0,0 +1,27 @@
1
+ export interface StaffList {
2
+ id: number;
3
+ email: string;
4
+ is_active: boolean;
5
+ groups: string[];
6
+ }
7
+
8
+ export interface StaffDetail {
9
+ id: number;
10
+ email: string;
11
+ is_staff: boolean;
12
+ is_active: boolean;
13
+ is_superuser: boolean;
14
+ groups: string[];
15
+ permissions: string[];
16
+ last_login: string | null;
17
+ date_created: string;
18
+ }
19
+
20
+ export interface GroupOption {
21
+ id: number;
22
+ name: string;
23
+ }
24
+
25
+ export interface StaffOptions {
26
+ groups: GroupOption[];
27
+ }
package/src/index.ts CHANGED
@@ -70,6 +70,7 @@ export * as pos from "./extensions/pos";
70
70
  export * as pricing from "./extensions/pricing";
71
71
  export * as report from "./extensions/report";
72
72
  export * as subscription from "./extensions/subscription";
73
+ export * as user from "./extensions/user";
73
74
  export * as mui from "./mui";
74
75
  export type { RouterExtension } from "./router/Router";
75
76
  export * as themes from "./themes";