@antelopejs/dms-saas 0.1.8 → 0.2.1
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/README.md +262 -13
- package/dist/auth/subscription-access-gate.d.ts +1 -1
- package/dist/billing-state/index.d.ts +1 -0
- package/dist/billing-state/index.js +1 -0
- package/dist/billing-state/index.js.map +1 -1
- package/dist/billing-state/past-due-banner.d.ts +4 -0
- package/dist/billing-state/past-due-banner.js +25 -0
- package/dist/billing-state/past-due-banner.js.map +1 -0
- package/dist/config/runtime.d.ts +6 -1
- package/dist/config/runtime.js +50 -1
- package/dist/config/runtime.js.map +1 -1
- package/dist/crons/recompute-billing-state.js +4 -1
- package/dist/crons/recompute-billing-state.js.map +1 -1
- package/dist/data-api/platformOwner/index.d.ts +1 -0
- package/dist/data-api/platformOwner/index.js +1 -0
- package/dist/data-api/platformOwner/index.js.map +1 -1
- package/dist/data-api/platformOwner/invitations.d.ts +22 -0
- package/dist/data-api/platformOwner/invitations.js +121 -0
- package/dist/data-api/platformOwner/invitations.js.map +1 -0
- package/dist/data-api/platformOwner/workspaces.d.ts +3 -0
- package/dist/data-api/platformOwner/workspaces.js +64 -12
- package/dist/data-api/platformOwner/workspaces.js.map +1 -1
- package/dist/data-api/tenant/tenant-invoices.js +2 -10
- package/dist/data-api/tenant/tenant-invoices.js.map +1 -1
- package/dist/hooks/register.js +2 -1
- package/dist/hooks/register.js.map +1 -1
- package/dist/implementations/dms-saas/billing.d.ts +2 -1
- package/dist/implementations/dms-saas/billing.js +6 -0
- package/dist/implementations/dms-saas/billing.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/invoice-line-items/injection.d.ts +10 -0
- package/dist/invoice-line-items/injection.js +7 -3
- package/dist/invoice-line-items/injection.js.map +1 -1
- package/dist/notifications/subjects.d.ts +19 -19
- package/dist/operator-actions/index.d.ts +1 -0
- package/dist/operator-actions/index.js +1 -0
- package/dist/operator-actions/index.js.map +1 -1
- package/dist/operator-actions/invitation-commands.d.ts +12 -0
- package/dist/operator-actions/invitation-commands.js +46 -0
- package/dist/operator-actions/invitation-commands.js.map +1 -0
- package/dist/operator-actions/types.d.ts +1 -1
- package/dist/pages/platform/categories.d.ts +6 -6
- package/dist/pages/platform/credit-notes.d.ts +1 -1
- package/dist/pages/platform/dashboard.d.ts +2 -2
- package/dist/pages/platform/dashboard.js +2 -4
- package/dist/pages/platform/dashboard.js.map +1 -1
- package/dist/pages/platform/features.d.ts +1 -1
- package/dist/pages/platform/invoices.d.ts +1 -1
- package/dist/pages/platform/plans/edit.d.ts +2 -2
- package/dist/pages/platform/plans/index.d.ts +1 -1
- package/dist/pages/platform/plans/migrations.d.ts +1 -1
- package/dist/pages/platform/segments/index.d.ts +1 -1
- package/dist/pages/platform/settings.d.ts +1 -1
- package/dist/pages/platform/users/detail.d.ts +1 -1
- package/dist/pages/platform/users/index.d.ts +1 -1
- package/dist/pages/platform/workspaces/detail.d.ts +1 -1
- package/dist/pages/platform/workspaces/detail.js +40 -1
- package/dist/pages/platform/workspaces/detail.js.map +1 -1
- package/dist/pages/platform/workspaces/index.d.ts +4 -10
- package/dist/pages/platform/workspaces/index.js +27 -111
- package/dist/pages/platform/workspaces/index.js.map +1 -1
- package/dist/pages/public/legal/privacy-policy.js +2 -1
- package/dist/pages/public/legal/privacy-policy.js.map +1 -1
- package/dist/pages/public/legal/terms-and-conditions.js +2 -1
- package/dist/pages/public/legal/terms-and-conditions.js.map +1 -1
- package/dist/pages/public/legal/terms-of-use.js +2 -1
- package/dist/pages/public/legal/terms-of-use.js.map +1 -1
- package/dist/pages/tenant/billing.d.ts +1 -1
- package/dist/plans/index.d.ts +1 -0
- package/dist/plans/index.js +1 -0
- package/dist/plans/index.js.map +1 -1
- package/dist/plans/platform-support.d.ts +2 -0
- package/dist/plans/platform-support.js +19 -0
- package/dist/plans/platform-support.js.map +1 -0
- package/dist/plans/seat-capacity.d.ts +9 -2
- package/dist/plans/seat-capacity.js +59 -12
- package/dist/plans/seat-capacity.js.map +1 -1
- package/dist/plans/seat-hooks.js +34 -13
- package/dist/plans/seat-hooks.js.map +1 -1
- package/dist/routes/platformOwner/billing-settings.d.ts +2 -2
- package/dist/routes/platformOwner/dashboard.d.ts +8 -6
- package/dist/routes/platformOwner/dashboard.js +31 -17
- package/dist/routes/platformOwner/dashboard.js.map +1 -1
- package/dist/routes/platformOwner/index.d.ts +1 -0
- package/dist/routes/platformOwner/index.js +1 -0
- package/dist/routes/platformOwner/index.js.map +1 -1
- package/dist/routes/platformOwner/plan-deletion.d.ts +1 -1
- package/dist/routes/platformOwner/plans.d.ts +5 -5
- package/dist/routes/platformOwner/segments.d.ts +1 -1
- package/dist/routes/platformOwner/workspace-invitations.d.ts +13 -0
- package/dist/routes/platformOwner/workspace-invitations.js +73 -0
- package/dist/routes/platformOwner/workspace-invitations.js.map +1 -0
- package/dist/routes/public/legal-documents.d.ts +2 -2
- package/dist/routes/public/register.d.ts +7 -3
- package/dist/routes/public/register.js +42 -27
- package/dist/routes/public/register.js.map +1 -1
- package/dist/routes/tenant/tenant-billing.d.ts +7 -11
- package/dist/routes/tenant/tenant-billing.js +34 -26
- package/dist/routes/tenant/tenant-billing.js.map +1 -1
- package/dist/routes/tenant/tenant-plan.d.ts +1 -0
- package/dist/routes/tenant/tenant-plan.js +27 -2
- package/dist/routes/tenant/tenant-plan.js.map +1 -1
- package/dist/routes/tenant/tenant-seats.d.ts +2 -4
- package/dist/routes/tenant/tenant-seats.js.map +1 -1
- package/dist/routes/tenant/workspaces.d.ts +3 -1
- package/dist/routes/tenant/workspaces.js +10 -4
- package/dist/routes/tenant/workspaces.js.map +1 -1
- package/dist/stripe/upcoming-invoice.d.ts +21 -0
- package/dist/stripe/upcoming-invoice.js +42 -0
- package/dist/stripe/upcoming-invoice.js.map +1 -0
- package/dist/stripe/webhook-dispatch.js +2 -0
- package/dist/stripe/webhook-dispatch.js.map +1 -1
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.js +7 -0
- package/dist/types/config.js.map +1 -1
- package/dist/upcoming-invoice/db/preview-cache.model.d.ts +38 -0
- package/dist/upcoming-invoice/db/preview-cache.model.js +57 -0
- package/dist/upcoming-invoice/db/preview-cache.model.js.map +1 -0
- package/dist/upcoming-invoice/db/preview-cache.table.d.ts +10 -0
- package/dist/upcoming-invoice/db/preview-cache.table.js +42 -0
- package/dist/upcoming-invoice/db/preview-cache.table.js.map +1 -0
- package/dist/upcoming-invoice/eligibility.d.ts +17 -0
- package/dist/upcoming-invoice/eligibility.js +52 -0
- package/dist/upcoming-invoice/eligibility.js.map +1 -0
- package/dist/upcoming-invoice/invalidation.d.ts +2 -0
- package/dist/upcoming-invoice/invalidation.js +39 -0
- package/dist/upcoming-invoice/invalidation.js.map +1 -0
- package/dist/upcoming-invoice/mapping.d.ts +8 -0
- package/dist/upcoming-invoice/mapping.js +82 -0
- package/dist/upcoming-invoice/mapping.js.map +1 -0
- package/dist/upcoming-invoice/outcomes.d.ts +13 -0
- package/dist/upcoming-invoice/outcomes.js +15 -0
- package/dist/upcoming-invoice/outcomes.js.map +1 -0
- package/dist/upcoming-invoice/preview.d.ts +3 -0
- package/dist/upcoming-invoice/preview.js +80 -0
- package/dist/upcoming-invoice/preview.js.map +1 -0
- package/dist/upcoming-invoice/pricing.d.ts +3 -0
- package/dist/upcoming-invoice/pricing.js +84 -0
- package/dist/upcoming-invoice/pricing.js.map +1 -0
- package/dist/upcoming-invoice/usage.d.ts +14 -0
- package/dist/upcoming-invoice/usage.js +53 -0
- package/dist/upcoming-invoice/usage.js.map +1 -0
- package/dist/utils/invoice-lines-type.d.ts +1 -1
- package/dist/workspaces/billing-identity.d.ts +19 -0
- package/dist/workspaces/billing-identity.js +51 -0
- package/dist/workspaces/billing-identity.js.map +1 -0
- package/dist/workspaces/default-plan.d.ts +5 -0
- package/dist/workspaces/default-plan.js +109 -0
- package/dist/workspaces/default-plan.js.map +1 -0
- package/dist/workspaces/index.d.ts +2 -0
- package/dist/workspaces/index.js +2 -0
- package/dist/workspaces/index.js.map +1 -1
- package/dist/workspaces/invitations.d.ts +26 -0
- package/dist/workspaces/invitations.js +112 -0
- package/dist/workspaces/invitations.js.map +1 -0
- package/dist/workspaces/provisioning.d.ts +3 -3
- package/dist/workspaces/provisioning.js +39 -11
- package/dist/workspaces/provisioning.js.map +1 -1
- package/frontend-vue/app/components/BillingIdentityFields.vue +179 -0
- package/frontend-vue/app/components/PastDueBanner.vue +81 -0
- package/frontend-vue/app/components/PaymentMethodCard.vue +94 -38
- package/frontend-vue/app/components/PlanComparisonModal.vue +62 -26
- package/frontend-vue/app/components/PlanUpgradeModal.vue +181 -0
- package/frontend-vue/app/components/PlatformNotes.vue +4 -0
- package/frontend-vue/app/components/RefundButton.vue +10 -1
- package/frontend-vue/app/components/Register.vue +49 -92
- package/frontend-vue/app/components/SeatQuotaBanner.vue +28 -0
- package/frontend-vue/app/components/TenantBillingForm.vue +52 -128
- package/frontend-vue/app/components/TenantPlanCard.vue +68 -10
- package/frontend-vue/app/components/UserSummaryCard.vue +12 -10
- package/frontend-vue/app/components/WorkspaceAdminCreateModal.vue +202 -0
- package/frontend-vue/app/components/WorkspaceCreateModal.vue +46 -9
- package/frontend-vue/app/components/WorkspaceInvitationLink.vue +106 -0
- package/frontend-vue/app/components/WorkspaceSummaryCard.vue +39 -15
- package/frontend-vue/app/components/WorkspaceSuspended.vue +3 -5
- package/frontend-vue/app/components/WorkspaceSwitcherWidget.vue +8 -5
- package/frontend-vue/app/composables/useBillingCountries.ts +25 -5
- package/frontend-vue/app/composables/useBillingIdentity.ts +141 -0
- package/frontend-vue/app/composables/useInvitationLink.ts +69 -0
- package/frontend-vue/app/composables/usePastDueBannerAction.ts +20 -0
- package/frontend-vue/app/composables/usePlanComparison.ts +15 -0
- package/frontend-vue/app/composables/usePlanFeatureFormat.ts +312 -20
- package/frontend-vue/app/composables/usePlanFeatureLabel.ts +24 -0
- package/frontend-vue/app/composables/useSaasRegistration.ts +110 -223
- package/frontend-vue/app/composables/useSeatQuota.ts +8 -0
- package/frontend-vue/app/composables/useStripePaymentElement.ts +3 -3
- package/frontend-vue/app/composables/useWorkspaceAccessCache.ts +29 -2
- package/frontend-vue/app/composables/useWorkspaceFinalization.ts +7 -32
- package/frontend-vue/app/custom-pages/auth/no-workspace.vue +49 -194
- package/frontend-vue/app/plugins/auth-links.ts +0 -4
- package/frontend-vue/app/types/runtime-config.ts +13 -0
- package/frontend-vue/dms.frontend.ts +10 -4
- package/frontend-vue/i18n/locales/saas-en-GB.json +161 -36
- package/frontend-vue/i18n/locales/saas-fr-FR.json +162 -37
- package/frontend-vue/package.json +0 -1
- package/frontend-vue/pnpm-lock.yaml +1 -125
- package/package.json +15 -15
- package/frontend-vue/app/middleware/workspace-suspended.global.ts +0 -59
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed, onMounted, ref } from "vue";
|
|
3
3
|
|
|
4
4
|
const FREE_PLAN_PRICE = 0;
|
|
5
|
+
const SUMMARY_SEPARATOR = " · ";
|
|
5
6
|
const FULL_DATE_FORMAT: Intl.DateTimeFormatOptions = {
|
|
6
7
|
year: "numeric",
|
|
7
8
|
month: "long",
|
|
@@ -19,12 +20,17 @@ const {
|
|
|
19
20
|
const { data: billingStatus, load: loadBillingStatus } = useBillingStatus();
|
|
20
21
|
const { formatMajorUnits } = useMoneyFormat();
|
|
21
22
|
const { t, locale } = useI18n();
|
|
23
|
+
const { formatFeatureValue } = usePlanFeatureFormat();
|
|
24
|
+
const { featureLabel } = usePlanFeatureLabel();
|
|
25
|
+
const { workspace, load: loadWorkspace } = useCurrentWorkspace();
|
|
26
|
+
const { isOpen: isComparisonOpen } = usePlanComparison();
|
|
22
27
|
const planIntervalLabel = usePlanIntervalLabel("saas.workspace.plan.interval");
|
|
23
28
|
|
|
24
29
|
const data = ref<TenantPlanResponse | null>(null);
|
|
25
30
|
const isLoading = ref(true);
|
|
26
31
|
const isCancelling = ref(false);
|
|
27
|
-
const
|
|
32
|
+
const isUpgradeOpen = ref(false);
|
|
33
|
+
const upgradeTarget = ref<TenantPlanView | null>(null);
|
|
28
34
|
|
|
29
35
|
const current = computed(() => data.value?.current ?? null);
|
|
30
36
|
const pendingPlan = computed(() => data.value?.pendingPlan ?? null);
|
|
@@ -38,6 +44,15 @@ const isAccessBlocked = computed(
|
|
|
38
44
|
!data.value?.canRecoverComplimentary,
|
|
39
45
|
);
|
|
40
46
|
|
|
47
|
+
/** Settling an invoice only lifts a block when there is one to settle: an
|
|
48
|
+
* operator suspension, or a member who cannot see the invoice, gets the copy
|
|
49
|
+
* that promises nothing about payment. */
|
|
50
|
+
const blockedHintKey = computed(() =>
|
|
51
|
+
billingStatus.value?.unpaidInvoice
|
|
52
|
+
? "saas.workspace.plan.blocked_hint"
|
|
53
|
+
: "saas.workspace.plan.blocked_hint_inactive",
|
|
54
|
+
);
|
|
55
|
+
|
|
41
56
|
/** Plan mutations are owner-only server-side; a member gets the read-only
|
|
42
57
|
* card instead of buttons that can only end in a 403 toast. */
|
|
43
58
|
const isTenantOwner = computed(() => !!billingStatus.value?.isTenantOwner);
|
|
@@ -68,6 +83,34 @@ const changeAction = computed(() =>
|
|
|
68
83
|
: { color: "primary" as const, variant: "solid" as const, key: "go_paid" },
|
|
69
84
|
);
|
|
70
85
|
|
|
86
|
+
/**
|
|
87
|
+
* One line on what the plan includes: the catalogue's own description when it
|
|
88
|
+
* has one, else the plan's key (non-detail) limits.
|
|
89
|
+
*/
|
|
90
|
+
const summary = computed(() => {
|
|
91
|
+
if (current.value?.description) return current.value.description;
|
|
92
|
+
const view = data.value?.available.find(
|
|
93
|
+
(plan) => plan._id === current.value?._id,
|
|
94
|
+
);
|
|
95
|
+
if (!view || !data.value) return "";
|
|
96
|
+
return data.value.features
|
|
97
|
+
.filter(
|
|
98
|
+
(feature) =>
|
|
99
|
+
!feature.isDetailRow &&
|
|
100
|
+
view.featureValues[feature.featureId] !== undefined,
|
|
101
|
+
)
|
|
102
|
+
.map(
|
|
103
|
+
(feature) =>
|
|
104
|
+
`${featureLabel(feature)} ${formatFeatureValue(feature, view.featureValues[feature.featureId], view.currency)}`,
|
|
105
|
+
)
|
|
106
|
+
.join(SUMMARY_SEPARATOR);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function openUpgrade(plan: TenantPlanView): void {
|
|
110
|
+
upgradeTarget.value = plan;
|
|
111
|
+
isUpgradeOpen.value = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
const priceLabel = computed(() =>
|
|
72
115
|
current.value
|
|
73
116
|
? formatMajorUnits(current.value.price, current.value.currency)
|
|
@@ -111,8 +154,15 @@ async function cancelDowngrade(): Promise<void> {
|
|
|
111
154
|
}
|
|
112
155
|
}
|
|
113
156
|
|
|
157
|
+
/** The workspace name only titles the owner's plan comparison, and reading
|
|
158
|
+
* it is owner-only: a member's card never asks. */
|
|
159
|
+
async function loadWorkspaceForOwner(): Promise<void> {
|
|
160
|
+
const status = await loadBillingStatus();
|
|
161
|
+
if (status?.isTenantOwner) await loadWorkspace().catch(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
|
|
114
164
|
onMounted(() => {
|
|
115
|
-
void
|
|
165
|
+
void loadWorkspaceForOwner();
|
|
116
166
|
return fetchPlan(false);
|
|
117
167
|
});
|
|
118
168
|
</script>
|
|
@@ -140,11 +190,11 @@ onMounted(() => {
|
|
|
140
190
|
<div v-else-if="data" class="flex flex-col gap-4">
|
|
141
191
|
<div class="flex flex-wrap items-start gap-4">
|
|
142
192
|
<div class="min-w-60 grow">
|
|
143
|
-
<h2 class="text-lg font-semibold">
|
|
144
|
-
{{ current
|
|
193
|
+
<h2 v-if="current" class="text-lg font-semibold">
|
|
194
|
+
{{ current.name }}
|
|
145
195
|
</h2>
|
|
146
|
-
<p v-if="
|
|
147
|
-
{{
|
|
196
|
+
<p v-if="summary" class="text-muted mt-1 text-sm">
|
|
197
|
+
{{ summary }}
|
|
148
198
|
</p>
|
|
149
199
|
</div>
|
|
150
200
|
<div v-if="current" class="text-right">
|
|
@@ -167,9 +217,9 @@ onMounted(() => {
|
|
|
167
217
|
</div>
|
|
168
218
|
<div class="mt-2 flex justify-between gap-4 font-semibold">
|
|
169
219
|
<span>{{ $t("saas.admission.total") }}</span>
|
|
170
|
-
<span class="tabular-nums">
|
|
171
|
-
formatMajorUnits(0, current.currency)
|
|
172
|
-
|
|
220
|
+
<span class="tabular-nums">
|
|
221
|
+
{{ formatMajorUnits(0, current.currency) }}
|
|
222
|
+
</span>
|
|
173
223
|
</div>
|
|
174
224
|
</div>
|
|
175
225
|
|
|
@@ -181,7 +231,7 @@ onMounted(() => {
|
|
|
181
231
|
</p>
|
|
182
232
|
|
|
183
233
|
<p v-if="isAccessBlocked" class="text-muted text-sm">
|
|
184
|
-
{{ $t(
|
|
234
|
+
{{ $t(blockedHintKey) }}
|
|
185
235
|
</p>
|
|
186
236
|
|
|
187
237
|
<p v-else-if="data.isPlanChangeLocked" class="text-muted text-sm">
|
|
@@ -216,6 +266,14 @@ onMounted(() => {
|
|
|
216
266
|
:current-plan-id="current?._id ?? null"
|
|
217
267
|
:pending-plan-id="pendingPlan?.planId ?? null"
|
|
218
268
|
:is-recovery="data.canRecoverComplimentary"
|
|
269
|
+
:is-current-paid="isPaid"
|
|
270
|
+
:workspace-name="workspace?.name ?? null"
|
|
271
|
+
@changed="refresh"
|
|
272
|
+
@upgrade="openUpgrade"
|
|
273
|
+
/>
|
|
274
|
+
<DmsSaasPlanUpgradeModal
|
|
275
|
+
v-model:open="isUpgradeOpen"
|
|
276
|
+
:plan="upgradeTarget"
|
|
219
277
|
@changed="refresh"
|
|
220
278
|
/>
|
|
221
279
|
</div>
|
|
@@ -20,12 +20,19 @@ interface UserSummaryData {
|
|
|
20
20
|
workspaces: Array<{ isTenantOwner: boolean }>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const MISSING_DATE = "—";
|
|
24
|
+
const DAY_FORMAT: Intl.DateTimeFormatOptions = {
|
|
25
|
+
day: "2-digit",
|
|
26
|
+
month: "short",
|
|
27
|
+
year: "numeric",
|
|
28
|
+
};
|
|
29
|
+
|
|
23
30
|
const props = defineProps<{
|
|
24
31
|
routeParams?: Record<string, string>;
|
|
25
32
|
}>();
|
|
26
33
|
|
|
27
34
|
const { $authFetch } = useAuthFetch();
|
|
28
|
-
const { t } = useI18n();
|
|
35
|
+
const { t, locale } = useI18n();
|
|
29
36
|
|
|
30
37
|
const userId = computed(() => props.routeParams?.id ?? "");
|
|
31
38
|
const user = ref<UserSummaryData | null>(null);
|
|
@@ -48,13 +55,8 @@ const memberCount = computed(
|
|
|
48
55
|
() => user.value?.workspaces.filter((w) => !w.isTenantOwner).length ?? 0,
|
|
49
56
|
);
|
|
50
57
|
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
return new Date(value).toLocaleDateString("fr-FR", {
|
|
54
|
-
day: "2-digit",
|
|
55
|
-
month: "short",
|
|
56
|
-
year: "numeric",
|
|
57
|
-
});
|
|
58
|
+
function formatDay(value: string | null): string {
|
|
59
|
+
return formatDate(value, locale.value, DAY_FORMAT) ?? MISSING_DATE;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
async function load(): Promise<void> {
|
|
@@ -114,11 +116,11 @@ onMounted(load);
|
|
|
114
116
|
<dl class="grid grid-cols-2 gap-3 text-sm lg:grid-cols-4">
|
|
115
117
|
<div>
|
|
116
118
|
<dt class="text-muted">{{ $t("saas.users.column.created_at") }}</dt>
|
|
117
|
-
<dd class="mt-0.5">{{
|
|
119
|
+
<dd class="mt-0.5">{{ formatDay(user.createdAt) }}</dd>
|
|
118
120
|
</div>
|
|
119
121
|
<div>
|
|
120
122
|
<dt class="text-muted">{{ $t("saas.users.info.last_active") }}</dt>
|
|
121
|
-
<dd class="mt-0.5">{{
|
|
123
|
+
<dd class="mt-0.5">{{ formatDay(user.lastActiveAt) }}</dd>
|
|
122
124
|
</div>
|
|
123
125
|
<div>
|
|
124
126
|
<dt class="text-muted">{{ $t("saas.users.info.language") }}</dt>
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
interface PlanOption {
|
|
5
|
+
_id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
isActive: boolean;
|
|
8
|
+
isDeleted: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type CreatedOwner =
|
|
12
|
+
| { kind: "added"; userId: string }
|
|
13
|
+
| { kind: "invited"; inviteId: string };
|
|
14
|
+
|
|
15
|
+
interface CreatedWorkspace {
|
|
16
|
+
tenantId: string;
|
|
17
|
+
owner: CreatedOwner;
|
|
18
|
+
invitationEmail: "sent" | "failed" | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = defineProps<{
|
|
22
|
+
onSuccessCallback?: () => void;
|
|
23
|
+
onCancelCallback?: () => void;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const PLANS_ENDPOINT = "/api/saas/plans";
|
|
27
|
+
const CREATE_ENDPOINT = "/modules/saas/customers/workspaces/create";
|
|
28
|
+
const WARNING_TOAST_DURATION = 0;
|
|
29
|
+
|
|
30
|
+
const { $authFetch } = useAuthFetch();
|
|
31
|
+
const nuxtApp = useDmsApp();
|
|
32
|
+
const toast = useToast();
|
|
33
|
+
const { resolveApiError } = useApiErrorMessage();
|
|
34
|
+
const { fetchAndCopy } = useInvitationLink();
|
|
35
|
+
|
|
36
|
+
const plans = ref<PlanOption[]>([]);
|
|
37
|
+
const name = ref("");
|
|
38
|
+
const planId = ref<string | undefined>(undefined);
|
|
39
|
+
const ownerEmail = ref("");
|
|
40
|
+
const freeUntil = ref("");
|
|
41
|
+
const isLoading = ref(true);
|
|
42
|
+
const isSubmitting = ref(false);
|
|
43
|
+
const errorMessage = ref<string | null>(null);
|
|
44
|
+
|
|
45
|
+
const planItems = computed(() =>
|
|
46
|
+
plans.value
|
|
47
|
+
.filter((plan) => plan.isActive && !plan.isDeleted)
|
|
48
|
+
.map((plan) => ({ value: plan._id, label: plan.name })),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const canSubmit = computed(
|
|
52
|
+
() => !!name.value.trim() && !!planId.value && !!ownerEmail.value.trim(),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
async function load(): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
plans.value = await $authFetch<PlanOption[]>(PLANS_ENDPOINT);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
errorMessage.value = resolveApiError(
|
|
60
|
+
error,
|
|
61
|
+
"saas.workspaces.admin.create.error.load",
|
|
62
|
+
);
|
|
63
|
+
} finally {
|
|
64
|
+
isLoading.value = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function notifyCreated(created: CreatedWorkspace): void {
|
|
69
|
+
const t = nuxtApp.$i18n.t;
|
|
70
|
+
const { owner } = created;
|
|
71
|
+
if (created.invitationEmail !== "failed" || owner.kind !== "invited") {
|
|
72
|
+
toast.add({
|
|
73
|
+
title: t("saas.workspaces.admin.create.success"),
|
|
74
|
+
color: "success",
|
|
75
|
+
icon: "i-ph-check-circle",
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// The workspace exists and the owner is invited, but nobody told them: the
|
|
80
|
+
// operator has to hand the link over, so the notice stays until dismissed.
|
|
81
|
+
toast.add({
|
|
82
|
+
title: t("saas.workspaces.admin.create.email_failed.title"),
|
|
83
|
+
description: t("saas.workspaces.admin.create.email_failed.description"),
|
|
84
|
+
color: "warning",
|
|
85
|
+
icon: "i-ph-warning",
|
|
86
|
+
duration: WARNING_TOAST_DURATION,
|
|
87
|
+
actions: [
|
|
88
|
+
{
|
|
89
|
+
label: t("saas.workspaces.invitations.action.copy_link"),
|
|
90
|
+
icon: "i-ph-link",
|
|
91
|
+
onClick: () =>
|
|
92
|
+
fetchAndCopy({ tenantId: created.tenantId, inviteId: owner.inviteId }),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function submit(): Promise<void> {
|
|
99
|
+
if (isSubmitting.value || !canSubmit.value) return;
|
|
100
|
+
errorMessage.value = null;
|
|
101
|
+
isSubmitting.value = true;
|
|
102
|
+
try {
|
|
103
|
+
const created = await $authFetch<CreatedWorkspace>(CREATE_ENDPOINT, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
body: {
|
|
106
|
+
name: name.value.trim(),
|
|
107
|
+
planId: planId.value,
|
|
108
|
+
ownerEmail: ownerEmail.value.trim(),
|
|
109
|
+
freeUntil: freeUntil.value || null,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
notifyCreated(created);
|
|
113
|
+
props.onSuccessCallback?.();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
errorMessage.value = resolveApiError(
|
|
116
|
+
error,
|
|
117
|
+
"saas.workspaces.admin.create.error.failed",
|
|
118
|
+
);
|
|
119
|
+
} finally {
|
|
120
|
+
isSubmitting.value = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
onMounted(load);
|
|
125
|
+
</script>
|
|
126
|
+
|
|
127
|
+
<template>
|
|
128
|
+
<div v-if="isLoading" class="flex flex-col gap-3">
|
|
129
|
+
<USkeleton class="h-10 w-full" />
|
|
130
|
+
<USkeleton class="h-10 w-full" />
|
|
131
|
+
</div>
|
|
132
|
+
<form v-else class="flex flex-col gap-4" @submit.prevent="submit">
|
|
133
|
+
<UFormField
|
|
134
|
+
:label="$t('saas.workspaces.admin.create.field.name')"
|
|
135
|
+
:description="$t('saas.workspaces.admin.create.field.name_description')"
|
|
136
|
+
required
|
|
137
|
+
>
|
|
138
|
+
<UInput
|
|
139
|
+
v-model="name"
|
|
140
|
+
:placeholder="$t('saas.workspaces.admin.create.placeholder.name')"
|
|
141
|
+
class="w-full"
|
|
142
|
+
autofocus
|
|
143
|
+
/>
|
|
144
|
+
</UFormField>
|
|
145
|
+
<UFormField
|
|
146
|
+
:label="$t('saas.workspaces.admin.create.field.plan')"
|
|
147
|
+
:description="$t('saas.workspaces.admin.create.field.plan_description')"
|
|
148
|
+
required
|
|
149
|
+
>
|
|
150
|
+
<USelect
|
|
151
|
+
v-model="planId"
|
|
152
|
+
:items="planItems"
|
|
153
|
+
:placeholder="$t('saas.workspaces.admin.create.placeholder.plan')"
|
|
154
|
+
class="w-full"
|
|
155
|
+
/>
|
|
156
|
+
</UFormField>
|
|
157
|
+
<UFormField
|
|
158
|
+
:label="$t('saas.workspaces.admin.create.field.owner_email')"
|
|
159
|
+
:description="
|
|
160
|
+
$t('saas.workspaces.admin.create.field.owner_email_description')
|
|
161
|
+
"
|
|
162
|
+
required
|
|
163
|
+
>
|
|
164
|
+
<UInput
|
|
165
|
+
v-model="ownerEmail"
|
|
166
|
+
type="email"
|
|
167
|
+
:placeholder="$t('saas.workspaces.admin.create.placeholder.owner_email')"
|
|
168
|
+
class="w-full"
|
|
169
|
+
/>
|
|
170
|
+
</UFormField>
|
|
171
|
+
<UFormField
|
|
172
|
+
:label="$t('saas.workspaces.admin.create.field.free_until')"
|
|
173
|
+
:description="
|
|
174
|
+
$t('saas.workspaces.admin.create.field.free_until_description')
|
|
175
|
+
"
|
|
176
|
+
>
|
|
177
|
+
<UInput v-model="freeUntil" type="date" class="w-full" />
|
|
178
|
+
</UFormField>
|
|
179
|
+
|
|
180
|
+
<p v-if="errorMessage" class="text-error text-sm">{{ errorMessage }}</p>
|
|
181
|
+
|
|
182
|
+
<div class="flex justify-end gap-2">
|
|
183
|
+
<UButton
|
|
184
|
+
v-if="props.onCancelCallback"
|
|
185
|
+
color="neutral"
|
|
186
|
+
variant="ghost"
|
|
187
|
+
:disabled="isSubmitting"
|
|
188
|
+
@click="props.onCancelCallback()"
|
|
189
|
+
>
|
|
190
|
+
{{ $t("common.cancel") }}
|
|
191
|
+
</UButton>
|
|
192
|
+
<UButton
|
|
193
|
+
type="submit"
|
|
194
|
+
color="primary"
|
|
195
|
+
:loading="isSubmitting"
|
|
196
|
+
:disabled="!canSubmit"
|
|
197
|
+
>
|
|
198
|
+
{{ $t("saas.workspaces.admin.create.submit") }}
|
|
199
|
+
</UButton>
|
|
200
|
+
</div>
|
|
201
|
+
</form>
|
|
202
|
+
</template>
|
|
@@ -21,6 +21,7 @@ interface CreateOptions {
|
|
|
21
21
|
|
|
22
22
|
const props = defineProps<{
|
|
23
23
|
onSuccessCallback?: (tenantId: string) => void;
|
|
24
|
+
onCancelCallback?: () => void;
|
|
24
25
|
}>();
|
|
25
26
|
|
|
26
27
|
const PLANS_ENDPOINT = "/api/saas/plans/public";
|
|
@@ -52,6 +53,7 @@ const freePlan = ref<FreePlanAvailability>({
|
|
|
52
53
|
const isLoading = ref(true);
|
|
53
54
|
const isSubmitting = ref(false);
|
|
54
55
|
const errorMessage = ref<string | null>(null);
|
|
56
|
+
const nameError = ref<string | null>(null);
|
|
55
57
|
const cardError = ref<string | null>(null);
|
|
56
58
|
const stripeHandle = ref<ReturnType<typeof useStripePaymentElement> | null>(
|
|
57
59
|
null,
|
|
@@ -94,6 +96,11 @@ const selectablePlans = computed(() =>
|
|
|
94
96
|
plans.value.filter((plan) => !isPlanDisabled(plan)),
|
|
95
97
|
);
|
|
96
98
|
|
|
99
|
+
// Only once the catalogue actually loaded: a failed load reports itself.
|
|
100
|
+
const hasNoPlanOnOffer = computed(
|
|
101
|
+
() => !isLoading.value && !errorMessage.value && plans.value.length === 0,
|
|
102
|
+
);
|
|
103
|
+
|
|
97
104
|
function selectPlan(plan: PlanOption): void {
|
|
98
105
|
if (isPlanDisabled(plan)) return;
|
|
99
106
|
selectedPlanId.value = plan._id;
|
|
@@ -125,6 +132,8 @@ async function load(): Promise<void> {
|
|
|
125
132
|
plans.value = loadedPlans;
|
|
126
133
|
freePlan.value = options.freePlan;
|
|
127
134
|
selectedPlanId.value = selectablePlans.value[0]?._id ?? null;
|
|
135
|
+
// Nothing to pay for, so no card to set up.
|
|
136
|
+
if (loadedPlans.length === 0) return;
|
|
128
137
|
} catch (error) {
|
|
129
138
|
errorMessage.value = resolveApiError(
|
|
130
139
|
error,
|
|
@@ -147,10 +156,10 @@ async function load(): Promise<void> {
|
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
function validate(): boolean {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
159
|
+
nameError.value = workspaceName.value.trim()
|
|
160
|
+
? null
|
|
161
|
+
: t("saas.workspaces.create.error.name");
|
|
162
|
+
if (nameError.value) return false;
|
|
154
163
|
if (!selectedPlanId.value) {
|
|
155
164
|
errorMessage.value = t("saas.workspaces.create.error.plan");
|
|
156
165
|
return false;
|
|
@@ -201,17 +210,28 @@ onMounted(load);
|
|
|
201
210
|
<USkeleton class="h-24 w-full" />
|
|
202
211
|
</div>
|
|
203
212
|
<form v-else class="flex flex-col gap-4" @submit.prevent="submit">
|
|
204
|
-
<UFormField
|
|
213
|
+
<UFormField
|
|
214
|
+
:label="$t('saas.workspaces.create.field.name')"
|
|
215
|
+
:error="nameError ?? false"
|
|
216
|
+
>
|
|
205
217
|
<UInput
|
|
206
218
|
v-model="workspaceName"
|
|
207
219
|
:placeholder="$t('saas.workspaces.create.placeholder.name')"
|
|
208
220
|
class="w-full"
|
|
209
221
|
autofocus
|
|
210
|
-
|
|
222
|
+
@update:model-value="nameError = null"
|
|
211
223
|
/>
|
|
212
224
|
</UFormField>
|
|
213
225
|
|
|
214
|
-
<
|
|
226
|
+
<UAlert
|
|
227
|
+
v-if="hasNoPlanOnOffer"
|
|
228
|
+
color="neutral"
|
|
229
|
+
variant="subtle"
|
|
230
|
+
icon="i-ph-info"
|
|
231
|
+
:description="$t('saas.workspaces.create.self_serve.no_plans')"
|
|
232
|
+
/>
|
|
233
|
+
|
|
234
|
+
<div v-else class="flex flex-col gap-2">
|
|
215
235
|
<span class="text-sm font-medium">
|
|
216
236
|
{{ $t("saas.workspaces.create.field.plan") }}
|
|
217
237
|
<span class="text-muted font-normal">
|
|
@@ -240,7 +260,10 @@ onMounted(load);
|
|
|
240
260
|
</button>
|
|
241
261
|
</div>
|
|
242
262
|
|
|
243
|
-
<UFormField
|
|
263
|
+
<UFormField
|
|
264
|
+
v-if="!hasNoPlanOnOffer"
|
|
265
|
+
:label="$t('saas.workspaces.create.self_serve.card')"
|
|
266
|
+
>
|
|
244
267
|
<div :id="PAYMENT_ELEMENT_ID" class="border-default rounded-md border p-4" />
|
|
245
268
|
<p v-if="cardError" class="text-error mt-1 text-sm">{{ cardError }}</p>
|
|
246
269
|
</UFormField>
|
|
@@ -248,7 +271,21 @@ onMounted(load);
|
|
|
248
271
|
<p v-if="errorMessage" class="text-error text-sm">{{ errorMessage }}</p>
|
|
249
272
|
|
|
250
273
|
<div class="flex justify-end gap-2">
|
|
251
|
-
<UButton
|
|
274
|
+
<UButton
|
|
275
|
+
v-if="props.onCancelCallback"
|
|
276
|
+
color="neutral"
|
|
277
|
+
variant="ghost"
|
|
278
|
+
:disabled="isSubmitting"
|
|
279
|
+
@click="props.onCancelCallback()"
|
|
280
|
+
>
|
|
281
|
+
{{ $t("common.cancel") }}
|
|
282
|
+
</UButton>
|
|
283
|
+
<UButton
|
|
284
|
+
type="submit"
|
|
285
|
+
color="primary"
|
|
286
|
+
:loading="isSubmitting"
|
|
287
|
+
:disabled="hasNoPlanOnOffer"
|
|
288
|
+
>
|
|
252
289
|
{{ $t("saas.workspaces.create.self_serve.submit") }}
|
|
253
290
|
</UButton>
|
|
254
291
|
</div>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onMounted, ref } from "vue";
|
|
3
|
+
|
|
4
|
+
interface InvitationRow {
|
|
5
|
+
_id: string;
|
|
6
|
+
_instance: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
rowData?: InvitationRow;
|
|
12
|
+
onSuccessCallback?: () => void;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const nuxtApp = useDmsApp();
|
|
16
|
+
const { resolveApiError } = useApiErrorMessage();
|
|
17
|
+
const { fetchInvitationLink, copyLink } = useInvitationLink();
|
|
18
|
+
|
|
19
|
+
const link = ref<string | null>(null);
|
|
20
|
+
const expiresAt = ref<string | null>(null);
|
|
21
|
+
const errorMessage = ref<string | null>(null);
|
|
22
|
+
const isLoading = ref(true);
|
|
23
|
+
|
|
24
|
+
function formatDate(value: string): string {
|
|
25
|
+
return new Intl.DateTimeFormat(nuxtApp.$i18n.locale.value, {
|
|
26
|
+
dateStyle: "medium",
|
|
27
|
+
timeStyle: "short",
|
|
28
|
+
}).format(new Date(value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function load(): Promise<void> {
|
|
32
|
+
if (!props.rowData) return;
|
|
33
|
+
try {
|
|
34
|
+
const invitation = await fetchInvitationLink({
|
|
35
|
+
tenantId: props.rowData._instance,
|
|
36
|
+
inviteId: props.rowData._id,
|
|
37
|
+
});
|
|
38
|
+
link.value = invitation.link;
|
|
39
|
+
expiresAt.value = invitation.expiresAt;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
errorMessage.value = resolveApiError(
|
|
42
|
+
error,
|
|
43
|
+
"saas.workspaces.invitations.link.error",
|
|
44
|
+
);
|
|
45
|
+
} finally {
|
|
46
|
+
isLoading.value = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function copy(): Promise<void> {
|
|
51
|
+
if (!link.value) return;
|
|
52
|
+
await copyLink(link.value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMounted(load);
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<div class="flex flex-col gap-4">
|
|
60
|
+
<USkeleton v-if="isLoading" class="h-10 w-full" />
|
|
61
|
+
<UAlert
|
|
62
|
+
v-else-if="errorMessage"
|
|
63
|
+
color="error"
|
|
64
|
+
variant="subtle"
|
|
65
|
+
icon="i-ph-warning-circle"
|
|
66
|
+
:description="errorMessage"
|
|
67
|
+
/>
|
|
68
|
+
<template v-else-if="link">
|
|
69
|
+
<UFormField
|
|
70
|
+
:label="$t('saas.workspaces.invitations.link.label')"
|
|
71
|
+
:help="
|
|
72
|
+
expiresAt
|
|
73
|
+
? $t('saas.workspaces.invitations.link.expires', {
|
|
74
|
+
date: formatDate(expiresAt),
|
|
75
|
+
})
|
|
76
|
+
: undefined
|
|
77
|
+
"
|
|
78
|
+
>
|
|
79
|
+
<div class="flex gap-2">
|
|
80
|
+
<UInput
|
|
81
|
+
:model-value="link"
|
|
82
|
+
readonly
|
|
83
|
+
class="w-full"
|
|
84
|
+
data-testid="invitation-link"
|
|
85
|
+
@focus="($event.target as HTMLInputElement).select()"
|
|
86
|
+
/>
|
|
87
|
+
<UButton icon="i-ph-copy" color="primary" @click="copy">
|
|
88
|
+
{{ $t("saas.workspaces.invitations.link.copy") }}
|
|
89
|
+
</UButton>
|
|
90
|
+
</div>
|
|
91
|
+
</UFormField>
|
|
92
|
+
<p class="text-muted text-xs">
|
|
93
|
+
{{ $t("saas.workspaces.invitations.link.warning") }}
|
|
94
|
+
</p>
|
|
95
|
+
</template>
|
|
96
|
+
<div class="flex justify-end">
|
|
97
|
+
<UButton
|
|
98
|
+
color="neutral"
|
|
99
|
+
variant="ghost"
|
|
100
|
+
@click="props.onSuccessCallback?.()"
|
|
101
|
+
>
|
|
102
|
+
{{ $t("saas.workspaces.invitations.link.close") }}
|
|
103
|
+
</UButton>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</template>
|