@coopenomics/desktop 2025.6.19 → 2025.6.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,36 +1,36 @@
1
- import { Router } from 'vue-router'
2
- import { useSessionStore } from 'src/entities/Session'
3
- import { useCurrentUserStore } from 'src/entities/User'
4
- import { useDesktopStore } from 'src/entities/Desktop/model'
5
- import { useSystemStore } from 'src/entities/System/model'
6
- import { LocalStorage } from 'quasar'
1
+ import { Router } from 'vue-router';
2
+ import { useSessionStore } from 'src/entities/Session';
3
+ import { useCurrentUserStore } from 'src/entities/User';
4
+ import { useDesktopStore } from 'src/entities/Desktop/model';
5
+ import { useSystemStore } from 'src/entities/System/model';
6
+ import { LocalStorage } from 'quasar';
7
7
 
8
8
  function hasAccess(to, userAccount) {
9
- if (!to.meta?.roles || to.meta?.roles.length === 0) return true
10
- return userAccount && to.meta?.roles.includes(userAccount.role)
9
+ if (!to.meta?.roles || to.meta?.roles.length === 0) return true;
10
+ return userAccount && to.meta?.roles.includes(userAccount.role);
11
11
  }
12
12
 
13
13
  // Функция для получения URL для редиректа
14
14
  function getRedirectUrl(router: Router, to: any): string {
15
15
  if (process.env.CLIENT) {
16
- return router.resolve(to).href
16
+ return router.resolve(to).href;
17
17
  }
18
- return ''
18
+ return '';
19
19
  }
20
20
 
21
21
  export function setupNavigationGuard(router: Router) {
22
- const desktops = useDesktopStore()
23
- const session = useSessionStore()
24
- const currentUser = useCurrentUserStore()
25
- const { info } = useSystemStore()
22
+ const desktops = useDesktopStore();
23
+ const session = useSessionStore();
24
+ const currentUser = useCurrentUserStore();
25
+ const { info } = useSystemStore();
26
26
 
27
27
  router.beforeEach(async (to, from, next) => {
28
- await desktops.healthCheck()
28
+ await desktops.healthCheck();
29
29
 
30
30
  // если требуется установка
31
31
  if (desktops.health?.status === 'install' && to.name !== 'install') {
32
- next({ name: 'install', params: { coopname: info.coopname } })
33
- return
32
+ next({ name: 'install', params: { coopname: info.coopname } });
33
+ return;
34
34
  }
35
35
 
36
36
  // редирект с index
@@ -39,18 +39,18 @@ export function setupNavigationGuard(router: Router) {
39
39
  if (session.isAuth && currentUser.isRegistrationComplete) {
40
40
  // Если рабочий стол не выбран - выбираем по правам пользователя
41
41
  if (!desktops.activeWorkspaceName) {
42
- desktops.selectDefaultWorkspace()
42
+ desktops.selectDefaultWorkspace();
43
43
  }
44
44
 
45
45
  // Переходим на маршрут по умолчанию для выбранного рабочего стола
46
- desktops.goToDefaultPage(router)
47
- // next(false)
48
- return
46
+ desktops.goToDefaultPage(router);
47
+
48
+ return;
49
49
  } else {
50
50
  // Если пользователь не авторизован, используем nonAuthorizedHome
51
- const homePage = desktops.currentDesktop?.nonAuthorizedHome
52
- next({ name: homePage, params: { coopname: info.coopname } })
53
- return
51
+ const homePage = desktops.currentDesktop?.nonAuthorizedHome;
52
+ next({ name: homePage, params: { coopname: info.coopname } });
53
+ return;
54
54
  }
55
55
  }
56
56
 
@@ -59,19 +59,19 @@ export function setupNavigationGuard(router: Router) {
59
59
  // Сохраняем целевой URL для редиректа после входа
60
60
  if (process.env.CLIENT) {
61
61
  // Получаем URL для редиректа
62
- const redirectUrl = getRedirectUrl(router, to)
63
- LocalStorage.set('redirectAfterLogin', redirectUrl)
62
+ const redirectUrl = getRedirectUrl(router, to);
63
+ LocalStorage.set('redirectAfterLogin', redirectUrl);
64
64
  }
65
65
  // Перенаправляем на страницу входа
66
- next({ name: 'login-redirect', params: { coopname: info.coopname } })
67
- return
66
+ next({ name: 'login-redirect', params: { coopname: info.coopname } });
67
+ return;
68
68
  }
69
69
 
70
70
  // проверка по ролям
71
71
  if (hasAccess(to, currentUser.userAccount)) {
72
- next()
72
+ next();
73
73
  } else {
74
- next({ name: 'permissionDenied' })
74
+ next({ name: 'permissionDenied' });
75
75
  }
76
- })
76
+ });
77
77
  }
@@ -0,0 +1,328 @@
1
+ <template lang="pug">
2
+ q-select(
3
+ :label='label',
4
+ v-model='selectedUser',
5
+ :options='selectOptions',
6
+ :option-value='(item) => item?.data?.username',
7
+ :option-label='(item) => (item ? getDisplayName(item) : "")',
8
+ use-input,
9
+ hide-selected,
10
+ fill-input,
11
+ input-debounce='300',
12
+ emit-value,
13
+ map-options,
14
+ clearable,
15
+ :standout='standout',
16
+ :filled='filled',
17
+ :outlined='outlined',
18
+ :dense='dense',
19
+ :color='color',
20
+ @filter='onFilter',
21
+ @update:model-value='onUpdate',
22
+ :loading='loading',
23
+ :rules='rules',
24
+ :key='`user-select-${selectedUser}`'
25
+ )
26
+ template(v-slot:option='scope')
27
+ q-item(v-bind='scope.itemProps')
28
+ q-item-section(avatar)
29
+ q-avatar(
30
+ text-color='white',
31
+ :icon='getTypeIcon(scope.opt.type)',
32
+ size='sm',
33
+ :color='getTypeColor(scope.opt.type)'
34
+ )
35
+ q-item-section
36
+ q-item-label(style='font-weight: bold') {{ getDisplayName(scope.opt) }}
37
+ q-item-label(caption) {{ getUsernameFromData(scope.opt) }}
38
+ q-item-label(caption, v-if='getAdditionalInfo(scope.opt)') {{ getAdditionalInfo(scope.opt) }}
39
+
40
+ template(v-slot:selected-item)
41
+ div(v-if='selectedUser')
42
+ // Если есть полные данные пользователя
43
+ div(v-if='selectedUserObject')
44
+ q-avatar(
45
+ text-color='white',
46
+ :icon='getTypeIcon(selectedUserObject.type)',
47
+ size='sm',
48
+ :color='getTypeColor(selectedUserObject.type)'
49
+ )
50
+ // Fallback - показываем только username
51
+ div(v-else)
52
+ q-avatar(text-color='white', icon='person', size='sm', color='grey')
53
+ span.q-ml-sm {{ selectedUser }}
54
+
55
+ template(v-slot:no-option)
56
+ q-item
57
+ q-item-section.text-grey
58
+ | {{ searchQuery ? 'Ничего не найдено' : 'Начните вводить для поиска' }}
59
+ </template>
60
+
61
+ <script lang="ts" setup>
62
+ import { ref, watch, computed, onMounted } from 'vue';
63
+ import { useUserSearch } from './composables/useUserSearch';
64
+ import type { UserSearchResult } from './model/types';
65
+
66
+ // Пропсы компонента
67
+ const props = defineProps<{
68
+ modelValue?: string;
69
+ label?: string;
70
+ rules?: ((val: string) => boolean | string)[];
71
+ dense?: boolean;
72
+ standout?: boolean | string;
73
+ filled?: boolean;
74
+ outlined?: boolean;
75
+ color?: string;
76
+ }>();
77
+
78
+ // Эмиты
79
+ const emit = defineEmits<{
80
+ (e: 'update:modelValue', value: string | undefined): void;
81
+ }>();
82
+
83
+ // Композабл для поиска пользователей
84
+ const { searchResults, loading, searchUsers } = useUserSearch();
85
+
86
+ // Локальное состояние
87
+ const selectedUser = ref<string | undefined>(props.modelValue);
88
+ const searchQuery = ref('');
89
+ const selectedUserData = ref<UserSearchResult | null>(null);
90
+
91
+ // Отладка при монтировании
92
+ onMounted(() => {
93
+ // console.log('UserSearchSelector mounted');
94
+ // console.log('searchResults:', searchResults.value);
95
+ // console.log('loading:', loading.value);
96
+ // console.log('searchUsers function:', typeof searchUsers);
97
+ });
98
+
99
+ // Следим за изменениями modelValue
100
+ watch(
101
+ () => props.modelValue,
102
+ (newVal) => {
103
+ // console.log('modelValue changed to:', newVal); // Отладка
104
+ selectedUser.value = newVal;
105
+ // Очищаем сохраненные данные при изменении извне
106
+ if (!newVal) {
107
+ selectedUserData.value = null;
108
+ }
109
+ },
110
+ );
111
+
112
+ // Следим за изменениями selectedUser для отладки
113
+ watch(selectedUser, (newVal, oldVal) => {
114
+ // console.log('selectedUser changed from:', oldVal, 'to:', newVal);
115
+ if (oldVal && !newVal) {
116
+ // console.warn('selectedUser was cleared! Stack trace:');
117
+ // console.trace();
118
+ }
119
+ });
120
+
121
+ // Вычисляемые свойства
122
+ const label = computed(() => props.label || 'Выберите пользователя');
123
+
124
+ // Опции для select - всегда включаем выбранного пользователя
125
+ const selectOptions = computed(() => {
126
+ // console.log('Computing selectOptions...'); // Отладка
127
+ // console.log('searchResults.value:', searchResults.value); // Отладка
128
+ // console.log('selectedUserData.value:', selectedUserData.value); // Отладка
129
+
130
+ const options = [...searchResults.value];
131
+
132
+ // Если есть выбранный пользователь, но его нет в результатах поиска, добавляем
133
+ if (selectedUserData.value) {
134
+ const isAlreadyInOptions = options.some(
135
+ (option) =>
136
+ option.data.username === selectedUserData.value?.data.username,
137
+ );
138
+ // console.log('Selected user already in options:', isAlreadyInOptions); // Отладка
139
+ if (!isAlreadyInOptions) {
140
+ options.unshift(selectedUserData.value);
141
+ // console.log('Added selected user to options'); // Отладка
142
+ }
143
+ }
144
+
145
+ // console.log('Final select options:', options); // Отладка
146
+ return options;
147
+ });
148
+
149
+ // Находим объект выбранного пользователя для отображения
150
+ const selectedUserObject = computed(() => {
151
+ // console.log('Computing selectedUserObject...'); // Отладка
152
+ // console.log('selectedUser.value:', selectedUser.value); // Отладка
153
+ // console.log('selectedUserData.value:', selectedUserData.value); // Отладка
154
+
155
+ // Сначала пробуем использовать сохраненные данные
156
+ if (
157
+ selectedUserData.value &&
158
+ selectedUserData.value.data.username === selectedUser.value
159
+ ) {
160
+ // console.log('Using saved user data'); // Отладка
161
+ return selectedUserData.value;
162
+ }
163
+
164
+ // Если нет сохраненных данных, ищем в опциях select
165
+ if (!selectedUser.value) {
166
+ // console.log('No selected user'); // Отладка
167
+ return null;
168
+ }
169
+
170
+ const found = selectOptions.value.find(
171
+ (result) => result.data.username === selectedUser.value,
172
+ );
173
+ // console.log('Found in select options:', found); // Отладка
174
+ return found || null;
175
+ });
176
+
177
+ // Методы для отображения
178
+ const getDisplayName = (user: UserSearchResult | null | undefined): string => {
179
+ if (!user || !user.data) return '';
180
+
181
+ try {
182
+ switch (user.type) {
183
+ case 'individual':
184
+ case 'entrepreneur': {
185
+ const data = user.data as any;
186
+ return (
187
+ `${data.last_name || ''} ${data.first_name || ''} ${data.middle_name || ''}`.trim() ||
188
+ data.username ||
189
+ ''
190
+ );
191
+ }
192
+ case 'organization': {
193
+ const data = user.data as any;
194
+ return data.short_name || data.full_name || data.username || '';
195
+ }
196
+ default:
197
+ return user.data?.username || '';
198
+ }
199
+ } catch (error) {
200
+ // console.error('Error in getDisplayName:', error, user);
201
+ return user.data?.username || '';
202
+ }
203
+ };
204
+
205
+ const getUsernameFromData = (
206
+ user: UserSearchResult | null | undefined,
207
+ ): string => {
208
+ return user?.data?.username || '';
209
+ };
210
+
211
+ const getAdditionalInfo = (
212
+ user: UserSearchResult | null | undefined,
213
+ ): string => {
214
+ if (!user || !user.data) return '';
215
+
216
+ try {
217
+ switch (user.type) {
218
+ case 'entrepreneur': {
219
+ const data = user.data as any;
220
+ return `ИП • ИНН: ${data.details?.inn || 'н/д'}`;
221
+ }
222
+ case 'organization': {
223
+ const data = user.data as any;
224
+ return `Организация • ИНН: ${data.details?.inn || 'н/д'}`;
225
+ }
226
+ default:
227
+ return '';
228
+ }
229
+ } catch (error) {
230
+ // console.error('Error in getAdditionalInfo:', error, user);
231
+ return '';
232
+ }
233
+ };
234
+
235
+ const getTypeIcon = (type: string | undefined): string => {
236
+ switch (type) {
237
+ case 'individual':
238
+ return 'person';
239
+ case 'entrepreneur':
240
+ return 'business';
241
+ case 'organization':
242
+ return 'corporate_fare';
243
+ default:
244
+ return 'person';
245
+ }
246
+ };
247
+
248
+ const getTypeColor = (type: string | undefined): string => {
249
+ switch (type) {
250
+ case 'individual':
251
+ return 'blue';
252
+ case 'entrepreneur':
253
+ return 'green';
254
+ case 'organization':
255
+ return 'purple';
256
+ default:
257
+ return 'grey';
258
+ }
259
+ };
260
+
261
+ // Обработчики событий
262
+ const onFilter = (
263
+ val: string,
264
+ update: (fn: () => void) => void,
265
+ abort: () => void,
266
+ ) => {
267
+ // console.log('onFilter called with:', val); // Отладка
268
+ searchQuery.value = val;
269
+
270
+ if (val.length >= 1) {
271
+ searchUsers(val)
272
+ .then(() => {
273
+ update(() => {
274
+ return;
275
+ });
276
+ })
277
+ .catch(() => {
278
+ abort();
279
+ });
280
+ } else {
281
+ if (selectedUserData.value) {
282
+ searchResults.value = [selectedUserData.value];
283
+ } else {
284
+ searchResults.value = [];
285
+ }
286
+ update(() => {
287
+ // console.log('Update callback for short query called'); // Отладка
288
+ });
289
+ }
290
+ };
291
+
292
+ const onUpdate = (value: UserSearchResult | string | undefined) => {
293
+ // console.log('onUpdate called with:', value, 'type:', typeof value); // Отладка
294
+
295
+ // Если передан объект результата поиска, извлекаем username и сохраняем данные
296
+ let username: string | undefined;
297
+ if (typeof value === 'string') {
298
+ username = value;
299
+ // console.log('String value received:', username); // Отладка
300
+
301
+ // При передаче строки пытаемся найти соответствующий объект в опциях
302
+ const foundUser = selectOptions.value.find(
303
+ (result) => result.data.username === value,
304
+ );
305
+ // console.log('Found user for string:', foundUser); // Отладка
306
+
307
+ if (foundUser) {
308
+ selectedUserData.value = foundUser;
309
+ }
310
+ } else if (value && typeof value === 'object' && 'data' in value) {
311
+ username = value.data.username;
312
+ selectedUserData.value = value; // Сохраняем полные данные пользователя
313
+ } else {
314
+ username = undefined;
315
+ selectedUserData.value = null;
316
+ }
317
+
318
+ selectedUser.value = username;
319
+ emit('update:modelValue', username);
320
+ };
321
+ </script>
322
+
323
+ <style scoped>
324
+ :deep(mark) {
325
+ background-color: #ffeb3b;
326
+ padding: 0;
327
+ }
328
+ </style>
@@ -0,0 +1,70 @@
1
+ import { ref } from 'vue';
2
+ import { client } from 'src/shared/api/client';
3
+ import { Queries } from '@coopenomics/sdk';
4
+ import type { UserSearchResult } from '../model/types';
5
+
6
+ /**
7
+ * Композабл для поиска пользователей
8
+ */
9
+ export function useUserSearch() {
10
+ const searchResults = ref<UserSearchResult[]>([]);
11
+ const loading = ref(false);
12
+ const error = ref<string | null>(null);
13
+
14
+ /**
15
+ * Выполняет поиск пользователей по запросу
16
+ * @param query Поисковый запрос
17
+ */
18
+ const searchUsers = async (query: string): Promise<void> => {
19
+ if (!query || query.length < 1) {
20
+ searchResults.value = [];
21
+ return;
22
+ }
23
+
24
+ loading.value = true;
25
+ error.value = null;
26
+
27
+ try {
28
+ console.log('Making API call with query:', query); // Отладка
29
+
30
+ const response = await client.Query(
31
+ Queries.Accounts.SearchPrivateAccounts.query,
32
+ {
33
+ variables: {
34
+ data: {
35
+ query: query.trim(),
36
+ },
37
+ },
38
+ },
39
+ );
40
+
41
+ console.log('API response:', response); // Отладка
42
+
43
+ searchResults.value = response.searchPrivateAccounts || [];
44
+ console.log('Updated searchResults:', searchResults.value); // Отладка
45
+ } catch (err) {
46
+ console.error('Ошибка поиска пользователей:', err);
47
+ error.value = 'Ошибка при поиске пользователей';
48
+ searchResults.value = [];
49
+ throw err; // Пробрасываем ошибку для обработки в компоненте
50
+ } finally {
51
+ loading.value = false;
52
+ }
53
+ };
54
+
55
+ /**
56
+ * Очищает результаты поиска
57
+ */
58
+ const clearResults = () => {
59
+ searchResults.value = [];
60
+ error.value = null;
61
+ };
62
+
63
+ return {
64
+ searchResults,
65
+ loading,
66
+ error,
67
+ searchUsers,
68
+ clearResults,
69
+ };
70
+ }
@@ -0,0 +1,3 @@
1
+ export { default as UserSearchSelector } from './UserSearchSelector.vue';
2
+ export * from './model/types';
3
+ export * from './composables/useUserSearch';
@@ -0,0 +1,17 @@
1
+ import type { Queries } from '@coopenomics/sdk';
2
+
3
+ /**
4
+ * Результат поиска пользователя из SDK
5
+ */
6
+ export type UserSearchResult =
7
+ Queries.Accounts.SearchPrivateAccounts.IOutput['searchPrivateAccounts'][0];
8
+
9
+ /**
10
+ * Данные пользователя для отображения
11
+ */
12
+ export interface UserDisplayData {
13
+ username: string;
14
+ displayName: string;
15
+ type: 'individual' | 'entrepreneur' | 'organization';
16
+ additionalInfo?: string;
17
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './CopyableInput';
2
2
  export * from './ExpandableDocument';
3
+ export * from './UserSearchSelector';