update
This commit is contained in:
+83
@@ -0,0 +1,83 @@
|
||||
export const DOC_WIZARD_STEP_GENERATOR = 'doc_generator';
|
||||
export const DOC_WIZARD_STEP_BLAGOROST = 'doc_blagorost';
|
||||
|
||||
export const DOC_WIZARD_REGISTRY: Record<string, number> = {
|
||||
[DOC_WIZARD_STEP_GENERATOR]: 994,
|
||||
[DOC_WIZARD_STEP_BLAGOROST]: 998,
|
||||
};
|
||||
|
||||
export const DOC_WIZARD_STEP_ORDER = [DOC_WIZARD_STEP_GENERATOR, DOC_WIZARD_STEP_BLAGOROST] as const;
|
||||
|
||||
export type DocWizardStepKey = (typeof DOC_WIZARD_STEP_ORDER)[number];
|
||||
|
||||
export function isDocWizardStep(key: string): key is DocWizardStepKey {
|
||||
return (DOC_WIZARD_STEP_ORDER as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
/** Группа 1: положение ГЕНЕРАТОР, шаблон договора, положение БЛАГОРОСТ — объявляются параллельно. */
|
||||
export const COUNCIL_GROUP_FOUNDATION = 'council_foundation';
|
||||
/** Группа 2: оферты ГЕНЕРАТОР и БЛАГОРОСТ — объявляются параллельно, каждая после своего положения. */
|
||||
export const COUNCIL_GROUP_OFFERS = 'council_offers';
|
||||
|
||||
/** @deprecated Миграция черновиков stepper → COUNCIL_GROUP_OFFERS */
|
||||
export const COUNCIL_GROUP_GENERATOR_OFFER = 'council_generator_offer';
|
||||
/** @deprecated Миграция черновиков stepper → COUNCIL_GROUP_OFFERS */
|
||||
export const COUNCIL_GROUP_BLAGOROST_OFFER = 'council_blagorost_offer';
|
||||
|
||||
export const COUNCIL_STEP_GROUP_ORDER = [
|
||||
COUNCIL_GROUP_FOUNDATION,
|
||||
COUNCIL_GROUP_OFFERS,
|
||||
] as const;
|
||||
|
||||
export type CouncilGroupStepKey = (typeof COUNCIL_STEP_GROUP_ORDER)[number];
|
||||
|
||||
export const COUNCIL_STEP_IDS_BY_GROUP: Record<CouncilGroupStepKey, readonly string[]> = {
|
||||
[COUNCIL_GROUP_FOUNDATION]: [
|
||||
'generator_program_template',
|
||||
'generation_contract_template',
|
||||
'blagorost_program',
|
||||
],
|
||||
[COUNCIL_GROUP_OFFERS]: ['generator_offer_template', 'blagorost_offer_template'],
|
||||
};
|
||||
|
||||
export const COUNCIL_WIZARD_STEP_META: Record<CouncilGroupStepKey, { label: string; description: string }> = {
|
||||
[COUNCIL_GROUP_FOUNDATION]: {
|
||||
label: 'Утверждение положений и шаблонов ЦПП',
|
||||
description:
|
||||
'Положение «ГЕНЕРАТОР», шаблон договора участия в хозяйственной деятельности и положение «БЛАГОРОСТ»',
|
||||
},
|
||||
[COUNCIL_GROUP_OFFERS]: {
|
||||
label: 'Утверждение оферт ЦПП',
|
||||
description: 'Оферты «ГЕНЕРАТОР» и «БЛАГОРОСТ» для пайщиков',
|
||||
},
|
||||
};
|
||||
|
||||
export function isCouncilGroupStep(key: string): key is CouncilGroupStepKey {
|
||||
return (COUNCIL_STEP_GROUP_ORDER as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
export function resolveCouncilGroupForStepId(stepId: string): CouncilGroupStepKey | null {
|
||||
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
|
||||
if (COUNCIL_STEP_IDS_BY_GROUP[groupKey].includes(stepId)) {
|
||||
return groupKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeWizardStepKey(key: string): string {
|
||||
if (isDocWizardStep(key)) {
|
||||
return key;
|
||||
}
|
||||
if (key === COUNCIL_GROUP_GENERATOR_OFFER || key === COUNCIL_GROUP_BLAGOROST_OFFER) {
|
||||
return COUNCIL_GROUP_OFFERS;
|
||||
}
|
||||
if (isCouncilGroupStep(key)) {
|
||||
return key;
|
||||
}
|
||||
return resolveCouncilGroupForStepId(key) ?? key;
|
||||
}
|
||||
|
||||
export function getCouncilStepIdsForGroup(groupKey: CouncilGroupStepKey): readonly string[] {
|
||||
return COUNCIL_STEP_IDS_BY_GROUP[groupKey];
|
||||
}
|
||||
+18
-12
@@ -13,16 +13,24 @@ export type EditableFieldKey =
|
||||
export const PREVIEW_PLACEHOLDER = '______';
|
||||
|
||||
export const FIELD_LABELS: Record<EditableFieldKey, string> = {
|
||||
generator_program_purpose: 'Направление самореализации после слов «по …»',
|
||||
eoap_definition: 'Определение ЕОАП после слов «а именно:»',
|
||||
generator_task_goal: 'Характеристика ЕОАП после слов «развитие ЕОАП как …»',
|
||||
idea_unit_cost: 'Стоимость единицы идеи (цифрами)',
|
||||
idea_unit_cost_words: 'Стоимость единицы идеи (прописью)',
|
||||
blagorost_goal_expansion: 'Потребность участников в развитии ЕОАП',
|
||||
blagorost_task_expansion: 'Задача расширения участников ЕОАП',
|
||||
blagorost_task_development: 'Задача развития ЕОАП',
|
||||
return_source_description: 'Описание экономического эффекта ЕОАП',
|
||||
return_additional_source: 'Дополнительный источник возврата паевых взносов',
|
||||
generator_program_purpose:
|
||||
'Направление ЦПП: какую экосистему или сферу деятельности развивают пайщики (название, правовая основа, цель интеграции в экономику)',
|
||||
eoap_definition:
|
||||
'Определение Платформы: что это, из чего состоит, кто участники, на какой технологии и принципах работает',
|
||||
generator_task_goal:
|
||||
'Главная задача ЦПП: чем Платформа должна стать для участников (центр привлечения чего и для чего)',
|
||||
idea_unit_cost: 'Стоимость одной Идеи в рублях — только число',
|
||||
idea_unit_cost_words: 'Та же стоимость Идеи прописью',
|
||||
blagorost_goal_expansion:
|
||||
'Зачем расширять участие в Платформе: какая потребность участников закрывается при росте сообщества',
|
||||
blagorost_task_expansion:
|
||||
'Задача расширения: как описывается рост числа участников Платформы и их взаимодействия',
|
||||
blagorost_task_development:
|
||||
'Задача развития: как описывается привлечение инноваций, разработок и решений в Платформу',
|
||||
return_source_description:
|
||||
'Основной источник возврата взносов: что создаёт экономический эффект (инфраструктура, продукт, механизм)',
|
||||
return_additional_source:
|
||||
'Дополнительные источники возврата: взносы пользователей, продукты и приложения, передаваемые или создаваемые в Обществе',
|
||||
};
|
||||
|
||||
export const GENERATOR_DOC_FIELDS: EditableFieldKey[] = [
|
||||
@@ -51,11 +59,9 @@ export const DOCUMENT_SECTIONS = [
|
||||
{
|
||||
registryId: 994,
|
||||
tabLabel: 'Положение ГЕНЕРАТОР',
|
||||
hint: 'Параметры подставляются в положение о программе «ГЕНЕРАТОР». Пустые поля отображаются прочерками — заполните их прямо в тексте документа.',
|
||||
},
|
||||
{
|
||||
registryId: 998,
|
||||
tabLabel: 'Положение БЛАГОРОСТ',
|
||||
hint: 'Параметры подставляются в положение о программе «БЛАГОРОСТ», включая источники возврата. Заполните поля inline в тексте документа.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -15,16 +15,16 @@ interface GeneratedDocument {
|
||||
|
||||
type CapitalOnboardingStepId = Mutations.Capital.CompleteOnboardingStep.IInput['data']['step'];
|
||||
|
||||
const onboardingState = ref<CapitalOnboardingState | null>(null);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const generatingDocument = ref(false);
|
||||
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
|
||||
|
||||
export const useCapitalOnboarding = () => {
|
||||
const systemStore = useSystemStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const onboardingState = ref<CapitalOnboardingState | null>(null);
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const generatingDocument = ref(false);
|
||||
const currentGeneratedDoc = ref<GeneratedDocument | null>(null);
|
||||
|
||||
// Маппинг шагов на registry_id
|
||||
const stepToRegistryId: Record<CapitalOnboardingStepId, number> = {
|
||||
'generator_program_template': 994,
|
||||
@@ -161,6 +161,10 @@ export const useCapitalOnboarding = () => {
|
||||
return stepsConfig.value.every(step => step.status === 'completed');
|
||||
});
|
||||
|
||||
const hasPendingCouncilDecisions = computed(() =>
|
||||
stepsConfig.value.some((step) => step.status === 'in_progress'),
|
||||
);
|
||||
|
||||
const docDataHash = computed(() => onboardingState.value?.capital_program_doc_data_hash || null);
|
||||
const isDocParamsReady = computed(() => Boolean(docDataHash.value));
|
||||
|
||||
@@ -171,18 +175,27 @@ export const useCapitalOnboarding = () => {
|
||||
completionMessage: 'Все необходимые документы для работы с программой утверждены.',
|
||||
}));
|
||||
|
||||
const loadState = async () => {
|
||||
const loadState = async (options?: { silent?: boolean }) => {
|
||||
const silent = options?.silent ?? false;
|
||||
try {
|
||||
loading.value = true;
|
||||
if (!silent) {
|
||||
loading.value = true;
|
||||
}
|
||||
await systemStore.loadSystemInfo();
|
||||
onboardingState.value = await api.loadOnboardingState();
|
||||
} catch (error) {
|
||||
FailAlert(error);
|
||||
if (!silent) {
|
||||
FailAlert(error);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (!silent) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const refreshState = () => loadState({ silent: true });
|
||||
|
||||
const handleStepClick = async (step: ICouncilOnboardingStep) => {
|
||||
try {
|
||||
const doc = await generateDocument(step);
|
||||
@@ -238,9 +251,11 @@ export const useCapitalOnboarding = () => {
|
||||
generatingDocument,
|
||||
currentGeneratedDoc,
|
||||
isOnboardingCompleted,
|
||||
hasPendingCouncilDecisions,
|
||||
docDataHash,
|
||||
isDocParamsReady,
|
||||
loadState,
|
||||
refreshState,
|
||||
handleStepClick,
|
||||
handleStepSubmit,
|
||||
handleDocParamsSaved,
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
export { useCapitalOnboarding } from './composable';
|
||||
export { useCapitalProgramDocParams, getRegistryIdForDocWizardStep } from './useCapitalProgramDocParams';
|
||||
export {
|
||||
DOC_WIZARD_STEP_BLAGOROST,
|
||||
DOC_WIZARD_STEP_GENERATOR,
|
||||
DOC_WIZARD_STEP_ORDER,
|
||||
isDocWizardStep,
|
||||
} from './capitalOnboardingWizard';
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import type { CapitalProgramPrivateData } from 'cooptypes';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { api as documentApi } from 'src/shared/lib/document/api';
|
||||
import { api } from '../api';
|
||||
import {
|
||||
ALL_DOC_FIELDS,
|
||||
BLAGOROST_DOC_FIELDS,
|
||||
FIELD_LABELS,
|
||||
GENERATOR_DOC_FIELDS,
|
||||
PREVIEW_PLACEHOLDER,
|
||||
type EditableFieldKey,
|
||||
} from './capitalProgramDocFields';
|
||||
import {
|
||||
DOC_WIZARD_STEP_BLAGOROST,
|
||||
DOC_WIZARD_STEP_GENERATOR,
|
||||
} from './capitalOnboardingWizard';
|
||||
import {
|
||||
getCapitalProgramDocParamsDraftKey,
|
||||
readCapitalProgramDocParamsDraft,
|
||||
writeCapitalProgramDocParamsDraft,
|
||||
} from './useCapitalProgramDocParamsDraft';
|
||||
|
||||
type PreviewState = {
|
||||
html: string;
|
||||
docDataHash: string;
|
||||
};
|
||||
|
||||
const DOC_FIELDS_BY_REGISTRY: Record<number, EditableFieldKey[]> = {
|
||||
994: GENERATOR_DOC_FIELDS,
|
||||
998: BLAGOROST_DOC_FIELDS,
|
||||
};
|
||||
|
||||
function createEmptyForm(): Record<EditableFieldKey, string> {
|
||||
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
export function useCapitalProgramDocParams(options?: { onSaved?: (hash: string) => void }) {
|
||||
const $q = useQuasar();
|
||||
const systemStore = useSystemStore();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
const saving = ref(false);
|
||||
const savedHash = ref('');
|
||||
const loadingRegistryId = ref<number | null>(null);
|
||||
const wizardStepKey = ref<string>(DOC_WIZARD_STEP_GENERATOR);
|
||||
|
||||
const previews = reactive<Partial<Record<number, PreviewState>>>({});
|
||||
const form = reactive<Record<EditableFieldKey, string>>(createEmptyForm());
|
||||
|
||||
const draftStorageKey = computed(() =>
|
||||
getCapitalProgramDocParamsDraftKey(systemStore.info?.coopname ?? '', sessionStore.username ?? ''),
|
||||
);
|
||||
|
||||
const editableFields = computed(() =>
|
||||
ALL_DOC_FIELDS.map((key) => ({
|
||||
key,
|
||||
label: FIELD_LABELS[key],
|
||||
})),
|
||||
);
|
||||
|
||||
function persistDraftToStorage() {
|
||||
writeCapitalProgramDocParamsDraft(draftStorageKey.value, {
|
||||
form: { ...form },
|
||||
wizardStepKey: wizardStepKey.value,
|
||||
savedHash: savedHash.value,
|
||||
previews: { ...previews },
|
||||
});
|
||||
}
|
||||
|
||||
function restoreDraftFromStorage() {
|
||||
const draft = readCapitalProgramDocParamsDraft(draftStorageKey.value);
|
||||
if (!draft) return;
|
||||
|
||||
Object.assign(form, draft.form);
|
||||
savedHash.value = draft.savedHash;
|
||||
Object.assign(previews, draft.previews);
|
||||
wizardStepKey.value = draft.wizardStepKey ?? DOC_WIZARD_STEP_GENERATOR;
|
||||
}
|
||||
|
||||
function getPreviewPayload(): CapitalProgramPrivateData {
|
||||
return Object.fromEntries(
|
||||
ALL_DOC_FIELDS.map((key) => [key, String(form[key] ?? '').trim() || PREVIEW_PLACEHOLDER]),
|
||||
) as CapitalProgramPrivateData;
|
||||
}
|
||||
|
||||
function getMissingCount(registryId: number): number {
|
||||
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
|
||||
return fields.filter((key) => !String(form[key] ?? '').trim()).length;
|
||||
}
|
||||
|
||||
function validateSection(registryId: number): string | null {
|
||||
const fields = DOC_FIELDS_BY_REGISTRY[registryId] ?? [];
|
||||
const missing = fields
|
||||
.filter((key) => !String(form[key] ?? '').trim())
|
||||
.map((key) => FIELD_LABELS[key]);
|
||||
|
||||
if (missing.length) {
|
||||
return `Заполните все поля документа: ${missing.join(', ')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateAll(): string | null {
|
||||
const missing = editableFields.value
|
||||
.filter(({ key }) => !String(form[key] ?? '').trim())
|
||||
.map(({ label }) => label);
|
||||
|
||||
if (missing.length) {
|
||||
return `Заполните все параметры в документах: ${missing.join(', ')}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDocDataHashFromResult(result: Awaited<ReturnType<typeof documentApi.generateDocument>>): string {
|
||||
const meta = result.meta as { doc_data_hash?: string } | undefined;
|
||||
return meta?.doc_data_hash ?? '';
|
||||
}
|
||||
|
||||
async function loadPreview(registryId: number) {
|
||||
loadingRegistryId.value = registryId;
|
||||
|
||||
try {
|
||||
const result = await documentApi.generateDocument({
|
||||
coopname: systemStore.info?.coopname ?? '',
|
||||
username: sessionStore.username ?? '',
|
||||
registry_id: registryId,
|
||||
doc_data: getPreviewPayload(),
|
||||
});
|
||||
|
||||
previews[registryId] = {
|
||||
html: result.html,
|
||||
docDataHash: getDocDataHashFromResult(result),
|
||||
};
|
||||
persistDraftToStorage();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : 'Не удалось сформировать предпросмотр',
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
loadingRegistryId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveParams() {
|
||||
const validationError = validateAll();
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return false;
|
||||
}
|
||||
|
||||
saving.value = true;
|
||||
|
||||
try {
|
||||
await loadPreview(994);
|
||||
await loadPreview(998);
|
||||
|
||||
const hash = previews[994]?.docDataHash ?? previews[998]?.docDataHash;
|
||||
if (!hash) {
|
||||
throw new Error('Не удалось получить хеш параметров документов');
|
||||
}
|
||||
|
||||
await api.saveProgramDocDataHash(hash);
|
||||
|
||||
savedHash.value = hash;
|
||||
persistDraftToStorage();
|
||||
options?.onSaved?.(hash);
|
||||
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: 'Параметры документов ЦПП сохранены',
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error instanceof Error ? error.message : 'Не удалось сохранить параметры',
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePreview(registryId: number) {
|
||||
if (previews[registryId]?.html) return;
|
||||
await loadPreview(registryId);
|
||||
}
|
||||
|
||||
function setWizardStepKey(key: string) {
|
||||
wizardStepKey.value = key;
|
||||
persistDraftToStorage();
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
previews,
|
||||
savedHash,
|
||||
saving,
|
||||
loadingRegistryId,
|
||||
wizardStepKey,
|
||||
restoreDraftFromStorage,
|
||||
persistDraftToStorage,
|
||||
getMissingCount,
|
||||
validateSection,
|
||||
loadPreview,
|
||||
ensurePreview,
|
||||
saveParams,
|
||||
setWizardStepKey,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRegistryIdForDocWizardStep(stepKey: string): number | null {
|
||||
if (stepKey === DOC_WIZARD_STEP_GENERATOR) return 994;
|
||||
if (stepKey === DOC_WIZARD_STEP_BLAGOROST) return 998;
|
||||
return null;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { ALL_DOC_FIELDS, type EditableFieldKey } from './capitalProgramDocFields';
|
||||
|
||||
const STORAGE_PREFIX = 'capital-cpp-doc-params-draft';
|
||||
|
||||
export type CapitalProgramDocParamsPreviewState = {
|
||||
html: string;
|
||||
docDataHash: string;
|
||||
};
|
||||
|
||||
export type CapitalProgramDocParamsDraft = {
|
||||
form: Record<EditableFieldKey, string>;
|
||||
/** @deprecated используйте wizardStepKey */
|
||||
activeTab?: number;
|
||||
wizardStepKey?: string;
|
||||
savedHash: string;
|
||||
previews: Partial<Record<number, CapitalProgramDocParamsPreviewState>>;
|
||||
};
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
||||
}
|
||||
|
||||
export function getCapitalProgramDocParamsDraftKey(coopname: string, username: string): string {
|
||||
return `${STORAGE_PREFIX}:${coopname || 'unknown'}:${username || 'unknown'}`;
|
||||
}
|
||||
|
||||
function isEditableFieldKey(key: string): key is EditableFieldKey {
|
||||
return (ALL_DOC_FIELDS as readonly string[]).includes(key);
|
||||
}
|
||||
|
||||
function createEmptyForm(): Record<EditableFieldKey, string> {
|
||||
return Object.fromEntries(ALL_DOC_FIELDS.map((key) => [key, ''])) as Record<EditableFieldKey, string>;
|
||||
}
|
||||
|
||||
function normalizeForm(raw: unknown): Record<EditableFieldKey, string> | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
const form = createEmptyForm();
|
||||
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
||||
if (isEditableFieldKey(key) && typeof value === 'string') {
|
||||
form[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
function normalizePreviews(raw: unknown): Partial<Record<number, CapitalProgramDocParamsPreviewState>> {
|
||||
const previews: Partial<Record<number, CapitalProgramDocParamsPreviewState>> = {};
|
||||
if (!raw || typeof raw !== 'object') return previews;
|
||||
|
||||
for (const [id, preview] of Object.entries(raw as Record<string, unknown>)) {
|
||||
const registryId = Number(id);
|
||||
if (!Number.isFinite(registryId) || !preview || typeof preview !== 'object') continue;
|
||||
|
||||
const html = (preview as CapitalProgramDocParamsPreviewState).html;
|
||||
const docDataHash = (preview as CapitalProgramDocParamsPreviewState).docDataHash;
|
||||
if (typeof html === 'string' && typeof docDataHash === 'string') {
|
||||
previews[registryId] = { html, docDataHash };
|
||||
}
|
||||
}
|
||||
|
||||
return previews;
|
||||
}
|
||||
|
||||
export function readCapitalProgramDocParamsDraft(storageKey: string): CapitalProgramDocParamsDraft | null {
|
||||
if (!isBrowser()) return null;
|
||||
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey);
|
||||
if (!raw) return null;
|
||||
|
||||
const parsed = JSON.parse(raw) as Partial<CapitalProgramDocParamsDraft>;
|
||||
const form = normalizeForm(parsed.form);
|
||||
if (!form) return null;
|
||||
|
||||
const legacyTab = typeof parsed.activeTab === 'number' ? parsed.activeTab : 994;
|
||||
const wizardStepKey =
|
||||
typeof parsed.wizardStepKey === 'string'
|
||||
? parsed.wizardStepKey
|
||||
: legacyTab === 998
|
||||
? 'doc_blagorost'
|
||||
: 'doc_generator';
|
||||
|
||||
return {
|
||||
form,
|
||||
wizardStepKey,
|
||||
savedHash: typeof parsed.savedHash === 'string' ? parsed.savedHash : '',
|
||||
previews: normalizePreviews(parsed.previews),
|
||||
};
|
||||
} catch {
|
||||
localStorage.removeItem(storageKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeCapitalProgramDocParamsDraft(storageKey: string, draft: CapitalProgramDocParamsDraft): void {
|
||||
if (!isBrowser()) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(draft));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist capital CPP doc params draft:', error);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<template lang="pug">
|
||||
.capital-council-group
|
||||
.capital-council-group__item(
|
||||
v-for='(step, index) in steps'
|
||||
:key='step.id'
|
||||
)
|
||||
.capital-council-group__head
|
||||
.capital-council-group__title
|
||||
span.capital-council-group__index {{ index + 1 }}.
|
||||
span {{ step.title }}
|
||||
.capital-council-group__status
|
||||
BaseChip(
|
||||
v-if='step.status === "completed"'
|
||||
variant='pos'
|
||||
size='sm'
|
||||
) Утверждено советом
|
||||
BaseChip(
|
||||
v-else-if='step.status === "in_progress"'
|
||||
variant='warn'
|
||||
size='sm'
|
||||
) Ожидаем решение совета
|
||||
|
||||
p.capital-council-group__desc.t-body2 {{ step.description }}
|
||||
|
||||
BaseButton.capital-council-group__action(
|
||||
v-if='canDeclare(step)'
|
||||
variant='primary'
|
||||
size='sm'
|
||||
:disable='busy'
|
||||
@click='emit("declare", step.id)'
|
||||
) Объявить собрание совета
|
||||
|
||||
q-separator.capital-council-group__sep(v-if='index < steps.length - 1')
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseButton, BaseChip } from 'src/shared/ui/base';
|
||||
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
|
||||
|
||||
defineProps<{
|
||||
steps: ICouncilOnboardingStep[];
|
||||
busy?: boolean;
|
||||
canDeclare: (step: ICouncilOnboardingStep) => boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
declare: [stepId: string];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-council-group__item {
|
||||
padding: var(--p-2) 0;
|
||||
}
|
||||
|
||||
.capital-council-group__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3);
|
||||
}
|
||||
|
||||
.capital-council-group__title {
|
||||
display: flex;
|
||||
gap: var(--p-2);
|
||||
font-weight: 600;
|
||||
color: var(--p-ink);
|
||||
}
|
||||
|
||||
.capital-council-group__index {
|
||||
flex: 0 0 auto;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.capital-council-group__status {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.capital-council-group__desc {
|
||||
margin: var(--p-2) 0 0;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
|
||||
.capital-council-group__action {
|
||||
margin-top: var(--p-3);
|
||||
}
|
||||
|
||||
.capital-council-group__sep {
|
||||
margin-top: var(--p-4);
|
||||
}
|
||||
</style>
|
||||
+436
-111
@@ -1,98 +1,124 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
div.capital-onboarding.q-pa-md
|
||||
WindowLoader(
|
||||
v-if="loading"
|
||||
text="Загрузка данных онбординга..."
|
||||
v-if='loading'
|
||||
text='Загрузка данных онбординга...'
|
||||
)
|
||||
template(v-else-if="!isOnboardingCompleted")
|
||||
CapitalProgramDocumentParametersWidget(
|
||||
:doc-data-hash='docDataHash'
|
||||
@saved='handleDocParamsSaved'
|
||||
)
|
||||
template(v-else-if='!isOnboardingCompleted')
|
||||
.capital-onboarding__shell
|
||||
BaseBanner.capital-onboarding__banner(variant='info')
|
||||
template(#icon)
|
||||
q-icon(name='info' size='20px')
|
||||
| Сначала заполните параметры положений «ГЕНЕРАТОР» и «БЛАГОРОСТ», затем объявите собрания
|
||||
| совета для утверждения документов ЦПП.
|
||||
|
||||
q-card(v-if='isDocParamsReady' flat class='q-mt-md')
|
||||
q-card-section.row.items-center.justify-between
|
||||
div
|
||||
div.text-h5 Адаптация к работе с программой "Благорост"
|
||||
|
||||
q-separator
|
||||
q-card-section
|
||||
q-list(separator)
|
||||
q-item(v-for="(step, index) in config?.steps || []" :key="step.id")
|
||||
q-item-section
|
||||
div.row.items-center.q-gutter-sm
|
||||
q-icon(
|
||||
:name="getIcon(step)"
|
||||
:color="getIconColor(step)"
|
||||
size="22px"
|
||||
)
|
||||
div.text-subtitle1 {{ index + 1 }}. {{ step.title }}
|
||||
div.text-caption.text-grey-7 {{ step.description }}
|
||||
q-chip(
|
||||
v-if="step.status === 'in_progress'"
|
||||
dense
|
||||
color="amber"
|
||||
text-color="black"
|
||||
icon="hourglass_top"
|
||||
class="q-mt-xs"
|
||||
) Ожидаем решение совета
|
||||
q-item-section(side)
|
||||
q-btn(
|
||||
v-if="showAction(index)"
|
||||
:disable="submitting || generatingDocument"
|
||||
:loading="generatingDocument && currentStepId === step.id"
|
||||
color="primary"
|
||||
label="Объявить собрание совета"
|
||||
@click="() => openDialog(step)"
|
||||
)
|
||||
|
||||
BaseDialog(
|
||||
v-model="dialogOpen",
|
||||
title="Предложение повестки",
|
||||
size="lg",
|
||||
:close-on-backdrop="false",
|
||||
:close-on-escape="false",
|
||||
@update:model-value="(v) => !v && closeDialog()"
|
||||
)
|
||||
Loader(
|
||||
v-if="generatingDocument"
|
||||
text="Генерация документа..."
|
||||
VerticalStepper.capital-onboarding__stepper(
|
||||
:steps='wizardSteps'
|
||||
:active-key='activeWizardStep'
|
||||
:completed='completedWizardSteps'
|
||||
:clickable-completed='true'
|
||||
@change='onStepperChange'
|
||||
)
|
||||
q-card-section(v-else-if="currentStep && currentGeneratedDoc && !generatingDocument")
|
||||
template(#active='{ step }')
|
||||
CapitalProgramDocumentStepPanel(
|
||||
v-if='isDocWizardStep(step.key)'
|
||||
v-model='form'
|
||||
:preview-html='getDocPreviewHtml(step.key)'
|
||||
:loading='isDocStepLoading(step.key)'
|
||||
@refresh='refreshDocStep(step.key)'
|
||||
)
|
||||
|
||||
CapitalCouncilStepGroup(
|
||||
v-else-if='isCouncilGroupStep(step.key)'
|
||||
:steps='getCouncilStepsForGroup(step.key)'
|
||||
:busy='submitting || generatingDocument'
|
||||
:can-declare='canDeclareCouncilStep'
|
||||
@declare='openCouncilDialog'
|
||||
)
|
||||
|
||||
.capital-onboarding__footer(v-if='showFooter')
|
||||
BaseButton(
|
||||
v-if='showBackButton'
|
||||
variant='ghost'
|
||||
:disable='footerBusy'
|
||||
@click='goBack'
|
||||
) Назад
|
||||
q-space(v-if='showBackButton && showContinueButton')
|
||||
BaseButton(
|
||||
v-if='showContinueButton'
|
||||
variant='primary'
|
||||
:class='{ "capital-onboarding__continue--solo": !showBackButton }'
|
||||
:loading='footerBusy'
|
||||
@click='goNext'
|
||||
) {{ continueLabel }}
|
||||
|
||||
BaseDialog(
|
||||
v-model='dialogOpen'
|
||||
title='Предложение повестки'
|
||||
size='lg'
|
||||
:close-on-backdrop='false'
|
||||
:close-on-escape='false'
|
||||
@update:model-value='(v) => !v && closeDialog()'
|
||||
)
|
||||
Loader(
|
||||
v-if='generatingDocument'
|
||||
text='Генерация документа...'
|
||||
)
|
||||
template(v-else-if='currentStep && currentGeneratedDoc && !generatingDocument')
|
||||
.capital-onboarding__dialog-section
|
||||
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
|
||||
q-icon(name="help_outline" size="18px" class="text-primary")
|
||||
q-icon(name='help_outline' size='18px' color='primary')
|
||||
span Вопрос на повестке
|
||||
div.q-mt-sm.q-pa-sm.text-body1.rounded-borders {{ currentStep.question }}
|
||||
div.q-mt-sm.t-body {{ currentStep.question }}
|
||||
|
||||
q-separator.q-my-md
|
||||
|
||||
div.row.items-center.q-gutter-xs.text-subtitle1.text-weight-medium
|
||||
q-icon(name="gavel" size="18px" class="text-primary")
|
||||
q-icon(name='gavel' size='18px' color='primary')
|
||||
span Проект решения
|
||||
div.q-mt-sm.q-pa-sm.rounded-borders
|
||||
div(v-if="currentStep.decisionPrefix") {{ currentStep.decisionPrefix }}
|
||||
DocumentHtmlReader(:html="currentGeneratedDoc.html" :sanitize="false")
|
||||
div.q-mt-sm.text-caption.text-grey-6
|
||||
div.q-mt-sm
|
||||
div(v-if='currentStep.decisionPrefix') {{ currentStep.decisionPrefix }}
|
||||
DocumentHtmlReader(:html='currentGeneratedDoc.html' :sanitize='false')
|
||||
div.t-caption.text-grey-6.q-mt-sm
|
||||
strong {{ currentGeneratedDoc.full_title }}
|
||||
|
||||
div.q-pb-lg(v-if="currentGeneratedDoc && !generatingDocument")
|
||||
q-btn(flat label="Отмена" @click="closeDialog" :disable="submitting")
|
||||
q-btn(
|
||||
color="primary"
|
||||
label="Объявить"
|
||||
:loading="submitting"
|
||||
@click="submitStep"
|
||||
)
|
||||
template(#footer v-if='currentGeneratedDoc && !generatingDocument')
|
||||
BaseButton(variant='ghost' :disable='submitting' @click='closeDialog') Отмена
|
||||
BaseButton(variant='primary' :loading='submitting' @click='submitStep') Объявить
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { DocumentHtmlReader } from 'src/shared/ui/DocumentHtmlReader';
|
||||
import { BaseBanner, BaseButton } from 'src/shared/ui/base';
|
||||
import { BaseDialog } from 'src/shared/ui/base/BaseDialog';
|
||||
import { WindowLoader, Loader } from 'src/shared/ui/Loader';
|
||||
import { VerticalStepper } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import type { StepperStep } from 'src/shared/ui/domain/VerticalStepper';
|
||||
import type { ICouncilOnboardingStep } from 'src/shared/ui/CouncilOnboarding';
|
||||
import CapitalProgramDocumentParametersWidget from './CapitalProgramDocumentParametersWidget.vue';
|
||||
import { useDataPoller } from 'src/shared/lib/composables';
|
||||
import { POLL_INTERVALS } from 'src/shared/lib/consts';
|
||||
import CapitalProgramDocumentStepPanel from './CapitalProgramDocumentStepPanel.vue';
|
||||
import CapitalCouncilStepGroup from './CapitalCouncilStepGroup.vue';
|
||||
import { useCapitalOnboarding } from '../model';
|
||||
import { useCapitalProgramDocParams, getRegistryIdForDocWizardStep } from '../model/useCapitalProgramDocParams';
|
||||
import {
|
||||
COUNCIL_GROUP_FOUNDATION,
|
||||
COUNCIL_GROUP_OFFERS,
|
||||
COUNCIL_STEP_GROUP_ORDER,
|
||||
COUNCIL_WIZARD_STEP_META,
|
||||
DOC_WIZARD_STEP_BLAGOROST,
|
||||
DOC_WIZARD_STEP_GENERATOR,
|
||||
DOC_WIZARD_STEP_ORDER,
|
||||
getCouncilStepIdsForGroup,
|
||||
isCouncilGroupStep,
|
||||
isDocWizardStep,
|
||||
normalizeWizardStepKey,
|
||||
type CouncilGroupStepKey,
|
||||
} from '../model/capitalOnboardingWizard';
|
||||
|
||||
const $q = useQuasar();
|
||||
|
||||
const {
|
||||
config,
|
||||
@@ -101,70 +127,369 @@ const {
|
||||
generatingDocument,
|
||||
currentGeneratedDoc,
|
||||
isOnboardingCompleted,
|
||||
docDataHash,
|
||||
hasPendingCouncilDecisions,
|
||||
isDocParamsReady,
|
||||
loadState,
|
||||
refreshState,
|
||||
handleStepClick,
|
||||
handleStepSubmit,
|
||||
handleDocParamsSaved,
|
||||
} = useCapitalOnboarding();
|
||||
|
||||
const {
|
||||
form,
|
||||
previews,
|
||||
saving,
|
||||
loadingRegistryId,
|
||||
wizardStepKey,
|
||||
restoreDraftFromStorage,
|
||||
persistDraftToStorage,
|
||||
validateSection,
|
||||
loadPreview,
|
||||
ensurePreview,
|
||||
saveParams,
|
||||
setWizardStepKey,
|
||||
} = useCapitalProgramDocParams({
|
||||
onSaved: () => {
|
||||
void handleDocParamsSaved();
|
||||
},
|
||||
});
|
||||
|
||||
const activeWizardStep = wizardStepKey;
|
||||
const advancing = ref(false);
|
||||
const dialogOpen = ref(false);
|
||||
const currentStep = ref<ICouncilOnboardingStep | null>(null);
|
||||
const currentStepId = ref<string | null>(null);
|
||||
|
||||
const councilSteps = computed(() => config.value?.steps ?? []);
|
||||
|
||||
const getIcon = (step: ICouncilOnboardingStep) => {
|
||||
if (step.status === 'completed') return 'task_alt';
|
||||
if (step.status === 'in_progress') return 'hourglass_top';
|
||||
return 'radio_button_unchecked';
|
||||
};
|
||||
const wizardSteps = computed<StepperStep[]>(() => [
|
||||
{
|
||||
key: DOC_WIZARD_STEP_GENERATOR,
|
||||
label: 'Параметры «ГЕНЕРАТОР»',
|
||||
description: 'Переменные положения о целевой программе «ГЕНЕРАТОР»',
|
||||
},
|
||||
{
|
||||
key: DOC_WIZARD_STEP_BLAGOROST,
|
||||
label: 'Параметры «БЛАГОРОСТ»',
|
||||
description: 'Переменные положения о целевой программе «БЛАГОРОСТ»',
|
||||
},
|
||||
...COUNCIL_STEP_GROUP_ORDER.map((groupKey) => ({
|
||||
key: groupKey,
|
||||
label: COUNCIL_WIZARD_STEP_META[groupKey].label,
|
||||
description: COUNCIL_WIZARD_STEP_META[groupKey].description,
|
||||
disabled: isCouncilGroupDisabled(groupKey),
|
||||
})),
|
||||
]);
|
||||
|
||||
const getIconColor = (step: ICouncilOnboardingStep) => {
|
||||
if (step.status === 'completed') return 'green-6';
|
||||
if (step.status === 'in_progress') return 'orange-6';
|
||||
return 'grey-6';
|
||||
};
|
||||
const wizardStepOrder = computed(() => [
|
||||
...DOC_WIZARD_STEP_ORDER,
|
||||
...COUNCIL_STEP_GROUP_ORDER,
|
||||
]);
|
||||
|
||||
const isDependenciesCompleted = (step: ICouncilOnboardingStep) => {
|
||||
if (!step.depends_on || step.depends_on.length === 0) return true;
|
||||
if (!config.value?.steps) return true;
|
||||
const completedWizardSteps = computed(() => {
|
||||
const completed: string[] = [];
|
||||
const activeIndex = wizardStepOrder.value.indexOf(activeWizardStep.value);
|
||||
|
||||
return step.depends_on.every(depId => {
|
||||
const depStep = config.value.steps.find(s => s.id === depId);
|
||||
return depStep && depStep.status === 'completed';
|
||||
});
|
||||
};
|
||||
if (activeIndex > 0 || isDocParamsReady.value) {
|
||||
completed.push(DOC_WIZARD_STEP_GENERATOR);
|
||||
}
|
||||
if (isDocParamsReady.value) {
|
||||
completed.push(DOC_WIZARD_STEP_BLAGOROST);
|
||||
}
|
||||
|
||||
const showAction = (index: number) => {
|
||||
if (!config.value?.steps) return false;
|
||||
const step = config.value.steps[index];
|
||||
if (!step) return false;
|
||||
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
|
||||
if (isCouncilGroupCompleted(groupKey)) {
|
||||
completed.push(groupKey);
|
||||
}
|
||||
}
|
||||
|
||||
return completed;
|
||||
});
|
||||
|
||||
const footerBusy = computed(() => saving.value || advancing.value || !!loadingRegistryId.value);
|
||||
|
||||
const showBackButton = computed(() => {
|
||||
const index = wizardStepOrder.value.indexOf(activeWizardStep.value);
|
||||
return index > 0;
|
||||
});
|
||||
|
||||
const showContinueButton = computed(() => isDocWizardStep(activeWizardStep.value));
|
||||
|
||||
const showFooter = computed(() => showBackButton.value || showContinueButton.value);
|
||||
|
||||
const continueLabel = computed(() =>
|
||||
activeWizardStep.value === DOC_WIZARD_STEP_BLAGOROST ? 'Сохранить и продолжить' : 'Продолжить',
|
||||
);
|
||||
|
||||
function getCouncilStep(stepId: string): ICouncilOnboardingStep | undefined {
|
||||
return councilSteps.value.find((step) => step.id === stepId);
|
||||
}
|
||||
|
||||
function getCouncilStepsForGroup(groupKey: CouncilGroupStepKey): ICouncilOnboardingStep[] {
|
||||
return getCouncilStepIdsForGroup(groupKey)
|
||||
.map((stepId) => getCouncilStep(stepId))
|
||||
.filter((step): step is ICouncilOnboardingStep => step != null);
|
||||
}
|
||||
|
||||
function isCouncilGroupCompleted(groupKey: CouncilGroupStepKey): boolean {
|
||||
return getCouncilStepsForGroup(groupKey).every((step) => step.status === 'completed');
|
||||
}
|
||||
|
||||
function isCouncilGroupDisabled(groupKey: CouncilGroupStepKey): boolean {
|
||||
if (groupKey === COUNCIL_GROUP_FOUNDATION) {
|
||||
return !isDocParamsReady.value;
|
||||
}
|
||||
if (groupKey === COUNCIL_GROUP_OFFERS) {
|
||||
return getCouncilStepsForGroup(groupKey).every(
|
||||
(step) =>
|
||||
step.status !== 'completed' &&
|
||||
step.status !== 'in_progress' &&
|
||||
!canDeclareCouncilStep(step),
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function getFirstAvailableCouncilGroup(): CouncilGroupStepKey | null {
|
||||
for (const groupKey of COUNCIL_STEP_GROUP_ORDER) {
|
||||
if (!isCouncilGroupCompleted(groupKey) && !isCouncilGroupDisabled(groupKey)) {
|
||||
return groupKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncWizardStepWithCouncilProgress() {
|
||||
if (!isDocParamsReady.value || isOnboardingCompleted.value) return;
|
||||
|
||||
const firstAvailable = getFirstAvailableCouncilGroup();
|
||||
if (!firstAvailable) return;
|
||||
|
||||
const current = activeWizardStep.value;
|
||||
|
||||
if (isDocWizardStep(current)) {
|
||||
setWizardStepKey(firstAvailable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isCouncilGroupStep(current)) return;
|
||||
|
||||
if (isCouncilGroupCompleted(current) && firstAvailable !== current) {
|
||||
setWizardStepKey(firstAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
function getDocPreviewHtml(stepKey: string): string {
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
return registryId ? previews[registryId]?.html ?? '' : '';
|
||||
}
|
||||
|
||||
function isDocStepLoading(stepKey: string): boolean {
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
return registryId != null && loadingRegistryId.value === registryId;
|
||||
}
|
||||
|
||||
async function refreshDocStep(stepKey: string) {
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
if (registryId == null) return;
|
||||
await loadPreview(registryId);
|
||||
}
|
||||
|
||||
function canDeclareCouncilStep(step: ICouncilOnboardingStep): boolean {
|
||||
if (step.status === 'completed' || step.status === 'in_progress') return false;
|
||||
return isDependenciesCompleted(step);
|
||||
};
|
||||
if (!step.depends_on?.length) return true;
|
||||
return step.depends_on.every((depId) => getCouncilStep(depId)?.status === 'completed');
|
||||
}
|
||||
|
||||
function onStepperChange(key: string) {
|
||||
if (isDocWizardStep(key)) {
|
||||
setWizardStepKey(key);
|
||||
return;
|
||||
}
|
||||
if (isCouncilGroupStep(key) && !isCouncilGroupDisabled(key)) {
|
||||
setWizardStepKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
async function goNext() {
|
||||
if (activeWizardStep.value === DOC_WIZARD_STEP_GENERATOR) {
|
||||
const validationError = validateSection(994);
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return;
|
||||
}
|
||||
|
||||
advancing.value = true;
|
||||
try {
|
||||
await ensurePreview(994);
|
||||
setWizardStepKey(DOC_WIZARD_STEP_BLAGOROST);
|
||||
} finally {
|
||||
advancing.value = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeWizardStep.value === DOC_WIZARD_STEP_BLAGOROST) {
|
||||
const validationError = validateSection(998);
|
||||
if (validationError) {
|
||||
$q.notify({ type: 'warning', message: validationError });
|
||||
return;
|
||||
}
|
||||
|
||||
advancing.value = true;
|
||||
try {
|
||||
const saved = await saveParams();
|
||||
if (!saved) return;
|
||||
|
||||
const firstCouncilGroup = getFirstAvailableCouncilGroup();
|
||||
if (firstCouncilGroup) {
|
||||
setWizardStepKey(firstCouncilGroup);
|
||||
}
|
||||
} finally {
|
||||
advancing.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const index = wizardStepOrder.value.indexOf(activeWizardStep.value);
|
||||
if (index <= 0) return;
|
||||
setWizardStepKey(wizardStepOrder.value[index - 1]);
|
||||
}
|
||||
|
||||
async function openCouncilDialog(stepId: string) {
|
||||
const step = getCouncilStep(stepId);
|
||||
if (!step || !canDeclareCouncilStep(step)) return;
|
||||
|
||||
const openDialog = async (step: ICouncilOnboardingStep) => {
|
||||
currentStep.value = step;
|
||||
currentStepId.value = step.id;
|
||||
dialogOpen.value = true;
|
||||
|
||||
// Генерируем документ
|
||||
await handleStepClick(step);
|
||||
};
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
function closeDialog() {
|
||||
dialogOpen.value = false;
|
||||
currentStep.value = null;
|
||||
currentStepId.value = null;
|
||||
};
|
||||
}
|
||||
|
||||
const submitStep = async () => {
|
||||
async function submitStep() {
|
||||
if (!currentStep.value) return;
|
||||
|
||||
await handleStepSubmit(currentStep.value);
|
||||
closeDialog();
|
||||
};
|
||||
syncWizardStepWithCouncilProgress();
|
||||
}
|
||||
|
||||
onMounted(loadState);
|
||||
const shouldPollCouncilState = computed(
|
||||
() => hasPendingCouncilDecisions.value && !isOnboardingCompleted.value,
|
||||
);
|
||||
|
||||
const { start: startCouncilPoll, stop: stopCouncilPoll } = useDataPoller(
|
||||
async () => {
|
||||
await refreshState();
|
||||
syncWizardStepWithCouncilProgress();
|
||||
},
|
||||
{
|
||||
interval: POLL_INTERVALS.FAST,
|
||||
immediate: false,
|
||||
enabled: shouldPollCouncilState,
|
||||
},
|
||||
);
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.hidden || !shouldPollCouncilState.value) return;
|
||||
void refreshState().then(() => syncWizardStepWithCouncilProgress());
|
||||
}
|
||||
|
||||
watch(shouldPollCouncilState, (enabled) => {
|
||||
if (enabled) {
|
||||
startCouncilPoll();
|
||||
} else {
|
||||
stopCouncilPoll();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => councilSteps.value.map((step) => `${step.id}:${step.status}`).join('|'),
|
||||
() => {
|
||||
syncWizardStepWithCouncilProgress();
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => activeWizardStep.value,
|
||||
(stepKey) => {
|
||||
if (!isDocWizardStep(stepKey)) return;
|
||||
const registryId = getRegistryIdForDocWizardStep(stepKey);
|
||||
if (registryId != null) {
|
||||
void ensurePreview(registryId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
form,
|
||||
() => {
|
||||
persistDraftToStorage();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadState();
|
||||
restoreDraftFromStorage();
|
||||
setWizardStepKey(normalizeWizardStepKey(activeWizardStep.value));
|
||||
syncWizardStepWithCouncilProgress();
|
||||
|
||||
if (shouldPollCouncilState.value) {
|
||||
startCouncilPoll();
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopCouncilPoll();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-onboarding {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.capital-onboarding__shell {
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.capital-onboarding__banner {
|
||||
margin-bottom: var(--p-4);
|
||||
}
|
||||
|
||||
.capital-onboarding__stepper {
|
||||
margin-top: var(--p-2);
|
||||
}
|
||||
|
||||
.capital-onboarding__continue--solo {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.capital-onboarding__dialog-section {
|
||||
padding: var(--p-1) 0;
|
||||
}
|
||||
|
||||
.capital-onboarding__footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3);
|
||||
margin-top: var(--p-5);
|
||||
padding: var(--p-4) 0;
|
||||
background: var(--p-canvas);
|
||||
border-top: 1px solid var(--p-line);
|
||||
}
|
||||
</style>
|
||||
|
||||
+48
-8
@@ -2,10 +2,6 @@
|
||||
BaseCard.capital-doc-params(title='Параметры документов ЦПП')
|
||||
p.t-body-sm.capital-doc-params__intro
|
||||
| Заполните параметры положений «ГЕНЕРАТОР» и «БЛАГОРОСТ» прямо в тексте документа.
|
||||
| Пустые поля подсвечиваются оранжевым — подсвеченные области можно редактировать inline.
|
||||
|
||||
BaseBanner(v-if='savedHash' type='success' class='q-mb-md')
|
||||
| Параметры сохранены. Хеш: {{ savedHash }}
|
||||
|
||||
q-tabs(v-model='activeTab' dense align='left' class='capital-doc-params__tabs')
|
||||
q-tab(
|
||||
@@ -22,8 +18,6 @@ BaseCard.capital-doc-params(title='Параметры документов ЦП
|
||||
:key='section.registryId'
|
||||
:name='section.registryId'
|
||||
)
|
||||
p.t-caption.text-grey-7.q-mb-md {{ section.hint }}
|
||||
|
||||
.capital-doc-params__toolbar
|
||||
BaseButton(
|
||||
color='primary'
|
||||
@@ -64,7 +58,7 @@ import { useQuasar } from 'quasar';
|
||||
import type { CapitalProgramPrivateData } from 'cooptypes';
|
||||
import { useSystemStore } from 'src/entities/System/model';
|
||||
import { useSessionStore } from 'src/entities/Session';
|
||||
import { BaseBanner, BaseButton, BaseCard } from 'src/shared/ui/base';
|
||||
import { BaseButton, BaseCard } from 'src/shared/ui/base';
|
||||
import { api as documentApi } from 'src/shared/lib/document/api';
|
||||
import { api } from '../api';
|
||||
import CapitalProgramInlineDocumentPreview from './CapitalProgramInlineDocumentPreview.vue';
|
||||
@@ -77,6 +71,11 @@ import {
|
||||
PREVIEW_PLACEHOLDER,
|
||||
type EditableFieldKey,
|
||||
} from '../model/capitalProgramDocFields';
|
||||
import {
|
||||
getCapitalProgramDocParamsDraftKey,
|
||||
readCapitalProgramDocParamsDraft,
|
||||
writeCapitalProgramDocParamsDraft,
|
||||
} from '../model/useCapitalProgramDocParamsDraft';
|
||||
|
||||
const emit = defineEmits<{
|
||||
saved: [hash: string];
|
||||
@@ -109,6 +108,29 @@ function createEmptyForm(): Record<EditableFieldKey, string> {
|
||||
|
||||
const form = reactive<Record<EditableFieldKey, string>>(createEmptyForm());
|
||||
|
||||
const draftStorageKey = computed(() =>
|
||||
getCapitalProgramDocParamsDraftKey(systemStore.info?.coopname ?? '', sessionStore.username ?? ''),
|
||||
);
|
||||
|
||||
function restoreDraftFromStorage() {
|
||||
const draft = readCapitalProgramDocParamsDraft(draftStorageKey.value);
|
||||
if (!draft) return;
|
||||
|
||||
Object.assign(form, draft.form);
|
||||
savedHash.value = draft.savedHash;
|
||||
Object.assign(previews, draft.previews);
|
||||
activeTab.value = draft.activeTab;
|
||||
}
|
||||
|
||||
function persistDraftToStorage() {
|
||||
writeCapitalProgramDocParamsDraft(draftStorageKey.value, {
|
||||
form: { ...form },
|
||||
activeTab: activeTab.value,
|
||||
savedHash: savedHash.value,
|
||||
previews: { ...previews },
|
||||
});
|
||||
}
|
||||
|
||||
const editableFields = computed(() =>
|
||||
ALL_DOC_FIELDS.map((key) => ({
|
||||
key,
|
||||
@@ -165,6 +187,7 @@ async function loadPreview(registryId: number, options?: { silent?: boolean }) {
|
||||
html: result.html,
|
||||
docDataHash: getDocDataHashFromResult(result),
|
||||
};
|
||||
persistDraftToStorage();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
@@ -221,13 +244,30 @@ async function saveParams() {
|
||||
}
|
||||
|
||||
watch(activeTab, (registryId) => {
|
||||
persistDraftToStorage();
|
||||
if (!previews[registryId]) {
|
||||
void loadPreview(registryId);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
form,
|
||||
() => {
|
||||
persistDraftToStorage();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(savedHash, () => {
|
||||
persistDraftToStorage();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadPreview(activeTab.value);
|
||||
restoreDraftFromStorage();
|
||||
|
||||
if (!previews[activeTab.value]) {
|
||||
void loadPreview(activeTab.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<template lang="pug">
|
||||
.capital-doc-step
|
||||
.capital-doc-step__preview-shell
|
||||
.capital-doc-step__toolbar
|
||||
BaseButton(
|
||||
variant='ghost'
|
||||
size='sm'
|
||||
:disable='loading'
|
||||
@click='emit("refresh")'
|
||||
)
|
||||
q-icon(name='refresh' size='18px')
|
||||
span.q-ml-xs {{ previewHtml ? 'Обновить предпросмотр' : 'Сформировать предпросмотр' }}
|
||||
|
||||
CapitalProgramInlineDocumentPreview(
|
||||
v-if='previewHtml && !loading'
|
||||
v-model='form'
|
||||
:html='previewHtml'
|
||||
:field-labels='FIELD_LABELS'
|
||||
)
|
||||
|
||||
.capital-doc-step__loading(v-else-if='loading')
|
||||
q-spinner(size='36px' color='primary')
|
||||
p.t-body-sm.capital-doc-step__loading-text Формируем предпросмотр документа…
|
||||
|
||||
EmptyState(
|
||||
v-else
|
||||
title='Предпросмотр ещё не сформирован'
|
||||
body='Нажмите «Сформировать предпросмотр», чтобы увидеть документ с редактируемыми полями.'
|
||||
)
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BaseButton, EmptyState } from 'src/shared/ui/base';
|
||||
import CapitalProgramInlineDocumentPreview from './CapitalProgramInlineDocumentPreview.vue';
|
||||
import { FIELD_LABELS, type EditableFieldKey } from '../model/capitalProgramDocFields';
|
||||
|
||||
defineProps<{
|
||||
previewHtml: string;
|
||||
loading: boolean;
|
||||
}>();
|
||||
|
||||
const form = defineModel<Record<EditableFieldKey, string>>({ required: true });
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.capital-doc-step__preview-shell {
|
||||
border: 1px solid var(--p-line);
|
||||
border-radius: var(--p-r-md);
|
||||
background: var(--p-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.capital-doc-step__toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--p-2) var(--p-3);
|
||||
border-bottom: 1px solid var(--p-line);
|
||||
background: var(--p-surface-2);
|
||||
}
|
||||
|
||||
.capital-doc-step__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--p-3);
|
||||
min-height: 240px;
|
||||
padding: var(--p-6);
|
||||
}
|
||||
|
||||
.capital-doc-step__loading-text {
|
||||
margin: 0;
|
||||
color: var(--p-ink-2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as CapitalCouncilStepGroup } from './CapitalCouncilStepGroup.vue';
|
||||
export { default as CapitalOnboardingCard } from './CapitalOnboardingCard.vue';
|
||||
export { default as CapitalProgramDocumentParametersWidget } from './CapitalProgramDocumentParametersWidget.vue';
|
||||
export { default as CapitalProgramDocumentStepPanel } from './CapitalProgramDocumentStepPanel.vue';
|
||||
export { default as CapitalProgramInlineDocumentPreview } from './CapitalProgramInlineDocumentPreview.vue';
|
||||
|
||||
@@ -66,21 +66,20 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
|
||||
<style scoped>
|
||||
.base-dialog {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - var(--p-6, 48px));
|
||||
}
|
||||
/* Maximized: контейнер растягивается на весь экран, контент скроллится. */
|
||||
.base-dialog--maximized {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.base-dialog--maximized .base-dialog__body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
max-height: none;
|
||||
}
|
||||
.base-dialog__head {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--p-3, 12px);
|
||||
@@ -94,13 +93,19 @@ const maxWidthBySize: Record<BaseDialogSize, string> = {
|
||||
}
|
||||
.base-dialog__body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding-bottom: var(--p-2, 8px);
|
||||
}
|
||||
.base-dialog__body > :first-child + * {
|
||||
margin-top: var(--p-4, 16px);
|
||||
}
|
||||
.base-dialog__foot {
|
||||
padding-top: var(--p-2, 8px);
|
||||
flex: 0 0 auto;
|
||||
padding: var(--p-3, 12px) var(--p-4, 16px);
|
||||
border-top: 1px solid var(--p-line);
|
||||
background: var(--p-surface);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user