Ask for organization business field at signup, including jewelry.
Store Didar-style industry on the org so registration, settings, and Ask Mento share the same catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Organization" ADD COLUMN "businessField" TEXT;
|
||||||
@@ -134,6 +134,8 @@ model Organization {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
slug String @unique
|
slug String @unique
|
||||||
|
/// زمینه فعالیت سازمان — فهرست همتراز صنایع دیدار
|
||||||
|
businessField String?
|
||||||
baseCurrency String @default("IRR")
|
baseCurrency String @default("IRR")
|
||||||
enabledCurrencies String[] @default(["IRR", "USD", "EUR"])
|
enabledCurrencies String[] @default(["IRR", "USD", "EUR"])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ async function main() {
|
|||||||
slug: 'demo-mento',
|
slug: 'demo-mento',
|
||||||
baseCurrency: 'IRR',
|
baseCurrency: 'IRR',
|
||||||
enabledCurrencies: ['IRR', 'USD', 'EUR'],
|
enabledCurrencies: ['IRR', 'USD', 'EUR'],
|
||||||
|
businessField: 'tech',
|
||||||
users: {
|
users: {
|
||||||
create: {
|
create: {
|
||||||
email,
|
email,
|
||||||
@@ -60,6 +61,7 @@ async function main() {
|
|||||||
data: {
|
data: {
|
||||||
baseCurrency: 'IRR',
|
baseCurrency: 'IRR',
|
||||||
enabledCurrencies: ['IRR', 'USD', 'EUR'],
|
enabledCurrencies: ['IRR', 'USD', 'EUR'],
|
||||||
|
businessField: 'tech',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
for (const row of [
|
for (const row of [
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
noteAccessWhere,
|
noteAccessWhere,
|
||||||
ownerWhere,
|
ownerWhere,
|
||||||
} from '../common/record-scope';
|
} from '../common/record-scope';
|
||||||
|
import { BUSINESS_FIELD_LABELS, isBusinessField } from '@mento/shared';
|
||||||
import { AiService } from './ai.service';
|
import { AiService } from './ai.service';
|
||||||
|
|
||||||
type CatalogItem = AskMentoSourceDto;
|
type CatalogItem = AskMentoSourceDto;
|
||||||
@@ -101,7 +102,11 @@ Rules:
|
|||||||
if (!person) throw new NotFoundException('شخص پیدا نشد');
|
if (!person) throw new NotFoundException('شخص پیدا نشد');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [notes, activities, deals, memories] = await Promise.all([
|
const [org, notes, activities, deals, memories] = await Promise.all([
|
||||||
|
this.prisma.organization.findUnique({
|
||||||
|
where: { id: actor.organizationId },
|
||||||
|
select: { name: true, businessField: true },
|
||||||
|
}),
|
||||||
this.prisma.note.findMany({
|
this.prisma.note.findMany({
|
||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: actor.organizationId,
|
||||||
@@ -167,8 +172,13 @@ Rules:
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const catalog: CatalogItem[] = [];
|
const catalog: CatalogItem[] = [];
|
||||||
|
const field =
|
||||||
|
org?.businessField && isBusinessField(org.businessField)
|
||||||
|
? BUSINESS_FIELD_LABELS[org.businessField]
|
||||||
|
: 'نامشخص';
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`Today: ${new Date().toISOString()}`,
|
`Today: ${new Date().toISOString()}`,
|
||||||
|
`Organization: ${org?.name ?? '—'} | business field: ${field}`,
|
||||||
personId ? `Focus personId: ${personId}` : 'Scope: current user records',
|
personId ? `Focus personId: ${personId}` : 'Scope: current user records',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export class AuthService {
|
|||||||
data: {
|
data: {
|
||||||
name: dto.organizationName,
|
name: dto.organizationName,
|
||||||
slug,
|
slug,
|
||||||
|
businessField: dto.businessField,
|
||||||
users: {
|
users: {
|
||||||
create: {
|
create: {
|
||||||
email: dto.email.toLowerCase(),
|
email: dto.email.toLowerCase(),
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { IsEmail, IsString, MinLength } from 'class-validator';
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsIn,
|
||||||
|
IsString,
|
||||||
|
MinLength,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { BUSINESS_FIELDS } from '@mento/shared';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
@@ -24,4 +30,7 @@ export class RegisterOrganizationDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(6)
|
@MinLength(6)
|
||||||
password!: string;
|
password!: string;
|
||||||
|
|
||||||
|
@IsIn([...BUSINESS_FIELDS])
|
||||||
|
businessField!: (typeof BUSINESS_FIELDS)[number];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ArrayMinSize,
|
ArrayMinSize,
|
||||||
IsArray,
|
IsArray,
|
||||||
|
IsIn,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
import { BUSINESS_FIELDS } from '@mento/shared';
|
||||||
|
|
||||||
export class ExchangeRateInputDto {
|
export class ExchangeRateInputDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -42,3 +44,15 @@ export class UpdateOrgCurrenciesDto {
|
|||||||
@Type(() => ExchangeRateInputDto)
|
@Type(() => ExchangeRateInputDto)
|
||||||
rates?: ExchangeRateInputDto[];
|
rates?: ExchangeRateInputDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateOrganizationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(2)
|
||||||
|
@MaxLength(120)
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([...BUSINESS_FIELDS])
|
||||||
|
businessField?: (typeof BUSINESS_FIELDS)[number];
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
|||||||
import { RolesGuard } from '../auth/roles.guard';
|
import { RolesGuard } from '../auth/roles.guard';
|
||||||
import { Roles } from '../auth/roles.decorator';
|
import { Roles } from '../auth/roles.decorator';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator';
|
import { CurrentUser } from '../auth/current-user.decorator';
|
||||||
import { UpdateOrgCurrenciesDto } from './dto';
|
import { UpdateOrgCurrenciesDto, UpdateOrganizationDto } from './dto';
|
||||||
import { OrganizationsService } from './organizations.service';
|
import { OrganizationsService } from './organizations.service';
|
||||||
|
|
||||||
@Controller('organizations')
|
@Controller('organizations')
|
||||||
@@ -21,6 +21,15 @@ export class OrganizationsController {
|
|||||||
return this.organizationsService.catalog();
|
return this.organizationsService.catalog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch('current')
|
||||||
|
@Roles('owner', 'admin')
|
||||||
|
updateCurrent(
|
||||||
|
@CurrentUser() user: { organizationId: string },
|
||||||
|
@Body() dto: UpdateOrganizationDto,
|
||||||
|
) {
|
||||||
|
return this.organizationsService.updateProfile(user.organizationId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch('current/currencies')
|
@Patch('current/currencies')
|
||||||
@Roles('owner', 'admin')
|
@Roles('owner', 'admin')
|
||||||
updateCurrencies(
|
updateCurrencies(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type {
|
|||||||
OrganizationDto,
|
OrganizationDto,
|
||||||
UpdateOrgCurrenciesRequest,
|
UpdateOrgCurrenciesRequest,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
import { CURRENCY_CATALOG } from '@mento/shared';
|
import { CURRENCY_CATALOG, isBusinessField } from '@mento/shared';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { isKnownCurrency } from '../common/currency';
|
import { isKnownCurrency } from '../common/currency';
|
||||||
@@ -29,6 +29,27 @@ export class OrganizationsService {
|
|||||||
return this.toDto(org);
|
return this.toDto(org);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateProfile(
|
||||||
|
organizationId: string,
|
||||||
|
dto: { name?: string; businessField?: string },
|
||||||
|
): Promise<OrganizationDto> {
|
||||||
|
if (dto.businessField && !isBusinessField(dto.businessField)) {
|
||||||
|
throw new BadRequestException('زمینه فعالیت معتبر نیست');
|
||||||
|
}
|
||||||
|
const name = dto.name?.trim();
|
||||||
|
if (dto.name !== undefined && (!name || name.length < 2)) {
|
||||||
|
throw new BadRequestException('نام سازمان معتبر نیست');
|
||||||
|
}
|
||||||
|
await this.prisma.organization.update({
|
||||||
|
where: { id: organizationId },
|
||||||
|
data: {
|
||||||
|
...(name ? { name } : {}),
|
||||||
|
...(dto.businessField ? { businessField: dto.businessField } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return this.getCurrent(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
async updateCurrencies(
|
async updateCurrencies(
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
dto: UpdateOrgCurrenciesRequest,
|
dto: UpdateOrgCurrenciesRequest,
|
||||||
@@ -130,6 +151,7 @@ export class OrganizationsService {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
businessField: string | null;
|
||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
enabledCurrencies: string[];
|
enabledCurrencies: string[];
|
||||||
exchangeRates: Array<{
|
exchangeRates: Array<{
|
||||||
@@ -142,6 +164,10 @@ export class OrganizationsService {
|
|||||||
id: org.id,
|
id: org.id,
|
||||||
name: org.name,
|
name: org.name,
|
||||||
slug: org.slug,
|
slug: org.slug,
|
||||||
|
businessField:
|
||||||
|
org.businessField && isBusinessField(org.businessField)
|
||||||
|
? org.businessField
|
||||||
|
: null,
|
||||||
baseCurrency: org.baseCurrency,
|
baseCurrency: org.baseCurrency,
|
||||||
enabledCurrencies: org.enabledCurrencies,
|
enabledCurrencies: org.enabledCurrencies,
|
||||||
exchangeRates: org.exchangeRates.map((r) => ({
|
exchangeRates: org.exchangeRates.map((r) => ({
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { BusinessField } from '@mento/shared';
|
||||||
|
|
||||||
|
export const BUSINESS_FIELDS: BusinessField[] = [
|
||||||
|
'education',
|
||||||
|
'beauty',
|
||||||
|
'real_estate',
|
||||||
|
'construction',
|
||||||
|
'advertising',
|
||||||
|
'insurance',
|
||||||
|
'consulting',
|
||||||
|
'startup',
|
||||||
|
'medical_devices',
|
||||||
|
'travel',
|
||||||
|
'contractor',
|
||||||
|
'trading',
|
||||||
|
'manufacturing',
|
||||||
|
'personal',
|
||||||
|
'tech',
|
||||||
|
'legal',
|
||||||
|
'repair',
|
||||||
|
'retail',
|
||||||
|
'jewelry',
|
||||||
|
'other',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BUSINESS_FIELD_LABELS: Record<BusinessField, string> = {
|
||||||
|
education: 'آموزشگاه و مؤسسات آموزشی',
|
||||||
|
beauty: 'کلینیک زیبایی و آرایشگاه',
|
||||||
|
real_estate: 'املاک و مشاورین مسکن',
|
||||||
|
construction: 'صنعت ساختمان',
|
||||||
|
advertising: 'تبلیغات و مشاوره کسبوکار',
|
||||||
|
insurance: 'بیمه',
|
||||||
|
consulting: 'شرکتهای مشاور',
|
||||||
|
startup: 'استارتاپ و کسبوکار آنلاین',
|
||||||
|
medical_devices: 'تجهیزات پزشکی',
|
||||||
|
travel: 'گردشگری و مهاجرت',
|
||||||
|
contractor: 'پیمانکاری و پروژهای',
|
||||||
|
trading: 'بازرگانی',
|
||||||
|
manufacturing: 'تولیدی و صنعتی',
|
||||||
|
personal: 'کسبوکار شخصی و خانگی',
|
||||||
|
tech: 'نرمافزار و سختافزار',
|
||||||
|
legal: 'وکلا و مشاورین حقوقی',
|
||||||
|
repair: 'خدمات و تعمیرات',
|
||||||
|
retail: 'خردهفروشی',
|
||||||
|
jewelry: 'طلا و جواهر',
|
||||||
|
other: 'سایر',
|
||||||
|
};
|
||||||
@@ -49,6 +49,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
businessField: string;
|
||||||
}) {
|
}) {
|
||||||
const { data } = await api.post<LoginResponse>('/auth/register', payload);
|
const { data } = await api.post<LoginResponse>('/auth/register', payload);
|
||||||
persist(data);
|
persist(data);
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useRouter, RouterLink } from 'vue-router';
|
import { useRouter, RouterLink } from 'vue-router';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const organizationName = ref('');
|
const organizationName = ref('');
|
||||||
|
const businessField = ref('');
|
||||||
const fullName = ref('');
|
const fullName = ref('');
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const password = ref('');
|
const password = ref('');
|
||||||
@@ -20,6 +22,7 @@ async function submit() {
|
|||||||
try {
|
try {
|
||||||
await auth.register({
|
await auth.register({
|
||||||
organizationName: organizationName.value,
|
organizationName: organizationName.value,
|
||||||
|
businessField: businessField.value,
|
||||||
fullName: fullName.value,
|
fullName: fullName.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
@@ -47,13 +50,22 @@ async function submit() {
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<p class="eyebrow">شروع با منتو</p>
|
<p class="eyebrow">شروع با منتو</p>
|
||||||
<h1>ساخت سازمان</h1>
|
<h1>ساخت سازمان</h1>
|
||||||
<p class="lead">اولین کاربر بهعنوان مالک سازمان ثبت میشود.</p>
|
<p class="lead">اولین کاربر بهعنوان مالک سازمان ثبت میشود. زمینه فعالیت کمک میکند منتو فضای کاری را با صنعت شما همتراز کند.</p>
|
||||||
|
|
||||||
<form class="form" @submit.prevent="submit">
|
<form class="form" @submit.prevent="submit">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="org">نام سازمان</label>
|
<label for="org">نام سازمان</label>
|
||||||
<input id="org" v-model="organizationName" required />
|
<input id="org" v-model="organizationName" required />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="field">زمینه فعالیت</label>
|
||||||
|
<select id="field" v-model="businessField" required>
|
||||||
|
<option value="" disabled>انتخاب کنید</option>
|
||||||
|
<option v-for="id in BUSINESS_FIELDS" :key="id" :value="id">
|
||||||
|
{{ BUSINESS_FIELD_LABELS[id] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="name">نام شما</label>
|
<label for="name">نام شما</label>
|
||||||
<input id="name" v-model="fullName" required />
|
<input id="name" v-model="fullName" required />
|
||||||
|
|||||||
@@ -5,16 +5,24 @@ import { api } from '@/lib/api';
|
|||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import type {
|
import type {
|
||||||
InviteUserRequest,
|
InviteUserRequest,
|
||||||
|
OrganizationDto,
|
||||||
OrgUserDto,
|
OrgUserDto,
|
||||||
|
UpdateOrganizationRequest,
|
||||||
UserRole,
|
UserRole,
|
||||||
} from '@mento/shared';
|
} from '@mento/shared';
|
||||||
import { USER_ROLE_LABELS, canManageUsers } from '@/lib/roles';
|
import { USER_ROLE_LABELS, canManageSettings, canManageUsers } from '@/lib/roles';
|
||||||
|
import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
const users = ref<OrgUserDto[]>([]);
|
const users = ref<OrgUserDto[]>([]);
|
||||||
|
const org = ref<OrganizationDto | null>(null);
|
||||||
|
const orgName = ref('');
|
||||||
|
const businessField = ref('');
|
||||||
|
const orgSaving = ref(false);
|
||||||
|
const orgOk = ref('');
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
const fullName = ref('');
|
const fullName = ref('');
|
||||||
@@ -29,6 +37,7 @@ const inviteRoles: Array<{ value: Exclude<UserRole, 'owner'>; label: string }> =
|
|||||||
|
|
||||||
const isOwner = computed(() => auth.user?.role === 'owner');
|
const isOwner = computed(() => auth.user?.role === 'owner');
|
||||||
const canInvite = computed(() => canManageUsers(auth.user?.role));
|
const canInvite = computed(() => canManageUsers(auth.user?.role));
|
||||||
|
const canEditOrg = computed(() => canManageSettings(auth.user?.role));
|
||||||
|
|
||||||
function roleOptionsFor(user: OrgUserDto): UserRole[] {
|
function roleOptionsFor(user: OrgUserDto): UserRole[] {
|
||||||
if (isOwner.value) return ['owner', 'admin', 'sales', 'support'];
|
if (isOwner.value) return ['owner', 'admin', 'sales', 'support'];
|
||||||
@@ -40,7 +49,14 @@ async function load() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
error.value = '';
|
error.value = '';
|
||||||
try {
|
try {
|
||||||
users.value = (await api.get<OrgUserDto[]>('/users')).data;
|
const [usersRes, orgRes] = await Promise.all([
|
||||||
|
api.get<OrgUserDto[]>('/users'),
|
||||||
|
api.get<OrganizationDto>('/organizations/current'),
|
||||||
|
]);
|
||||||
|
users.value = usersRes.data;
|
||||||
|
org.value = orgRes.data;
|
||||||
|
orgName.value = orgRes.data.name;
|
||||||
|
businessField.value = orgRes.data.businessField ?? '';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (axios.isAxiosError(e) && !e.response) {
|
if (axios.isAxiosError(e) && !e.response) {
|
||||||
error.value = 'اتصال به API برقرار نشد.';
|
error.value = 'اتصال به API برقرار نشد.';
|
||||||
@@ -52,6 +68,37 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveOrg() {
|
||||||
|
if (!canEditOrg.value) return;
|
||||||
|
if (!businessField.value) {
|
||||||
|
error.value = 'زمینه فعالیت را انتخاب کنید.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
orgSaving.value = true;
|
||||||
|
error.value = '';
|
||||||
|
orgOk.value = '';
|
||||||
|
try {
|
||||||
|
const payload: UpdateOrganizationRequest = {
|
||||||
|
name: orgName.value.trim(),
|
||||||
|
businessField: businessField.value as UpdateOrganizationRequest['businessField'],
|
||||||
|
};
|
||||||
|
const { data } = await api.patch<OrganizationDto>('/organizations/current', payload);
|
||||||
|
org.value = data;
|
||||||
|
orgName.value = data.name;
|
||||||
|
businessField.value = data.businessField ?? '';
|
||||||
|
orgOk.value = 'مشخصات سازمان ذخیره شد.';
|
||||||
|
} catch (e) {
|
||||||
|
if (axios.isAxiosError(e)) {
|
||||||
|
const msg = (e.response?.data as { message?: string | string[] })?.message;
|
||||||
|
error.value = Array.isArray(msg) ? msg.join('، ') : (msg ?? 'ذخیره سازمان ناموفق بود');
|
||||||
|
} else {
|
||||||
|
error.value = 'ذخیره سازمان ناموفق بود';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
orgSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function invite() {
|
async function invite() {
|
||||||
if (!email.value.trim() || !fullName.value.trim() || password.value.length < 8) {
|
if (!email.value.trim() || !fullName.value.trim() || password.value.length < 8) {
|
||||||
error.value = 'نام، ایمیل و رمز حداقل ۸ کاراکتر لازم است.';
|
error.value = 'نام، ایمیل و رمز حداقل ۸ کاراکتر لازم است.';
|
||||||
@@ -110,10 +157,33 @@ onMounted(load);
|
|||||||
<p class="eyebrow">تنظیمات</p>
|
<p class="eyebrow">تنظیمات</p>
|
||||||
<h1>تیم و نقشها</h1>
|
<h1>تیم و نقشها</h1>
|
||||||
<p class="lead">
|
<p class="lead">
|
||||||
کاربران سازمان را اضافه کنید و نقش بدهید. تنظیمات سازمانی فقط برای مالک و مدیر است.
|
زمینه فعالیت سازمان و کاربران را اینجا مدیریت کنید. تنظیمات سازمانی فقط برای مالک و مدیر است.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<form v-if="org" class="invite" @submit.prevent="saveOrg">
|
||||||
|
<h2>سازمان</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<label>
|
||||||
|
نام سازمان
|
||||||
|
<input v-model="orgName" :disabled="!canEditOrg" required minlength="2" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
زمینه فعالیت
|
||||||
|
<select v-model="businessField" :disabled="!canEditOrg" required>
|
||||||
|
<option value="" disabled>انتخاب کنید</option>
|
||||||
|
<option v-for="id in BUSINESS_FIELDS" :key="id" :value="id">
|
||||||
|
{{ BUSINESS_FIELD_LABELS[id] }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p v-if="orgOk" class="ok">{{ orgOk }}</p>
|
||||||
|
<button v-if="canEditOrg" class="btn" type="submit" :disabled="orgSaving">
|
||||||
|
{{ orgSaving ? 'در حال ذخیره…' : 'ذخیره مشخصات سازمان' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<p v-if="error" class="error">{{ error }}</p>
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
<p v-else-if="loading" class="muted">در حال بارگذاری…</p>
|
||||||
|
|
||||||
@@ -202,6 +272,12 @@ h1 {
|
|||||||
color: #b42318;
|
color: #b42318;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ok {
|
||||||
|
margin: 0;
|
||||||
|
color: #0f766e;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
.invite,
|
.invite,
|
||||||
.row {
|
.row {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ export interface LoginRequest {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RegisterOrganizationRequest {
|
||||||
|
organizationName: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
businessField: BusinessField;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
user: AuthUserDto;
|
user: AuthUserDto;
|
||||||
@@ -77,11 +85,70 @@ export interface OrganizationDto {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
businessField: BusinessField | null;
|
||||||
baseCurrency: string;
|
baseCurrency: string;
|
||||||
enabledCurrencies: string[];
|
enabledCurrencies: string[];
|
||||||
exchangeRates: ExchangeRateDto[];
|
exchangeRates: ExchangeRateDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateOrganizationRequest {
|
||||||
|
name?: string;
|
||||||
|
businessField?: BusinessField;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** زمینه فعالیت سازمان — همتراز صنایع دیدار */
|
||||||
|
export const BUSINESS_FIELDS = [
|
||||||
|
"education",
|
||||||
|
"beauty",
|
||||||
|
"real_estate",
|
||||||
|
"construction",
|
||||||
|
"advertising",
|
||||||
|
"insurance",
|
||||||
|
"consulting",
|
||||||
|
"startup",
|
||||||
|
"medical_devices",
|
||||||
|
"travel",
|
||||||
|
"contractor",
|
||||||
|
"trading",
|
||||||
|
"manufacturing",
|
||||||
|
"personal",
|
||||||
|
"tech",
|
||||||
|
"legal",
|
||||||
|
"repair",
|
||||||
|
"retail",
|
||||||
|
"jewelry",
|
||||||
|
"other",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type BusinessField = (typeof BUSINESS_FIELDS)[number];
|
||||||
|
|
||||||
|
export const BUSINESS_FIELD_LABELS: Record<BusinessField, string> = {
|
||||||
|
education: "آموزشگاه و مؤسسات آموزشی",
|
||||||
|
beauty: "کلینیک زیبایی و آرایشگاه",
|
||||||
|
real_estate: "املاک و مشاورین مسکن",
|
||||||
|
construction: "صنعت ساختمان",
|
||||||
|
advertising: "تبلیغات و مشاوره کسبوکار",
|
||||||
|
insurance: "بیمه",
|
||||||
|
consulting: "شرکتهای مشاور",
|
||||||
|
startup: "استارتاپ و کسبوکار آنلاین",
|
||||||
|
medical_devices: "تجهیزات پزشکی",
|
||||||
|
travel: "گردشگری و مهاجرت",
|
||||||
|
contractor: "پیمانکاری و پروژهای",
|
||||||
|
trading: "بازرگانی",
|
||||||
|
manufacturing: "تولیدی و صنعتی",
|
||||||
|
personal: "کسبوکار شخصی و خانگی",
|
||||||
|
tech: "نرمافزار و سختافزار",
|
||||||
|
legal: "وکلا و مشاورین حقوقی",
|
||||||
|
repair: "خدمات و تعمیرات",
|
||||||
|
retail: "خردهفروشی",
|
||||||
|
jewelry: "طلا و جواهر",
|
||||||
|
other: "سایر",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isBusinessField(value: string): value is BusinessField {
|
||||||
|
return (BUSINESS_FIELDS as readonly string[]).includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExchangeRateDto {
|
export interface ExchangeRateDto {
|
||||||
currency: string;
|
currency: string;
|
||||||
rateToBase: number;
|
rateToBase: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user