310 lines
10 KiB
Vue
310 lines
10 KiB
Vue
<script setup>
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useRouter } from 'vue-router';
|
|
import { mdiAccountArrowRightOutline, mdiDomainPlus, mdiEmailOutline } from '@mdi/js';
|
|
import { useOrganizationStore } from '@/features/organizations/stores/organizationStore.js';
|
|
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
|
|
|
|
const { t } = useI18n();
|
|
const router = useRouter();
|
|
const organizationStore = useOrganizationStore();
|
|
const workspaceStore = useWorkspaceStore();
|
|
|
|
const createForm = reactive({
|
|
name: '',
|
|
membershipTierId: null,
|
|
});
|
|
const accessForm = reactive({
|
|
targetType: 'organization',
|
|
targetName: '',
|
|
adminEmail: '',
|
|
note: '',
|
|
});
|
|
const createError = ref(null);
|
|
const requestStatus = ref(null);
|
|
|
|
const accessTypeOptions = computed(() => [
|
|
{ title: t('organizationOnboarding.request.types.organization'), value: 'organization' },
|
|
{ title: t('organizationOnboarding.request.types.workspace'), value: 'workspace' },
|
|
]);
|
|
const membershipTierOptions = computed(() =>
|
|
organizationStore.membershipTiers.map(tier => ({
|
|
title: tier.name,
|
|
value: tier.id,
|
|
props: {
|
|
subtitle: formatTierPrice(tier),
|
|
},
|
|
}))
|
|
);
|
|
|
|
function formatTierPrice(tier) {
|
|
if (tier.isCustom || tier.monthlyPriceCents === null || tier.monthlyPriceCents === undefined) {
|
|
return t('organizationOnboarding.create.tiers.customPrice');
|
|
}
|
|
|
|
if (tier.monthlyPriceCents === 0) {
|
|
return t('organizationOnboarding.create.tiers.freePrice');
|
|
}
|
|
|
|
return t('organizationOnboarding.create.tiers.monthlyPrice', {
|
|
price: `$${Math.round(tier.monthlyPriceCents / 100)}`,
|
|
});
|
|
}
|
|
|
|
async function createOrganization() {
|
|
if (organizationStore.isCreating) {
|
|
return;
|
|
}
|
|
|
|
createError.value = null;
|
|
const name = createForm.name.trim();
|
|
|
|
if (!name) {
|
|
createError.value = t('organizationOnboarding.create.errors.required');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await organizationStore.createOrganization({
|
|
name,
|
|
membershipTierId: createForm.membershipTierId,
|
|
});
|
|
await Promise.all([
|
|
organizationStore.fetchOrganizations(),
|
|
workspaceStore.fetchWorkspaces(),
|
|
]);
|
|
await router.push({ name: 'workspace-create' });
|
|
} catch (error) {
|
|
createError.value = t('organizationOnboarding.create.errors.failed');
|
|
}
|
|
}
|
|
|
|
function requestAccess() {
|
|
requestStatus.value = null;
|
|
|
|
if (!accessForm.adminEmail.trim() || !accessForm.targetName.trim()) {
|
|
requestStatus.value = t('organizationOnboarding.request.errors.required');
|
|
return;
|
|
}
|
|
|
|
const subject = t('organizationOnboarding.request.emailSubject', {
|
|
target: accessForm.targetName.trim(),
|
|
});
|
|
const body = t('organizationOnboarding.request.emailBody', {
|
|
targetType: t(`organizationOnboarding.request.types.${accessForm.targetType}`),
|
|
target: accessForm.targetName.trim(),
|
|
note: accessForm.note.trim() || t('organizationOnboarding.request.noNote'),
|
|
});
|
|
|
|
window.location.href = `mailto:${encodeURIComponent(accessForm.adminEmail.trim())}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
|
requestStatus.value = t('organizationOnboarding.request.sent');
|
|
}
|
|
|
|
onMounted(async () => {
|
|
const tiers = await organizationStore.fetchMembershipTiers();
|
|
createForm.membershipTierId = tiers[0]?.id ?? null;
|
|
});
|
|
|
|
watch(
|
|
() => organizationStore.membershipTiers,
|
|
tiers => {
|
|
if (!createForm.membershipTierId) {
|
|
createForm.membershipTierId = tiers[0]?.id ?? null;
|
|
}
|
|
}
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<section class="onboarding-page">
|
|
<header class="hero">
|
|
<div>
|
|
<div class="eyebrow">{{ t('organizationOnboarding.eyebrow') }}</div>
|
|
<h1>{{ t('organizationOnboarding.title') }}</h1>
|
|
<p>{{ t('organizationOnboarding.description') }}</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="onboarding-grid">
|
|
<article class="panel">
|
|
<div class="panel-header">
|
|
<v-icon :icon="mdiDomainPlus" />
|
|
<div>
|
|
<strong>{{ t('organizationOnboarding.create.title') }}</strong>
|
|
<span>{{ t('organizationOnboarding.create.description') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="createError"
|
|
class="page-message error"
|
|
>
|
|
{{ createError }}
|
|
</div>
|
|
|
|
<v-form
|
|
class="form-stack"
|
|
@submit.prevent="createOrganization"
|
|
>
|
|
<v-text-field
|
|
v-model="createForm.name"
|
|
:label="t('organizationOnboarding.create.fields.name')"
|
|
:placeholder="t('organizationOnboarding.create.fields.namePlaceholder')"
|
|
:disabled="organizationStore.isCreating"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
<v-select
|
|
v-model="createForm.membershipTierId"
|
|
:items="membershipTierOptions"
|
|
:label="t('organizationOnboarding.create.fields.membershipTier')"
|
|
:loading="organizationStore.isLoadingMembershipTiers"
|
|
:disabled="organizationStore.isCreating"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
|
|
<v-btn
|
|
:loading="organizationStore.isCreating"
|
|
color="primary"
|
|
type="submit"
|
|
>
|
|
{{ t('organizationOnboarding.create.action') }}
|
|
</v-btn>
|
|
</v-form>
|
|
</article>
|
|
|
|
<article class="panel">
|
|
<div class="panel-header">
|
|
<v-icon :icon="mdiAccountArrowRightOutline" />
|
|
<div>
|
|
<strong>{{ t('organizationOnboarding.request.title') }}</strong>
|
|
<span>{{ t('organizationOnboarding.request.description') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="requestStatus"
|
|
class="page-message"
|
|
>
|
|
{{ requestStatus }}
|
|
</div>
|
|
|
|
<v-form
|
|
class="form-stack"
|
|
@submit.prevent="requestAccess"
|
|
>
|
|
<v-select
|
|
v-model="accessForm.targetType"
|
|
:items="accessTypeOptions"
|
|
:label="t('organizationOnboarding.request.fields.type')"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
<v-text-field
|
|
v-model="accessForm.targetName"
|
|
:label="t('organizationOnboarding.request.fields.name')"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
<v-text-field
|
|
v-model="accessForm.adminEmail"
|
|
:label="t('organizationOnboarding.request.fields.adminEmail')"
|
|
type="email"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
<v-textarea
|
|
v-model="accessForm.note"
|
|
:label="t('organizationOnboarding.request.fields.note')"
|
|
rows="3"
|
|
variant="outlined"
|
|
hide-details
|
|
/>
|
|
|
|
<v-btn
|
|
color="secondary"
|
|
type="submit"
|
|
:prepend-icon="mdiEmailOutline"
|
|
>
|
|
{{ t('organizationOnboarding.request.action') }}
|
|
</v-btn>
|
|
</v-form>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
@reference "@/assets/main.css";
|
|
.onboarding-page {
|
|
@apply mx-auto flex w-full max-w-6xl flex-col gap-6 px-5 py-8 md:px-8;
|
|
}
|
|
|
|
.hero,
|
|
.panel {
|
|
@apply rounded-lg border;
|
|
border-color: rgba(23, 32, 51, 0.08);
|
|
background: rgba(255, 255, 255, 0.92);
|
|
}
|
|
|
|
.hero {
|
|
@apply p-6 md:p-8;
|
|
}
|
|
|
|
.eyebrow {
|
|
@apply text-xs font-bold uppercase tracking-[0.22em];
|
|
color: #0f766e;
|
|
}
|
|
|
|
.hero h1 {
|
|
@apply mt-2 text-3xl font-black md:text-4xl;
|
|
color: #172033;
|
|
}
|
|
|
|
.hero p,
|
|
.panel-header span {
|
|
@apply mt-2 text-sm leading-6;
|
|
color: #526178;
|
|
}
|
|
|
|
.onboarding-grid {
|
|
@apply grid gap-4 lg:grid-cols-2;
|
|
}
|
|
|
|
.panel {
|
|
@apply flex flex-col gap-4 p-5;
|
|
}
|
|
|
|
.panel-header {
|
|
@apply flex gap-3;
|
|
}
|
|
|
|
.panel-header .v-icon {
|
|
@apply mt-1;
|
|
color: #0f766e;
|
|
}
|
|
|
|
.panel-header strong {
|
|
@apply block text-xl font-black;
|
|
color: #172033;
|
|
}
|
|
|
|
.form-stack {
|
|
@apply flex flex-col gap-3;
|
|
}
|
|
|
|
.page-message {
|
|
@apply rounded-lg border p-3 text-sm font-semibold;
|
|
border-color: rgba(23, 32, 51, 0.08);
|
|
background: rgba(23, 32, 51, 0.04);
|
|
color: #526178;
|
|
}
|
|
|
|
.page-message.error {
|
|
border-color: rgba(220, 38, 38, 0.24);
|
|
color: #b91c1c;
|
|
}
|
|
</style>
|