@micha.bigler/ui-core-micha 4.7.1 → 4.8.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.
- package/CHANGELOG.md +15 -0
- package/dist/billing/BillingAccessCell.js +32 -0
- package/dist/billing/FreeAccessSwitch.js +41 -0
- package/dist/billing/OperatorOverview.js +66 -0
- package/dist/billing/SubscriptionSection.js +5 -1
- package/dist/billing/api.js +13 -0
- package/dist/billing/format.js +8 -0
- package/dist/billing/useBillingUsersColumn.js +21 -0
- package/dist/billing/useOperatorAccounts.js +63 -0
- package/dist/billing/wire.js +4 -0
- package/dist/i18n/billingTranslations.js +39 -0
- package/dist/index.js +5 -0
- package/dist/pages/AccountPage.js +6 -2
- package/dist/theme/kitSxRegistry.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.8.0 - UCM-BILL-2
|
|
4
|
+
|
|
5
|
+
Operator surfaces for billing and a fuller "Abo" section, against the `django-core-micha` operator endpoints
|
|
6
|
+
(DCM-BILL-5, dcm >= 3.7.0). `OperatorOverview` (month selector; active accounts by source, new and ended subscriptions,
|
|
7
|
+
packages sold, tokens granted, used and expired, AI cost and cost per token, AI cost by model) is read-only and renders
|
|
8
|
+
nothing without the operator right or without billing. `useBillingUsersColumn` gives the shared Users tab the column
|
|
9
|
+
"Zugang" (plan and end date, team seat, free access, read-only) with the switch "Ohne Abo freigeschaltet" per account
|
|
10
|
+
(`FreeAccessSwitch`: a mandatory reason when switching on, a confirmation when switching off; the switch only shows what
|
|
11
|
+
the server confirmed); `AccountPage` takes the new optional prop `billingUsers` (`true` or a billing api) and adds the
|
|
12
|
+
column, without it nothing changes and no request is made. `SubscriptionSection` shows plan name and period with
|
|
13
|
+
"renews" or "ends", the token grants with remaining amount and expiry, for a team payer "n of m seats used" with the
|
|
14
|
+
holders' names, and for a seat holder "team seat until"; a `me/` without the new keys renders as before.
|
|
15
|
+
`createBillingApi` gets `getOperatorOverview`, `listOperatorAccounts`, `grantFreeAccess`, `revokeFreeAccess`; new keys
|
|
16
|
+
`Billing.OP_*`.
|
|
17
|
+
|
|
3
18
|
## 4.7.1 - UCM-BILL-4
|
|
4
19
|
|
|
5
20
|
The withdrawal dialog no longer says the subscription will not renew for a token package: for `kind: token_package` it says that the unused tokens of the purchase are removed (`Billing.WITHDRAWAL_EFFECT_TOKENS`, de, de_informal, en, fr, sw).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Button, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatDate } from './format';
|
|
6
|
+
import { FreeAccessSwitch } from './FreeAccessSwitch';
|
|
7
|
+
export function accountAccessText(account, t, language) {
|
|
8
|
+
if (!account)
|
|
9
|
+
return '–'; // no row for this person (not loaded): say nothing rather than claim read-only
|
|
10
|
+
const access = account.access || {};
|
|
11
|
+
if (access.source === 'subscription') {
|
|
12
|
+
const date = formatDate(account.term_ends_at || access.ends_at, language);
|
|
13
|
+
const plan = account.plan_key || '';
|
|
14
|
+
return [t('Billing.OP_PLAN', { plan }), date && t('Billing.OP_UNTIL', { date }), access.status === 'read_only' && t('Billing.OP_READ_ONLY')].filter(Boolean).join(' · ');
|
|
15
|
+
}
|
|
16
|
+
if (access.source === 'team') {
|
|
17
|
+
const date = formatDate(access.ends_at, language);
|
|
18
|
+
return [t('Billing.OP_TEAM_SEAT'), date && t('Billing.OP_UNTIL', { date }), access.status === 'read_only' && t('Billing.OP_READ_ONLY')].filter(Boolean).join(' · ');
|
|
19
|
+
}
|
|
20
|
+
if (access.source === 'free')
|
|
21
|
+
return [t('Billing.OP_FREE_ACCESS'), access.status === 'read_only' && t('Billing.OP_READ_ONLY')].filter(Boolean).join(' · ');
|
|
22
|
+
return t('Billing.OP_READ_ONLY');
|
|
23
|
+
}
|
|
24
|
+
export function BillingAccessCell({ account, api, onChanged }) {
|
|
25
|
+
const { t, i18n } = useTranslation();
|
|
26
|
+
return _jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { children: accountAccessText(account, t, i18n.language) }), account && _jsx(FreeAccessSwitch, { account: account, api: api, onChanged: onChanged })] });
|
|
27
|
+
}
|
|
28
|
+
export function BillingAccessError({ onRetry }) {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
return _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { color: "error", children: t('Billing.OP_LOAD_ERROR') }), onRetry && _jsx(Button, { size: "small", onClick: onRetry, children: t('Billing.RETRY') })] });
|
|
31
|
+
}
|
|
32
|
+
export default BillingAccessCell;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useId, useRef, useState } from 'react';
|
|
3
|
+
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Switch, TextField, Box } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
export const FREE_ACCESS_SWITCH_ALERT_SX = { width: '100%' };
|
|
6
|
+
const statusOf = (error) => { var _a; return (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status; };
|
|
7
|
+
export function FreeAccessSwitch({ account, api, onChanged, sx }) {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const [reason, setReason] = useState('');
|
|
11
|
+
const [busy, setBusy] = useState(false);
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
const inFlight = useRef(false);
|
|
14
|
+
const titleId = useId();
|
|
15
|
+
const active = Boolean(account === null || account === void 0 ? void 0 : account.free_access);
|
|
16
|
+
const openDialog = () => { setReason(''); setError(null); setOpen(true); };
|
|
17
|
+
const close = () => { if (!busy)
|
|
18
|
+
setOpen(false); };
|
|
19
|
+
const submit = async () => {
|
|
20
|
+
if (inFlight.current)
|
|
21
|
+
return;
|
|
22
|
+
inFlight.current = true;
|
|
23
|
+
setBusy(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const answer = active ? await api.revokeFreeAccess(account.user_id) : await api.grantFreeAccess(account.user_id, reason.trim());
|
|
27
|
+
onChanged === null || onChanged === void 0 ? void 0 : onChanged(answer);
|
|
28
|
+
setOpen(false);
|
|
29
|
+
}
|
|
30
|
+
catch (failure) {
|
|
31
|
+
setError(statusOf(failure) === 403 ? 'Billing.OP_FORBIDDEN' : 'Billing.OP_SAVE_ERROR');
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
inFlight.current = false;
|
|
35
|
+
setBusy(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const validReason = reason.trim().length > 0 && reason.trim().length <= 1000;
|
|
39
|
+
return _jsxs(_Fragment, { children: [_jsx(Box, { sx: sx, children: _jsx(FormControlLabel, { control: _jsx(Switch, { checked: active, disabled: busy, onChange: openDialog, slotProps: { input: { 'aria-label': `${t('Billing.OP_FREE_ACCESS')} – ${(account === null || account === void 0 ? void 0 : account.name) || (account === null || account === void 0 ? void 0 : account.email) || (account === null || account === void 0 ? void 0 : account.user_id)}` } } }), label: t('Billing.OP_FREE_ACCESS') }) }), _jsxs(Dialog, { open: open, onClose: close, "aria-labelledby": titleId, children: [_jsx(DialogTitle, { id: titleId, children: active ? t('Billing.OP_REVOKE_TITLE') : t('Billing.OP_FREE_ACCESS') }), _jsxs(DialogContent, { children: [active ? _jsx("p", { children: t('Billing.OP_REVOKE_BODY') }) : _jsx(TextField, { label: t('Billing.OP_REASON'), multiline: true, required: true, fullWidth: true, disabled: busy, value: reason, onChange: (event) => setReason(event.target.value), inputProps: { maxLength: 1001 } }), error && _jsx(Alert, { severity: "error", sx: FREE_ACCESS_SWITCH_ALERT_SX, children: t(error) })] }), _jsxs(DialogActions, { children: [_jsx(Button, { disabled: busy, onClick: close, children: t('Billing.OP_CANCEL') }), _jsx(Button, { disabled: busy || (!active && !validReason), onClick: submit, children: active ? t('Billing.OP_REVOKE') : t('Billing.OP_GRANT') })] })] })] });
|
|
40
|
+
}
|
|
41
|
+
export default FreeAccessSwitch;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Alert, Button, Card, CardContent, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography } from '@mui/material';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import StatTile from '../components/StatTile';
|
|
6
|
+
import { defaultBillingApi } from './api';
|
|
7
|
+
import { formatTokens, formatUsdMicros } from './format';
|
|
8
|
+
export const OPERATOR_OVERVIEW_ALERT_SX = { width: '100%' };
|
|
9
|
+
const hiddenStatus = (error) => { var _a; return [401, 403, 404].includes((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status); };
|
|
10
|
+
const currentUtcMonth = () => new Date().toISOString().slice(0, 7);
|
|
11
|
+
export function OperatorOverview({ api = defaultBillingApi, sx }) {
|
|
12
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
13
|
+
const { t, i18n } = useTranslation();
|
|
14
|
+
const [month, setMonth] = useState(currentUtcMonth);
|
|
15
|
+
const [state, setState] = useState('loading');
|
|
16
|
+
const [data, setData] = useState(null);
|
|
17
|
+
const [attempt, setAttempt] = useState(0);
|
|
18
|
+
const generation = useRef(0);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const current = ++generation.current;
|
|
21
|
+
setState('loading');
|
|
22
|
+
Promise.resolve().then(() => api.getOperatorOverview({ month })).then((result) => {
|
|
23
|
+
if (generation.current !== current)
|
|
24
|
+
return;
|
|
25
|
+
setData(result);
|
|
26
|
+
setState('ready');
|
|
27
|
+
}).catch((error) => {
|
|
28
|
+
if (generation.current !== current)
|
|
29
|
+
return;
|
|
30
|
+
setData(null);
|
|
31
|
+
setState(hiddenStatus(error) ? 'hidden' : 'error');
|
|
32
|
+
});
|
|
33
|
+
return () => { generation.current += 1; };
|
|
34
|
+
}, [api, month, attempt]);
|
|
35
|
+
if (state === 'hidden')
|
|
36
|
+
return null;
|
|
37
|
+
if (state === 'error')
|
|
38
|
+
return _jsxs(Stack, { sx: sx, spacing: 1, children: [_jsx(Alert, { severity: "error", sx: OPERATOR_OVERVIEW_ALERT_SX, children: t('Billing.OP_LOAD_ERROR') }), _jsx(Button, { onClick: () => setAttempt((value) => value + 1), children: t('Billing.RETRY') })] });
|
|
39
|
+
if (state === 'loading')
|
|
40
|
+
return _jsxs(Stack, { sx: sx, spacing: 2, children: [_jsx(Typography, { variant: "h5", children: t('Billing.OP_OVERVIEW_TITLE') }), _jsx(TextField, { type: "month", label: t('Billing.OP_MONTH'), value: month, onChange: (event) => setMonth(event.target.value) })] });
|
|
41
|
+
if (state !== 'ready')
|
|
42
|
+
return null;
|
|
43
|
+
const accounts = (data === null || data === void 0 ? void 0 : data.accounts) || { active: 0, by_source: { subscription: {}, team_seat: 0, free_access: 0 } };
|
|
44
|
+
const subscriptions = (data === null || data === void 0 ? void 0 : data.subscriptions) || { new: 0, ended: 0 };
|
|
45
|
+
const tokens = (data === null || data === void 0 ? void 0 : data.tokens) || { granted: 0, used: 0, expired: 0 };
|
|
46
|
+
const models = Array.isArray((_a = data === null || data === void 0 ? void 0 : data.ai_cost) === null || _a === void 0 ? void 0 : _a.by_model) ? data.ai_cost.by_model : [];
|
|
47
|
+
const n = (value) => formatTokens(Number.isInteger(value) ? value : 0, i18n.language);
|
|
48
|
+
const planRows = Object.entries(((_b = accounts.by_source) === null || _b === void 0 ? void 0 : _b.subscription) || {}).map(([plan, value]) => t('Billing.OP_PLAN', { plan }) + ': ' + n(value));
|
|
49
|
+
const activeLines = [...planRows, `${t('Billing.OP_TEAM_SEAT')}: ${n((_c = accounts.by_source) === null || _c === void 0 ? void 0 : _c.team_seat)}`, `${t('Billing.OP_FREE_ACCESS')}: ${n((_d = accounts.by_source) === null || _d === void 0 ? void 0 : _d.free_access)}`];
|
|
50
|
+
const packages = ((_e = data === null || data === void 0 ? void 0 : data.team) === null || _e === void 0 ? void 0 : _e.packages_sold) || 0;
|
|
51
|
+
const tokenPackages = ((_f = data === null || data === void 0 ? void 0 : data.token_packages) === null || _f === void 0 ? void 0 : _f.sold) || 0;
|
|
52
|
+
const packageTokens = ((_g = data === null || data === void 0 ? void 0 : data.token_packages) === null || _g === void 0 ? void 0 : _g.tokens) || 0;
|
|
53
|
+
const tiles = [
|
|
54
|
+
[t('Billing.OP_ACTIVE_ACCOUNTS'), n(accounts.active), activeLines],
|
|
55
|
+
[t('Billing.OP_NEW_SUBSCRIPTIONS'), n(subscriptions.new)],
|
|
56
|
+
[t('Billing.OP_ENDED_SUBSCRIPTIONS'), n(subscriptions.ended)],
|
|
57
|
+
[t('Billing.OP_PACKAGES_SOLD'), n(packages + tokenPackages), undefined, `${n(packageTokens)} ${t('Billing.OP_TOKENS_GRANTED')}`],
|
|
58
|
+
[t('Billing.OP_TOKENS_GRANTED'), n(tokens.granted)],
|
|
59
|
+
[t('Billing.OP_TOKENS_USED'), n(tokens.used)],
|
|
60
|
+
[t('Billing.OP_TOKENS_EXPIRED'), n(tokens.expired)],
|
|
61
|
+
[t('Billing.OP_AI_COST'), formatUsdMicros(((_h = data === null || data === void 0 ? void 0 : data.ai_cost) === null || _h === void 0 ? void 0 : _h.usd_micros) || 0, i18n.language)],
|
|
62
|
+
[t('Billing.OP_COST_PER_TOKEN'), ((_j = data === null || data === void 0 ? void 0 : data.ai_cost) === null || _j === void 0 ? void 0 : _j.usd_micros_per_used_token) == null ? '–' : formatUsdMicros(data.ai_cost.usd_micros_per_used_token, i18n.language, { fractionDigits: 6 })],
|
|
63
|
+
];
|
|
64
|
+
return _jsxs(Stack, { sx: sx, spacing: 2, children: [_jsx(Typography, { variant: "h5", children: t('Billing.OP_OVERVIEW_TITLE') }), _jsx(TextField, { type: "month", label: t('Billing.OP_MONTH'), value: month, onChange: (event) => setMonth(event.target.value) }), _jsx(Stack, { direction: "row", useFlexGap: true, flexWrap: "wrap", gap: 1, children: tiles.map(([label, value, lines, caption]) => _jsx(StatTile, { label: label, value: value, caption: caption, minWidth: 170, children: lines === null || lines === void 0 ? void 0 : lines.map((line) => _jsx(Typography, { variant: "body2", children: line }, line)) }, label)) }), _jsx(Card, { children: _jsxs(CardContent, { children: [_jsx(Typography, { variant: "h6", children: t('Billing.OP_AI_COST') }), models.length ? _jsx(TableContainer, { children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Billing.OP_PROVIDER') }), _jsx(TableCell, { children: t('Billing.OP_MODEL') }), _jsx(TableCell, { children: t('Billing.OP_CALLS') }), _jsx(TableCell, { children: t('Billing.OP_COST_USD') })] }) }), _jsx(TableBody, { children: models.map((model, index) => _jsxs(TableRow, { children: [_jsx(TableCell, { children: model.provider }), _jsx(TableCell, { children: model.model }), _jsx(TableCell, { children: n(model.calls) }), _jsx(TableCell, { children: formatUsdMicros(model.usd_micros, i18n.language) })] }, `${model.provider}-${model.model}-${index}`)) })] }) }) : _jsx(Typography, { color: "text.secondary", children: t('Billing.OP_NO_AI_COST') })] }) })] });
|
|
65
|
+
}
|
|
66
|
+
export default OperatorOverview;
|
|
@@ -92,7 +92,11 @@ export function SubscriptionSection({ api = defaultBillingApi, redirect = (url)
|
|
|
92
92
|
var _a, _b, _c, _d;
|
|
93
93
|
(_b = (_a = renewalPickerRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView) === null || _b === void 0 ? void 0 : _b.call(_a, { behavior: 'smooth', block: 'start' });
|
|
94
94
|
(_d = (_c = renewalPickerRef.current) === null || _c === void 0 ? void 0 : _c.querySelector('input[type="radio"]')) === null || _d === void 0 ? void 0 : _d.focus();
|
|
95
|
-
}) }), _jsx("div", { ref: renewalPickerRef, children: _jsx(PlanPicker, { plans: me.plans || [], api: api, redirect: redirect, allowedHosts: allowedHosts, describePlan: describePlan }) })] }) : _jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h5", children: t('Billing.HEADING') }), _jsx(Typography, { children: sourceLine }), access.source !== 'free' && dateLine && _jsx(Typography, { children: dateLine }), access.source === 'subscription' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.openPortal(), 'Billing.ERROR_PORTAL'), children: t('Billing.MANAGE') }), _jsx(Typography, { children: t('Billing.MANAGE_DETAILS') })] }), access.source === 'team' && seatError && !seats && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t('Billing.ERROR_LOAD') }), access.source === 'team' && seats && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.TEAM_SEATS') }), seats.map((seat) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: String(seat.user_id) }), _jsx(Button, { disabled: busy, onClick: () => updateSeat(() => api.removeSeat({ userId: seat.user_id })), children: t('Billing.SEAT_REMOVE') })] }, seat.user_id)), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('Billing.SEAT_USER'), value: seatInput, onChange: (event) => setSeatInput(event.target.value) }), _jsx(Button, { disabled: busy || !seatInput.trim(), onClick: () => updateSeat(() => seatInput.includes('@') ? api.addSeat({ email: seatInput.trim() }) : api.addSeat({ userId: seatInput.trim() })), children: t('Billing.SEATS_ADD') })] }), seatError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: typeof seatError === 'string' ? t(seatError) : t('Billing.ERROR_LOAD') })] }), actionError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t(actionError) })
|
|
95
|
+
}) }), _jsx("div", { ref: renewalPickerRef, children: _jsx(PlanPicker, { plans: me.plans || [], api: api, redirect: redirect, allowedHosts: allowedHosts, describePlan: describePlan }) })] }) : _jsxs(_Fragment, { children: [_jsx(Typography, { variant: "h5", children: t('Billing.HEADING') }), _jsx(Typography, { children: sourceLine }), access.source !== 'free' && dateLine && _jsx(Typography, { children: dateLine }), me.subscription && typeof me.subscription === 'object' && typeof me.subscription.plan_key === 'string' && typeof me.subscription.renews === 'boolean' && formatDate(me.subscription.current_period_end, i18n.language) && _jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { children: t('Billing.OP_PLAN', { plan: me.subscription.plan_name || me.subscription.plan_key }) }), formatDate(me.subscription.current_period_end, i18n.language) && _jsx(Typography, { children: formatDate(me.subscription.current_period_start, i18n.language) ? `${formatDate(me.subscription.current_period_start, i18n.language)} – ${formatDate(me.subscription.current_period_end, i18n.language)}` : t('Billing.OP_UNTIL', { date: formatDate(me.subscription.current_period_end, i18n.language) }) }), typeof me.subscription.renews === 'boolean' && _jsx(Typography, { children: t(me.subscription.renews ? 'Billing.OP_RENEWS' : 'Billing.OP_ENDS') })] }), access.source === 'subscription' && _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.openPortal(), 'Billing.ERROR_PORTAL'), children: t('Billing.MANAGE') }), _jsx(Typography, { children: t('Billing.MANAGE_DETAILS') })] }), access.source === 'team' && seatError && !seats && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t('Billing.ERROR_LOAD') }), me.team && Number.isInteger(me.team.seats_used) && Number.isInteger(me.team.seats_total) && _jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { children: t('Billing.OP_SEATS_USED', { n: formatTokens(me.team.seats_used, i18n.language), m: formatTokens(me.team.seats_total, i18n.language) }) }), (Array.isArray(me.team.seat_holders) ? me.team.seat_holders : []).map((holder) => _jsxs(Stack, { children: [_jsx(Typography, { children: holder.name || holder.email }), holder.name && _jsx(Typography, { color: "text.secondary", children: holder.email })] }, holder.user_id))] }), access.source === 'team' && seats && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { children: t('Billing.TEAM_SEATS') }), seats.map((seat) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [(() => { var _a; const holder = (((_a = me.team) === null || _a === void 0 ? void 0 : _a.seat_holders) || []).find((entry) => String(entry.user_id) === String(seat.user_id)); return _jsxs(Stack, { children: [_jsx(Typography, { children: (holder === null || holder === void 0 ? void 0 : holder.name) || (holder === null || holder === void 0 ? void 0 : holder.email) || String(seat.user_id) }), (holder === null || holder === void 0 ? void 0 : holder.name) && _jsx(Typography, { color: "text.secondary", children: holder.email })] }); })(), _jsx(Button, { disabled: busy, onClick: () => updateSeat(() => api.removeSeat({ userId: seat.user_id })), children: t('Billing.SEAT_REMOVE') })] }, seat.user_id)), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(TextField, { size: "small", label: t('Billing.SEAT_USER'), value: seatInput, onChange: (event) => setSeatInput(event.target.value) }), _jsx(Button, { disabled: busy || !seatInput.trim(), onClick: () => updateSeat(() => seatInput.includes('@') ? api.addSeat({ email: seatInput.trim() }) : api.addSeat({ userId: seatInput.trim() })), children: t('Billing.SEATS_ADD') })] }), seatError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: typeof seatError === 'string' ? t(seatError) : t('Billing.ERROR_LOAD') })] }), actionError && _jsx(Alert, { severity: "error", sx: SUBSCRIPTION_SECTION_ALERT_SX, children: t(actionError) }), me.seat && typeof me.seat === 'object' && formatDate(me.seat.ends_at, i18n.language) && _jsx(Typography, { children: t('Billing.OP_SEAT_UNTIL', { date: formatDate(me.seat.ends_at, i18n.language) }) })] }), _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { children: t('Billing.BALANCE_LABEL') }), _jsx(TokenBalanceBadge, { balance: tokens.balance })] }), _jsx(Typography, { children: t('Billing.BALANCE', { tokens: formatTokens(tokens.balance, i18n.language) }) }), Array.isArray(tokens.grants) && tokens.grants.length > 0 && _jsx(Stack, { spacing: 0.5, children: tokens.grants.map((grant, index) => {
|
|
96
|
+
if (!['plan', 'package'].includes(grant === null || grant === void 0 ? void 0 : grant.source) || !Number.isInteger(grant.amount) || !Number.isInteger(grant.remaining))
|
|
97
|
+
return null;
|
|
98
|
+
return _jsxs(Stack, { spacing: 0.25, children: [_jsxs(Typography, { children: [t(grant.source === 'plan' ? 'Billing.OP_GRANT_PLAN' : 'Billing.OP_GRANT_PACKAGE'), ": ", formatTokens(grant.amount, i18n.language)] }), _jsxs(Typography, { children: [formatTokens(grant.remaining, i18n.language), " ", t('Billing.OP_REMAINING')] }), formatDate(grant.expires_at, i18n.language) && _jsx(Typography, { children: t('Billing.OP_VALID_UNTIL', { date: formatDate(grant.expires_at, i18n.language) }) })] }, `${grant.source}-${grant.expires_at || 'never'}-${index}`);
|
|
99
|
+
}) }), packagePlans.length > 0 && _jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "h6", children: t('Billing.BUY_PACKAGE') }), packagePlans.map((plan) => _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsxs(Typography, { children: [formatPrice(plan.amount_cents, i18n.language), " \u00B7 ", t('Billing.PACKAGE_TOKENS', { tokens: formatTokens(plan.tokens, i18n.language) })] }), _jsx(Button, { disabled: busy, onClick: () => safeRedirect(() => api.startCheckout(plan.key), 'Billing.ERROR_CHECKOUT'), children: t('Billing.PACKAGE_BUY') })] }, plan.key))] }), withdrawal && _jsx(WithdrawalSection, { api: api, plans: me.plans, sessionWithdrawals: withdrawnPurchases, onWithdrawn: (result, purchase) => {
|
|
96
100
|
if ((purchase === null || purchase === void 0 ? void 0 : purchase.id) != null)
|
|
97
101
|
setWithdrawnPurchases((current) => (Object.assign(Object.assign({}, current), { [purchase.id]: { result, purchase } })));
|
|
98
102
|
reload();
|
package/dist/billing/api.js
CHANGED
|
@@ -13,6 +13,19 @@ export function createBillingApi(overrides = {}, { apiBase = '/api/billing/' } =
|
|
|
13
13
|
listWithdrawals: () => data(apiClient.get(urls.withdrawals)),
|
|
14
14
|
previewWithdrawal: (id) => data(apiClient.get(urls.withdrawalPreview(id))),
|
|
15
15
|
confirmWithdrawal: (id) => data(apiClient.post(urls.withdrawalConfirm(id), { confirm: true })),
|
|
16
|
+
getOperatorOverview: ({ month } = {}) => data(month === undefined ? apiClient.get(urls.operatorOverview) : apiClient.get(urls.operatorOverview, { params: { month } })),
|
|
17
|
+
listOperatorAccounts: ({ q, page, pageSize } = {}) => {
|
|
18
|
+
const params = {};
|
|
19
|
+
if (q !== undefined)
|
|
20
|
+
params.q = q;
|
|
21
|
+
if (page !== undefined)
|
|
22
|
+
params.page = page;
|
|
23
|
+
if (pageSize !== undefined)
|
|
24
|
+
params.page_size = pageSize;
|
|
25
|
+
return data(apiClient.get(urls.operatorAccounts, { params }));
|
|
26
|
+
},
|
|
27
|
+
grantFreeAccess: (userId, reason) => data(apiClient.post(urls.operatorFreeAccess(userId), { reason })),
|
|
28
|
+
revokeFreeAccess: (userId) => data(apiClient.delete(urls.operatorFreeAccess(userId))),
|
|
16
29
|
};
|
|
17
30
|
return Object.assign(Object.assign({}, defaults), overrides);
|
|
18
31
|
}
|
package/dist/billing/format.js
CHANGED
|
@@ -22,6 +22,14 @@ export function formatTokens(count, language) {
|
|
|
22
22
|
return '';
|
|
23
23
|
return new Intl.NumberFormat(intlLocale(language), { maximumFractionDigits: 0 }).format(count);
|
|
24
24
|
}
|
|
25
|
+
export function formatUsdMicros(micros, language, { fractionDigits = 2 } = {}) {
|
|
26
|
+
if (!Number.isFinite(micros))
|
|
27
|
+
return '';
|
|
28
|
+
return new Intl.NumberFormat(intlLocale(language), {
|
|
29
|
+
style: 'currency', currency: 'USD', minimumFractionDigits: fractionDigits === 2 ? 2 : 0,
|
|
30
|
+
maximumFractionDigits: fractionDigits,
|
|
31
|
+
}).format(micros / 1000000);
|
|
32
|
+
}
|
|
25
33
|
export function formatDate(iso, language) {
|
|
26
34
|
if (typeof iso !== 'string' || !iso)
|
|
27
35
|
return '';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { defaultBillingApi } from './api';
|
|
4
|
+
import { BillingAccessCell, BillingAccessError, accountAccessText } from './BillingAccessCell';
|
|
5
|
+
import { useOperatorAccounts } from './useOperatorAccounts';
|
|
6
|
+
export function useBillingUsersColumn({ api = defaultBillingApi, enabled = true } = {}) {
|
|
7
|
+
const { t, i18n } = useTranslation();
|
|
8
|
+
const billing = useOperatorAccounts(api, { enabled });
|
|
9
|
+
const extraColumns = useMemo(() => billing.status === 'error' ? [{
|
|
10
|
+
key: 'billing-access', label: t('Billing.OP_ACCESS'), minWidth: 260,
|
|
11
|
+
renderCell: () => React.createElement(BillingAccessError, { onRetry: billing.reload }),
|
|
12
|
+
getSortValue: () => '', getSearchValue: () => '',
|
|
13
|
+
}] : billing.status !== 'ready' ? [] : [{
|
|
14
|
+
key: 'billing-access', label: t('Billing.OP_ACCESS'), minWidth: 260,
|
|
15
|
+
renderCell: ({ user }) => React.createElement(BillingAccessCell, { account: billing.accounts.get(user === null || user === void 0 ? void 0 : user.id), api, onChanged: (answer) => billing.setFreeAccess(user.id, answer) }),
|
|
16
|
+
getSortValue: ({ user }) => accountAccessText(billing.accounts.get(user === null || user === void 0 ? void 0 : user.id), t, i18n.language),
|
|
17
|
+
getSearchValue: ({ user }) => accountAccessText(billing.accounts.get(user === null || user === void 0 ? void 0 : user.id), t, i18n.language),
|
|
18
|
+
}], [billing.status, billing.accounts, billing.setFreeAccess, billing.reload, api, t, i18n.language]);
|
|
19
|
+
return { status: billing.status, extraColumns, reload: billing.reload };
|
|
20
|
+
}
|
|
21
|
+
export default useBillingUsersColumn;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
const hiddenStatus = (error) => { var _a; return [401, 403, 404].includes((_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.status); };
|
|
3
|
+
export function useOperatorAccounts(api, { enabled = true } = {}) {
|
|
4
|
+
const [status, setStatus] = useState(enabled ? 'loading' : 'idle');
|
|
5
|
+
const [accounts, setAccounts] = useState(() => new Map());
|
|
6
|
+
const [reloadCount, setReloadCount] = useState(0);
|
|
7
|
+
const generation = useRef(0);
|
|
8
|
+
const reload = useCallback(() => setReloadCount((count) => count + 1), []);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const current = ++generation.current;
|
|
11
|
+
if (!enabled) {
|
|
12
|
+
setStatus('idle');
|
|
13
|
+
setAccounts(new Map());
|
|
14
|
+
return () => { generation.current += 1; };
|
|
15
|
+
}
|
|
16
|
+
setStatus('loading');
|
|
17
|
+
setAccounts(new Map());
|
|
18
|
+
const load = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const merged = new Map();
|
|
21
|
+
let page = 1;
|
|
22
|
+
let count = Infinity;
|
|
23
|
+
while (merged.size < count && page <= 50) {
|
|
24
|
+
const result = await api.listOperatorAccounts({ page, pageSize: 100 });
|
|
25
|
+
if (generation.current !== current)
|
|
26
|
+
return;
|
|
27
|
+
if (!Array.isArray(result === null || result === void 0 ? void 0 : result.results) || !Number.isInteger(result === null || result === void 0 ? void 0 : result.count) || result.count < 0)
|
|
28
|
+
throw new Error('invalid_operator_accounts');
|
|
29
|
+
count = result.count;
|
|
30
|
+
for (const account of result.results)
|
|
31
|
+
if (Number.isInteger(account === null || account === void 0 ? void 0 : account.user_id))
|
|
32
|
+
merged.set(account.user_id, account);
|
|
33
|
+
setAccounts(new Map(merged));
|
|
34
|
+
page += 1;
|
|
35
|
+
if (result.results.length === 0)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
if (generation.current === current)
|
|
39
|
+
setStatus('ready');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (generation.current !== current)
|
|
43
|
+
return;
|
|
44
|
+
setAccounts(new Map());
|
|
45
|
+
setStatus(hiddenStatus(error) ? 'hidden' : 'error');
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
load();
|
|
49
|
+
return () => { generation.current += 1; };
|
|
50
|
+
}, [api, enabled, reloadCount]);
|
|
51
|
+
const setFreeAccess = useCallback((userId, answer) => {
|
|
52
|
+
setAccounts((current) => {
|
|
53
|
+
const previous = current.get(userId);
|
|
54
|
+
if (!previous || !answer)
|
|
55
|
+
return current;
|
|
56
|
+
const updated = new Map(current);
|
|
57
|
+
updated.set(userId, Object.assign(Object.assign({}, previous), { free_access: answer.free_access, access: answer.access }));
|
|
58
|
+
return updated;
|
|
59
|
+
});
|
|
60
|
+
}, []);
|
|
61
|
+
return { status, accounts, reload, setFreeAccess };
|
|
62
|
+
}
|
|
63
|
+
export default useOperatorAccounts;
|
package/dist/billing/wire.js
CHANGED
|
@@ -2,6 +2,7 @@ export const BILLING_STATUSES = { ACTIVE: 'active', READ_ONLY: 'read_only' };
|
|
|
2
2
|
export const BILLING_SOURCES = { SUBSCRIPTION: 'subscription', TEAM: 'team', FREE: 'free', NONE: null };
|
|
3
3
|
export const BILLING_KINDS = { SUBSCRIPTION: 'subscription', TEAM_PACKAGE: 'team_package', TOKEN_PACKAGE: 'token_package' };
|
|
4
4
|
export const BILLING_ERROR_CODES = ['unknown_plan', 'checkout_unavailable', 'no_customer', 'portal_unavailable', 'no_active_team', 'invalid_user', 'seat_limit'];
|
|
5
|
+
export const BILLING_OPERATOR_ERROR_CODES = ['authentication_required', 'forbidden', 'invalid_month', 'invalid_page', 'invalid_query', 'reason_required', 'user_not_found'];
|
|
5
6
|
export const billingApiUrls = (base = '/api/billing/') => {
|
|
6
7
|
const root = base.replace(/\/$/, '') + '/';
|
|
7
8
|
return {
|
|
@@ -12,5 +13,8 @@ export const billingApiUrls = (base = '/api/billing/') => {
|
|
|
12
13
|
withdrawals: root + 'withdrawal/',
|
|
13
14
|
withdrawalPreview: (id) => root + `withdrawal/${encodeURIComponent(id)}/preview/`,
|
|
14
15
|
withdrawalConfirm: (id) => root + `withdrawal/${encodeURIComponent(id)}/`,
|
|
16
|
+
operatorOverview: root + 'operator/overview/',
|
|
17
|
+
operatorAccounts: root + 'operator/accounts/',
|
|
18
|
+
operatorFreeAccess: (id) => root + `operator/accounts/${encodeURIComponent(id)}/free-access/`,
|
|
15
19
|
};
|
|
16
20
|
};
|
|
@@ -61,4 +61,43 @@ export const billingTranslations = {
|
|
|
61
61
|
'Billing.WITHDRAWAL_TOKEN_SHARE': { de: 'Tokenanteil: {{amount}}', de_informal: 'Tokenanteil: {{amount}}', en: 'Token share: {{amount}}', fr: 'Part des jetons : {{amount}}', sw: 'Sehemu ya tokeni: {{amount}}' },
|
|
62
62
|
'Billing.WITHDRAWAL_USED': { de: 'Genutzt: {{amount}}', de_informal: 'Genutzt: {{amount}}', en: 'Used: {{amount}}', fr: 'Utilisé : {{amount}}', sw: 'Imetumika: {{amount}}' },
|
|
63
63
|
'Billing.WITHDRAWAL_CLOSE': { de: 'Schließen', de_informal: 'Schließen', en: 'Close', fr: 'Fermer', sw: 'Funga' },
|
|
64
|
+
'Billing.OP_OVERVIEW_TITLE': { de: 'Übersicht Abos', en: 'Subscription overview', fr: 'Aperçu des abonnements', sw: 'Muhtasari wa usajili' },
|
|
65
|
+
'Billing.OP_ACTIVE_ACCOUNTS': { de: 'Aktive Konten', en: 'Active accounts', fr: 'Comptes actifs', sw: 'Akaunti zinazotumika' },
|
|
66
|
+
'Billing.OP_PLAN': { de: 'Abo {{plan}}', en: 'Subscription {{plan}}', fr: 'Abonnement {{plan}}', sw: 'Usajili {{plan}}' },
|
|
67
|
+
'Billing.OP_TEAM_SEAT': { de: 'Team-Platz', en: 'Team seat', fr: 'Place d’équipe', sw: 'Nafasi ya timu' },
|
|
68
|
+
'Billing.OP_FREE_ACCESS': { de: 'Ohne Abo freigeschaltet', en: 'Access enabled without a subscription', fr: 'Accès activé sans abonnement', sw: 'Ufikiaji umewezeshwa bila usajili' },
|
|
69
|
+
'Billing.OP_READ_ONLY': { de: 'Schreibgeschützt', en: 'Read-only', fr: 'Lecture seule', sw: 'Kusoma pekee' },
|
|
70
|
+
'Billing.OP_NEW_SUBSCRIPTIONS': { de: 'Neue Abos', en: 'New subscriptions', fr: 'Nouveaux abonnements', sw: 'Usajili mpya' },
|
|
71
|
+
'Billing.OP_ENDED_SUBSCRIPTIONS': { de: 'Beendete Abos', en: 'Ended subscriptions', fr: 'Abonnements terminés', sw: 'Usajili uliomalizika' },
|
|
72
|
+
'Billing.OP_PACKAGES_SOLD': { de: 'Verkaufte Pakete', en: 'Packages sold', fr: 'Forfaits vendus', sw: 'Vifurushi vilivyouzwa' },
|
|
73
|
+
'Billing.OP_TOKENS_GRANTED': { de: 'Token gewährt', en: 'Tokens granted', fr: 'Jetons accordés', sw: 'Tokeni zilizotolewa' },
|
|
74
|
+
'Billing.OP_TOKENS_USED': { de: 'Token verbraucht', en: 'Tokens used', fr: 'Jetons utilisés', sw: 'Tokeni zilizotumika' },
|
|
75
|
+
'Billing.OP_TOKENS_EXPIRED': { de: 'Token verfallen', en: 'Tokens expired', fr: 'Jetons expirés', sw: 'Tokeni zilizoisha muda' },
|
|
76
|
+
'Billing.OP_AI_COST': { de: 'KI-Kosten', en: 'AI costs', fr: 'Coûts de l’IA', sw: 'Gharama za AI' },
|
|
77
|
+
'Billing.OP_COST_PER_TOKEN': { de: 'Kosten pro Token', en: 'Cost per token', fr: 'Coût par jeton', sw: 'Gharama kwa tokeni' },
|
|
78
|
+
'Billing.OP_ACCESS': { de: 'Zugang', en: 'Access', fr: 'Accès', sw: 'Ufikiaji' },
|
|
79
|
+
'Billing.OP_REASON': { de: 'Grund', en: 'Reason', fr: 'Motif', sw: 'Sababu' },
|
|
80
|
+
'Billing.OP_REVOKE_TITLE': { de: 'Freischaltung aufheben?', en: 'Revoke access?', fr: 'Retirer l’accès ?', sw: 'Uondoe ruhusa ya ufikiaji?' },
|
|
81
|
+
'Billing.OP_REVOKE_BODY': { de: 'Die Daten bleiben erhalten; ohne anderen Zugang sind sie danach schreibgeschützt.', en: 'The data will remain; without another access source, it will become read-only.', fr: 'Les données sont conservées ; sans autre accès, elles seront ensuite en lecture seule.', sw: 'Data zitabaki; bila chanzo kingine cha ufikiaji, zitasomeka pekee.' },
|
|
82
|
+
'Billing.OP_UNTIL': { de: 'bis {{date}}', en: 'until {{date}}', fr: 'jusqu’au {{date}}', sw: 'hadi {{date}}' },
|
|
83
|
+
'Billing.OP_RENEWS': { de: 'verlängert sich', en: 'renews', fr: 'se renouvelle', sw: 'inasasishwa' },
|
|
84
|
+
'Billing.OP_ENDS': { de: 'endet', en: 'ends', fr: 'se termine', sw: 'inaisha' },
|
|
85
|
+
'Billing.OP_GRANT_PLAN': { de: 'Kontingent', en: 'Allowance', fr: 'Allocation', sw: 'Kiwango kilichotengwa' },
|
|
86
|
+
'Billing.OP_GRANT_PACKAGE': { de: 'Paket', en: 'Package', fr: 'Forfait', sw: 'Kifurushi' },
|
|
87
|
+
'Billing.OP_REMAINING': { de: 'verbleibend', en: 'remaining', fr: 'restants', sw: 'zilizobaki' },
|
|
88
|
+
'Billing.OP_VALID_UNTIL': { de: 'gültig bis {{date}}', en: 'valid until {{date}}', fr: 'valable jusqu’au {{date}}', sw: 'halali hadi {{date}}' },
|
|
89
|
+
'Billing.OP_SEATS_USED': { de: '{{n}} von {{m}} Plätzen belegt', en: '{{n}} of {{m}} seats occupied', fr: '{{n}} places occupées sur {{m}}', sw: 'Nafasi {{n}} kati ya {{m}} zimejazwa' },
|
|
90
|
+
'Billing.OP_MONTH': { de: 'Monat', en: 'Month', fr: 'Mois', sw: 'Mwezi' },
|
|
91
|
+
'Billing.OP_PROVIDER': { de: 'Anbieter', en: 'Provider', fr: 'Fournisseur', sw: 'Mtoa huduma' },
|
|
92
|
+
'Billing.OP_MODEL': { de: 'Modell', en: 'Model', fr: 'Modèle', sw: 'Muundo' },
|
|
93
|
+
'Billing.OP_CALLS': { de: 'Aufrufe', en: 'Calls', fr: 'Appels', sw: 'Miito' },
|
|
94
|
+
'Billing.OP_COST_USD': { de: 'Kosten (USD)', en: 'Cost (USD)', fr: 'Coût (USD)', sw: 'Gharama (USD)' },
|
|
95
|
+
'Billing.OP_CANCEL': { de: 'Abbrechen', en: 'Cancel', fr: 'Annuler', sw: 'Ghairi' },
|
|
96
|
+
'Billing.OP_GRANT': { de: 'Freischalten', en: 'Grant access', fr: 'Autoriser l’accès', sw: 'Ruhusu ufikiaji' },
|
|
97
|
+
'Billing.OP_REVOKE': { de: 'Aufheben', en: 'Revoke', fr: 'Retirer', sw: 'Ondoa' },
|
|
98
|
+
'Billing.OP_SEAT_UNTIL': { de: 'Team-Platz bis {{date}}', en: 'Team seat until {{date}}', fr: 'Place d’équipe jusqu’au {{date}}', sw: 'Nafasi ya timu hadi {{date}}' },
|
|
99
|
+
'Billing.OP_LOAD_ERROR': { de: 'Die Übersicht konnte nicht geladen werden.', en: 'The overview could not be loaded.', fr: 'Impossible de charger l’aperçu.', sw: 'Muhtasari haukuweza kupakiwa.' },
|
|
100
|
+
'Billing.OP_SAVE_ERROR': { de: 'Die Änderung konnte nicht gespeichert werden.', en: 'The change could not be saved.', fr: 'Impossible d’enregistrer la modification.', sw: 'Mabadiliko hayakuweza kuhifadhiwa.' },
|
|
101
|
+
'Billing.OP_FORBIDDEN': { de: 'Dafür fehlt die Berechtigung.', en: 'You do not have permission for this action.', fr: 'Vous n’avez pas l’autorisation pour cette action.', sw: 'Huna ruhusa ya kufanya kitendo hiki.' },
|
|
102
|
+
'Billing.OP_NO_AI_COST': { de: 'In diesem Monat gab es keine KI-Aufrufe.', en: 'There were no AI calls this month.', fr: 'Aucun appel à l’IA ce mois-ci.', sw: 'Hakukuwa na miito ya AI mwezi huu.' },
|
|
64
103
|
};
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,11 @@ export { AccessEndedBanner } from './billing/AccessEndedBanner';
|
|
|
49
49
|
export { PlanPicker } from './billing/PlanPicker';
|
|
50
50
|
export { SubscriptionSection } from './billing/SubscriptionSection';
|
|
51
51
|
export { WithdrawalSection } from './billing/WithdrawalSection';
|
|
52
|
+
export { OperatorOverview } from './billing/OperatorOverview';
|
|
53
|
+
export { FreeAccessSwitch } from './billing/FreeAccessSwitch';
|
|
54
|
+
export { BillingAccessCell } from './billing/BillingAccessCell';
|
|
55
|
+
export { useBillingUsersColumn } from './billing/useBillingUsersColumn';
|
|
56
|
+
export { useOperatorAccounts } from './billing/useOperatorAccounts';
|
|
52
57
|
export { WithdrawalDialog } from './billing/WithdrawalDialog';
|
|
53
58
|
export { useBilling } from './billing/useBilling';
|
|
54
59
|
export { createBillingApi, defaultBillingApi } from './billing/api';
|
|
@@ -25,7 +25,9 @@ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
|
|
|
25
25
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
26
26
|
import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
|
|
27
27
|
import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
|
|
28
|
-
|
|
28
|
+
import { defaultBillingApi } from '../billing/api';
|
|
29
|
+
import { useBillingUsersColumn } from '../billing/useBillingUsersColumn';
|
|
30
|
+
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowNewColumn = true, userListShowSuccessfulLoginColumn = true, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], showProfileStatements = true, billingUsers = false, }) {
|
|
29
31
|
var _a;
|
|
30
32
|
const { t, i18n } = useTranslation();
|
|
31
33
|
const { user, login, loading } = useContext(AuthContext);
|
|
@@ -34,6 +36,8 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
34
36
|
const [authPolicyError, setAuthPolicyError] = useState('');
|
|
35
37
|
const theme = useTheme();
|
|
36
38
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
|
39
|
+
const billingApi = billingUsers && typeof billingUsers === 'object' ? billingUsers : defaultBillingApi;
|
|
40
|
+
const billing = useBillingUsersColumn({ api: billingApi, enabled: Boolean(billingUsers) });
|
|
37
41
|
// 1. URL State Management
|
|
38
42
|
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
39
43
|
const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
|
|
@@ -173,5 +177,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
173
177
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
174
178
|
? null
|
|
175
179
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
176
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx("title", { children: `${t('Account.PAGE_TITLE', 'Account')} – ${user.email}` }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true, showStatements: showProfileStatements }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
180
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx("title", { children: `${t('Account.PAGE_TITLE', 'Account')} – ${user.email}` }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true, showStatements: showProfileStatements }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: [...userListExtraColumns, ...billing.extraColumns], extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
177
181
|
}
|
|
@@ -39,6 +39,8 @@ import { PLAN_PICKER_ALERT_SX, PLAN_PICKER_CARD_SX, PLAN_PICKER_LABEL_SX, PLAN_P
|
|
|
39
39
|
import { SUBSCRIPTION_SECTION_ALERT_SX } from '../billing/SubscriptionSection';
|
|
40
40
|
import { WITHDRAWAL_DIALOG_ALERT_SX } from '../billing/WithdrawalDialog';
|
|
41
41
|
import { WITHDRAWAL_SECTION_ALERT_SX } from '../billing/WithdrawalSection';
|
|
42
|
+
import { FREE_ACCESS_SWITCH_ALERT_SX } from '../billing/FreeAccessSwitch';
|
|
43
|
+
import { OPERATOR_OVERVIEW_ALERT_SX } from '../billing/OperatorOverview';
|
|
42
44
|
import { RECEIPT_CONSENT_ALERT_SX, RECEIPT_CONSENT_CONTROL_SX } from '../receipts/ReceiptConsentSwitch';
|
|
43
45
|
import { RECEIPT_CAPTURE_ACTION_SX, RECEIPT_CAPTURE_ALERT_SX, RECEIPT_CAPTURE_CARD_SX, RECEIPT_CAPTURE_PROGRESS_SX } from '../receipts/ReceiptCaptureCard';
|
|
44
46
|
import { RECEIPT_PICKER_ACTION_SX, RECEIPT_PICKER_CARD_SX } from '../receipts/ReceiptPicker';
|
|
@@ -46,6 +48,8 @@ import { RECEIPT_REVIEW_ACTIONS_SX, RECEIPT_REVIEW_ACTION_BUTTON_SX, RECEIPT_REV
|
|
|
46
48
|
const SX_EXPORTS = {
|
|
47
49
|
ACCESS_CODE_ACTION_SX,
|
|
48
50
|
ACCESS_ENDED_BANNER_ALERT_SX,
|
|
51
|
+
FREE_ACCESS_SWITCH_ALERT_SX,
|
|
52
|
+
OPERATOR_OVERVIEW_ALERT_SX,
|
|
49
53
|
ACCESS_CODE_ALERT_SX,
|
|
50
54
|
ACCESS_CODE_PRIMARY_ACTION_SX,
|
|
51
55
|
ACCESS_CODE_SINGLE_USE_ALERT_SX,
|
|
@@ -236,6 +240,8 @@ export const KIT_COMPONENT_SX_REGISTRY = [
|
|
|
236
240
|
entry('PlanPicker.label', 'MuiFormControlLabel', PLAN_PICKER_LABEL_SX),
|
|
237
241
|
entry('PlanPicker.alert', 'MuiAlert', PLAN_PICKER_ALERT_SX),
|
|
238
242
|
entry('SubscriptionSection.alert', 'MuiAlert', SUBSCRIPTION_SECTION_ALERT_SX),
|
|
243
|
+
entry('FreeAccessSwitch.alert', 'MuiAlert', FREE_ACCESS_SWITCH_ALERT_SX),
|
|
244
|
+
entry('OperatorOverview.alert', 'MuiAlert', OPERATOR_OVERVIEW_ALERT_SX),
|
|
239
245
|
entry('WithdrawalDialog.alert', 'MuiAlert', WITHDRAWAL_DIALOG_ALERT_SX),
|
|
240
246
|
entry('WithdrawalSection.alert', 'MuiAlert', WITHDRAWAL_SECTION_ALERT_SX),
|
|
241
247
|
entry('ReceiptConsentSwitch.alert', 'MuiAlert', RECEIPT_CONSENT_ALERT_SX),
|