Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74dce675b9 |
@@ -28,6 +28,39 @@ import { normalizeUserEmail } from '~/utils/normalize-user-email';
|
||||
/** Минимум членов совета — как MIN_SOVIET_MEMBERS_COUNT в контракте soviet (3 prod / 1 dev). */
|
||||
const MIN_SOVIET_MEMBERS_COUNT = config.min_soviet_members_count;
|
||||
|
||||
function councilRoleLabel(index: number, role?: string): string {
|
||||
if (role === 'chairman' || index === 0) return 'председателя совета';
|
||||
return `члена совета №${index + 1}`;
|
||||
}
|
||||
|
||||
function assertSovietMemberComplete(
|
||||
member: InstallInputDomainInterface['soviet'][number],
|
||||
index: number,
|
||||
): void {
|
||||
const data = member?.individual_data;
|
||||
const label = councilRoleLabel(index, member?.role);
|
||||
const email = typeof data?.email === 'string' ? data.email.trim() : '';
|
||||
|
||||
if (!email) {
|
||||
throw new BadRequestException(`Не указан email для ${label}`);
|
||||
}
|
||||
|
||||
const requiredStringFields: Array<[keyof Cooperative.Users.IIndividualData, string]> = [
|
||||
['last_name', 'фамилия'],
|
||||
['first_name', 'имя'],
|
||||
['full_address', 'адрес регистрации'],
|
||||
['birthdate', 'дата рождения'],
|
||||
['phone', 'телефон'],
|
||||
];
|
||||
|
||||
for (const [field, humanName] of requiredStringFields) {
|
||||
const value = data?.[field];
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new BadRequestException(`Не заполнено поле «${humanName}» для ${label}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InstallInteractor {
|
||||
constructor(
|
||||
@@ -140,6 +173,8 @@ export class InstallInteractor {
|
||||
);
|
||||
}
|
||||
|
||||
data.soviet.forEach((member, index) => assertSovietMemberComplete(member, index));
|
||||
|
||||
// Повтор после прерванной установки: снимаем maintenance, дальше — обычный прогон.
|
||||
if (isIncompleteInstallMaintenance) {
|
||||
await this.monoStatusRepository.setStatus(SystemStatus.initialized);
|
||||
|
||||
@@ -3,3 +3,9 @@ export {
|
||||
getSovietMembersProgressHint,
|
||||
getSovietContinueBlockedTooltip,
|
||||
} from './minSovietMembers';
|
||||
export {
|
||||
isSovietMemberComplete,
|
||||
countCompleteSovietMembers,
|
||||
getFirstIncompleteSovietMemberIndex,
|
||||
type SovietInstallMember,
|
||||
} from './sovietMemberValidation';
|
||||
|
||||
@@ -1,23 +1,57 @@
|
||||
import { env } from 'src/shared/config';
|
||||
import {
|
||||
countCompleteSovietMembers,
|
||||
getFirstIncompleteSovietMemberIndex,
|
||||
type SovietInstallMember,
|
||||
} from './sovietMemberValidation';
|
||||
|
||||
/** Как MIN_SOVIET_MEMBERS_COUNT в контракте soviet: 1 на dev/testnet, 3 на production. */
|
||||
export function getMinSovietMembersCount(): number {
|
||||
return env.NODE_ENV === 'production' ? 3 : 1;
|
||||
}
|
||||
|
||||
function councilRoleLabel(index: number, role?: SovietInstallMember['role']): string {
|
||||
if (role === 'chairman' || index === 0) return 'председателя совета';
|
||||
return `члена совета №${index + 1}`;
|
||||
}
|
||||
|
||||
/** Краткая подсказка под формой: сколько указано и чего не хватает. */
|
||||
export function getSovietMembersProgressHint(currentCount: number, min = getMinSovietMembersCount()): string | null {
|
||||
if (min <= 1 || currentCount >= min) return null;
|
||||
const missing = min - currentCount;
|
||||
return `Указано ${currentCount} из ${min}. Добавьте ещё ${missing} ${pluralCouncilSlot(missing)}.`;
|
||||
export function getSovietMembersProgressHint(
|
||||
members: SovietInstallMember[],
|
||||
min = getMinSovietMembersCount(),
|
||||
): string | null {
|
||||
const completeCount = countCompleteSovietMembers(members);
|
||||
if (completeCount < min) {
|
||||
const missing = min - completeCount;
|
||||
return `Заполнены данные ${completeCount} из ${min}. Дозаполните ещё ${missing} ${pluralCouncilSlot(missing)}.`;
|
||||
}
|
||||
|
||||
const incompleteIndex = getFirstIncompleteSovietMemberIndex(members);
|
||||
if (incompleteIndex >= 0) {
|
||||
return `Заполните все поля для ${councilRoleLabel(incompleteIndex, members[incompleteIndex]?.role)}.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Текст для tooltip заблокированной кнопки «Продолжить». */
|
||||
export function getSovietContinueBlockedTooltip(currentCount: number, min = getMinSovietMembersCount()): string {
|
||||
if (currentCount >= min) return '';
|
||||
if (min === 1) return 'Заполните данные председателя';
|
||||
const missing = min - currentCount;
|
||||
return `Для продолжения нужно ${min} человека в составе совета (ещё ${missing} ${pluralCouncilSlot(missing)})`;
|
||||
export function getSovietContinueBlockedTooltip(
|
||||
members: SovietInstallMember[],
|
||||
min = getMinSovietMembersCount(),
|
||||
): string {
|
||||
const completeCount = countCompleteSovietMembers(members);
|
||||
if (completeCount < min) {
|
||||
const missing = min - completeCount;
|
||||
if (min === 1) return 'Заполните все данные председателя совета';
|
||||
return `Для продолжения нужно ${min} человека с полными данными (ещё ${missing} ${pluralCouncilSlot(missing)})`;
|
||||
}
|
||||
|
||||
const incompleteIndex = getFirstIncompleteSovietMemberIndex(members);
|
||||
if (incompleteIndex >= 0) {
|
||||
return `Заполните все поля для ${councilRoleLabel(incompleteIndex, members[incompleteIndex]?.role)}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function pluralCouncilSlot(count: number): string {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import emailRegex from 'email-regex';
|
||||
import type { IIndividualData } from 'src/shared/lib/types/user/IUserData';
|
||||
|
||||
const emailValidator = emailRegex({ exact: true });
|
||||
|
||||
/** Член совета в мастере установки (store + API install). */
|
||||
export interface SovietInstallMember {
|
||||
role?: 'chairman' | 'member';
|
||||
individual_data?: IIndividualData & { email?: string };
|
||||
}
|
||||
|
||||
function isNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === 'string' && value.trim().length > 0;
|
||||
}
|
||||
|
||||
function isPhoneFilled(value: unknown): boolean {
|
||||
if (typeof value !== 'string') return false;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return false;
|
||||
return trimmed !== '+7 (___) ___-__-__';
|
||||
}
|
||||
|
||||
/** Все обязательные поля IndividualDataForm + email (как на экране «Состав совета»). */
|
||||
export function isSovietMemberComplete(member: SovietInstallMember | null | undefined): boolean {
|
||||
const data = member?.individual_data;
|
||||
if (!data) return false;
|
||||
|
||||
if (!isNonEmptyString(data.email) || emailValidator.test(data.email.trim()) !== true) {
|
||||
return false;
|
||||
}
|
||||
if (!isNonEmptyString(data.last_name) || !isNonEmptyString(data.first_name)) {
|
||||
return false;
|
||||
}
|
||||
if (!isNonEmptyString(data.full_address) || !isNonEmptyString(data.birthdate)) {
|
||||
return false;
|
||||
}
|
||||
if (!isPhoneFilled(data.phone)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function countCompleteSovietMembers(members: SovietInstallMember[]): number {
|
||||
return members.filter(isSovietMemberComplete).length;
|
||||
}
|
||||
|
||||
export function getFirstIncompleteSovietMemberIndex(members: SovietInstallMember[]): number {
|
||||
return members.findIndex((member) => !isSovietMemberComplete(member));
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Mutations, Queries } from '@coopenomics/sdk';
|
||||
import { useInstallCooperativeStore } from 'src/entities/Installer/model'
|
||||
import { stripLegacyBankKpp } from 'src/shared/lib/utils/stripLegacyBankKpp';
|
||||
import { getMinSovietMembersCount } from '../lib';
|
||||
import { getMinSovietMembersCount, isSovietMemberComplete } from '../lib';
|
||||
import { api } from '../api'
|
||||
|
||||
export type IInstallInput = Mutations.System.InstallSystem.IInput['data']
|
||||
@@ -61,6 +61,15 @@ export const useInstallCooperative = (): {
|
||||
);
|
||||
}
|
||||
|
||||
const incompleteIndex = store.soviet.findIndex((member) => !isSovietMemberComplete(member));
|
||||
if (incompleteIndex >= 0) {
|
||||
const roleLabel =
|
||||
store.soviet[incompleteIndex]?.role === 'chairman' || incompleteIndex === 0
|
||||
? 'председателя совета'
|
||||
: `члена совета №${incompleteIndex + 1}`;
|
||||
throw new Error(`Заполните все обязательные поля для ${roleLabel}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const soviet = store.soviet.map(({ id, type, ...rest }) => rest);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
getMinSovietMembersCount,
|
||||
getSovietMembersProgressHint,
|
||||
getSovietContinueBlockedTooltip,
|
||||
isSovietMemberComplete,
|
||||
} from '../../lib';
|
||||
import { notEmpty } from 'src/shared/lib/utils';
|
||||
import { validEmail } from 'src/shared/lib/utils/validEmailRule';
|
||||
@@ -74,11 +75,15 @@ const minSovietMembers = getMinSovietMembersCount();
|
||||
|
||||
installStore.is_finish = false;
|
||||
|
||||
const sovietCount = computed(() => installStore.soviet.length);
|
||||
const canContinue = computed(() => sovietCount.value >= minSovietMembers);
|
||||
const progressHint = computed(() => getSovietMembersProgressHint(sovietCount.value, minSovietMembers));
|
||||
const sovietMembers = computed(() => installStore.soviet);
|
||||
const canContinue = computed(
|
||||
() =>
|
||||
sovietMembers.value.length >= minSovietMembers &&
|
||||
sovietMembers.value.every(isSovietMemberComplete),
|
||||
);
|
||||
const progressHint = computed(() => getSovietMembersProgressHint(sovietMembers.value, minSovietMembers));
|
||||
const continueBlockedTooltip = computed(() =>
|
||||
getSovietContinueBlockedTooltip(sovietCount.value, minSovietMembers),
|
||||
getSovietContinueBlockedTooltip(sovietMembers.value, minSovietMembers),
|
||||
);
|
||||
|
||||
const add = () => {
|
||||
|
||||
Reference in New Issue
Block a user