@coopenomics/desktop 2025.5.14 → 2025.6.13

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.
Files changed (114) hide show
  1. package/.env-example +4 -0
  2. package/extensions/participant/install.ts +37 -25
  3. package/extensions/soviet/install.ts +23 -20
  4. package/package.json +7 -5
  5. package/quasar.config.cjs +1 -1
  6. package/src/app/App.vue +1 -0
  7. package/src/app/providers/routes/index.ts +12 -0
  8. package/src/app/styles/app.scss +1 -1
  9. package/src/app/styles/style.css +11 -0
  10. package/src/css/quasar.variables.scss +1 -1
  11. package/src/entities/Desktop/model/store.ts +68 -0
  12. package/src/entities/Desktop/model/types.ts +7 -0
  13. package/src/entities/Document/model/types.ts +1 -1
  14. package/src/entities/Meet/api/index.ts +2 -28
  15. package/src/entities/Meet/model/store.ts +7 -22
  16. package/src/entities/Wallet/api/index.ts +3 -3
  17. package/src/env.d.ts +1 -0
  18. package/src/features/Meet/CloseMeetWithDecision/model/index.ts +119 -17
  19. package/src/features/Meet/CreateMeet/model/index.ts +51 -9
  20. package/src/features/Meet/CreateMeet/ui/CreateMeet.vue +37 -6
  21. package/src/features/Meet/CreateMeet/ui/CreateMeetForm.vue +87 -65
  22. package/src/features/Meet/GenerateSovietDecision/model/index.ts +14 -4
  23. package/src/features/Meet/RestartMeet/model/index.ts +121 -3
  24. package/src/features/Meet/RestartMeet/ui/RestartMeet.vue +4 -6
  25. package/src/features/Meet/RestartMeet/ui/RestartMeetForm.vue +64 -28
  26. package/src/features/Meet/SignNotification/index.ts +2 -0
  27. package/src/features/Meet/SignNotification/model/index.ts +137 -0
  28. package/src/features/Meet/SignNotification/ui/SignNotificationButton.vue +61 -0
  29. package/src/features/Meet/SignNotification/ui/index.ts +1 -0
  30. package/src/features/Meet/VoteOnMeet/model/composable.ts +180 -0
  31. package/src/features/Meet/VoteOnMeet/model/index.ts +2 -17
  32. package/src/features/Meet/VoteOnMeet/model/types.ts +4 -0
  33. package/src/features/Meet/index.ts +6 -0
  34. package/src/features/User/LoginRedirect/ui/LoginRedirectPage.vue +15 -0
  35. package/src/features/User/LoginRedirect/ui/index.ts +1 -0
  36. package/src/features/User/LoginUser/ui/LoginForm/LoginForm.vue +38 -1
  37. package/src/features/User/LoginWithRedirect/ui/LoginRedirectForm/LoginRedirectForm.vue +0 -0
  38. package/src/pages/Cooperative/ListOfMeets/ui/ListOfMeetsPage.vue +22 -46
  39. package/src/pages/Cooperative/MeetDetails/ui/MeetDetailsPage.vue +84 -28
  40. package/src/pages/PermissionDenied/PermissionDenied.vue +1 -1
  41. package/src/processes/init-app/index.ts +12 -5
  42. package/src/processes/navigation-guard-setup/index.ts +37 -5
  43. package/src/processes/process-decisions/index.ts +7 -6
  44. package/src/shared/config/Environment.ts +35 -27
  45. package/src/shared/lib/composables/index.ts +1 -0
  46. package/src/shared/lib/composables/useMeetStatus.ts +96 -0
  47. package/src/shared/lib/consts/index.ts +1 -0
  48. package/src/shared/lib/consts/meet-statuses.ts +114 -0
  49. package/src/shared/lib/document/model/entity.ts +4 -2
  50. package/src/shared/lib/types/certificate/index.ts +6 -0
  51. package/src/shared/lib/types/document/index.ts +1 -1
  52. package/src/shared/lib/types/workspace.ts +24 -0
  53. package/src/shared/lib/utils/dates/index.ts +5 -0
  54. package/src/shared/lib/utils/dates/moment.ts +43 -2
  55. package/src/shared/lib/utils/dates/timezone.ts +75 -0
  56. package/src/shared/lib/utils/getNameFromCertificate.ts +108 -0
  57. package/src/shared/lib/utils/index.ts +1 -0
  58. package/src/shared/lib/utils/parseLinks.ts +10 -0
  59. package/src/shared/ui/AgendaNumberAvatar/AgendaNumberAvatar.vue +12 -0
  60. package/src/shared/ui/AgendaNumberAvatar/index.ts +1 -0
  61. package/src/shared/ui/BaseDocument/BaseDocument.vue +37 -8
  62. package/src/shared/ui/ExpandableDocument/ExpandableDocument.vue +49 -0
  63. package/src/shared/ui/ExpandableDocument/index.ts +1 -0
  64. package/src/shared/ui/MeetInfoCard/index.ts +1 -0
  65. package/src/shared/ui/MeetInfoCard/ui/MeetInfoCard.vue +62 -0
  66. package/src/shared/ui/MeetStatusBanner/index.ts +1 -0
  67. package/src/shared/ui/MeetStatusBanner/ui/MeetStatusBanner.vue +94 -0
  68. package/src/shared/ui/index.ts +1 -0
  69. package/src/widgets/Cooperative/Documents/ListOfDocuments/ui/DocumentsTable.vue +3 -3
  70. package/src/widgets/Cooperative/Orders/ListOfOrders/ui/ListOfOrdersWidget.vue +2 -2
  71. package/src/widgets/Cooperative/Payments/ListOfPayments/ui/ListOfPaymentsWidget.vue +2 -2
  72. package/src/widgets/Desktop/WorkspaceMenu/WorkspaceMenu.vue +74 -82
  73. package/src/widgets/Header/CommonHeader/CooperativeSettingsHeader.vue +1 -1
  74. package/src/widgets/Header/CommonHeader/ExtstoreHeader.vue +1 -1
  75. package/src/widgets/Header/CommonHeader/MainHeader.vue +6 -0
  76. package/src/widgets/Header/CommonHeader/UserSettingsHeader.vue +1 -1
  77. package/src/widgets/Meets/MeetCardsList/index.ts +1 -0
  78. package/src/widgets/Meets/MeetCardsList/ui/MeetCardsList.vue +55 -0
  79. package/src/widgets/Meets/MeetDetailsActions/MeetDetailsActions.vue +33 -15
  80. package/src/widgets/Meets/MeetDetailsAgenda/MeetDetailsAgenda.vue +34 -15
  81. package/src/widgets/Meets/MeetDetailsInfo/MeetDetailsInfo.vue +27 -0
  82. package/src/widgets/Meets/MeetDetailsInfo/index.ts +1 -0
  83. package/src/widgets/Meets/MeetDetailsResults/MeetDetailsResults.vue +129 -0
  84. package/src/widgets/Meets/MeetDetailsResults/index.ts +1 -0
  85. package/src/widgets/Meets/MeetDetailsVoting/MeetDetailsVoting.vue +221 -62
  86. package/src/widgets/Meets/MeetQuorumIndicator/MeetQuorumIndicator.vue +0 -0
  87. package/src/widgets/Meets/MeetQuorumIndicator/index.ts +1 -0
  88. package/src/widgets/Meets/MeetQuorumIndicator/ui/MeetQuorumIndicator.vue +35 -0
  89. package/src/widgets/Meets/MeetsTable/ui/MeetsTable.vue +56 -65
  90. package/src/widgets/NotificationCenter/NotificationCenter.vue +90 -0
  91. package/src/widgets/NotificationCenter/index.ts +1 -0
  92. package/src/widgets/Participants/ui/ParticipantsTable.vue +1 -1
  93. package/src/widgets/Questions/ui/QuestionsTable/QuestionsTable.vue +22 -10
  94. package/src/widgets/Questions/ui/VotingButtons/VotingButtons.vue +59 -14
  95. package/src/widgets/Registrator/AlreadyRegistered/AlreadyRegistered.vue +1 -1
  96. package/src/widgets/User/PaymentMethods/ui/PaymentMethods.vue +0 -1
  97. package/src-ssr/middlewares/injectEnv.ts +18 -12
  98. package/src/features/Meet/GenerateAgenda/index.ts +0 -1
  99. package/src/features/Meet/GenerateAgenda/model/index.ts +0 -18
  100. package/src/features/Meet/GenerateBallot/index.ts +0 -1
  101. package/src/features/Meet/GenerateBallot/model/index.ts +0 -18
  102. package/src/features/Meet/GenerateNotification/index.ts +0 -1
  103. package/src/features/Meet/GenerateNotification/model/index.ts +0 -18
  104. package/src/features/Meet/MeetDetailsManagement/index.ts +0 -1
  105. package/src/features/Meet/MeetDetailsManagement/model/index.ts +0 -121
  106. package/src/pages/Cooperative/ListOfMeets/model/index.ts +0 -1
  107. package/src/pages/Cooperative/ListOfMeets/model/model.ts +0 -117
  108. package/src/widgets/Meets/MeetDetailsActions/model.ts +0 -46
  109. package/src/widgets/Meets/MeetDetailsHeader/MeetDetailsHeader.vue +0 -40
  110. package/src/widgets/Meets/MeetDetailsHeader/index.ts +0 -1
  111. package/src/widgets/Meets/MeetDetailsVoting/model.ts +0 -117
  112. package/src/widgets/Meets/MeetInfoCard/ui/MeetInfoCard.vue +0 -38
  113. package/src/widgets/Meets/MeetInfoCard/ui/index.ts +0 -1
  114. /package/src/{widgets/Meets/MeetInfoCard → features/User/LoginRedirect}/index.ts +0 -0
@@ -11,6 +11,8 @@ import { computed } from 'vue'
11
11
  import { useAgendaStore } from 'src/entities/Agenda/model'
12
12
  import type { IAgenda } from 'src/entities/Agenda/model'
13
13
  import { DigitalDocument } from 'src/shared/lib/document'
14
+ import type { IUserCertificateUnion } from 'src/shared/lib/types/certificate'
15
+ import { getNameFromCertificate } from 'src/shared/lib/utils/getNameFromCertificate'
14
16
 
15
17
  /**
16
18
  * Процесс обработки решений
@@ -28,14 +30,11 @@ export function useDecisionProcessor() {
28
30
  /**
29
31
  * Форматирует заголовок вопроса
30
32
  */
31
- function formatDecisionTitle(title: string, user: any) {
33
+ function formatDecisionTitle(title: string, cert: IUserCertificateUnion) {
32
34
  let result = 'Вопрос на голосование'
33
35
 
34
- if (user.first_name) {
35
- result = `${title} от ${user.last_name} ${user.first_name} ${user.middle_name}`
36
- } else {
37
- result = `${title} от ${user.short_name}`
38
- }
36
+ const name = getNameFromCertificate(cert)
37
+ result = `${title} от ${name}`
39
38
 
40
39
  return result
41
40
  }
@@ -138,6 +137,8 @@ export function useDecisionProcessor() {
138
137
  const { generateSovietDecisionOnAnnualMeet } = useGenerateSovietDecisionOnAnnualMeet()
139
138
  document = await generateSovietDecisionOnAnnualMeet({
140
139
  username: username,
140
+ decision_id: decision_id,
141
+ meet_hash: row.table.hash as string
141
142
  })
142
143
  }
143
144
  else {
@@ -1,19 +1,23 @@
1
1
  // Типы для переменных окружения
2
2
  export interface EnvVars {
3
- NODE_ENV?: string;
4
- BACKEND_URL?: string;
5
- CHAIN_URL?: string;
6
- CHAIN_ID?: string;
7
- CURRENCY?: string;
8
- COOP_SHORT_NAME?: string;
9
- SITE_DESCRIPTION?: string;
10
- SITE_IMAGE?: string;
11
- STORAGE_URL?: string;
12
- UPLOAD_URL?: string;
13
- CLIENT?: boolean;
14
- SERVER?: boolean;
15
- VUE_ROUTER_MODE?: string;
16
- VUE_ROUTER_BASE?: string;
3
+ NODE_ENV: string;
4
+ BACKEND_URL: string;
5
+ CHAIN_URL: string;
6
+ CHAIN_ID: string;
7
+ CURRENCY: string;
8
+ COOP_SHORT_NAME: string;
9
+ SITE_DESCRIPTION: string;
10
+ SITE_IMAGE: string;
11
+ STORAGE_URL: string;
12
+ UPLOAD_URL: string;
13
+ TIMEZONE: string;
14
+ CLIENT: boolean;
15
+ SERVER: boolean;
16
+ VUE_ROUTER_MODE: string;
17
+ VUE_ROUTER_BASE: string;
18
+ NOVU_APP_ID: string;
19
+ NOVU_BACKEND_URL: string;
20
+ NOVU_SOCKET_URL: string;
17
21
  [key: string]: string | boolean | undefined;
18
22
  }
19
23
 
@@ -37,20 +41,24 @@ function getEnv(): EnvVars {
37
41
  // Для SSR сервера - берем реальные переменные
38
42
  // Для SPA - эти значения заменятся при сборке
39
43
  return {
40
- NODE_ENV: process.env.NODE_ENV,
41
- BACKEND_URL: process.env.BACKEND_URL,
42
- CHAIN_URL: process.env.CHAIN_URL,
43
- CHAIN_ID: process.env.CHAIN_ID,
44
- CURRENCY: process.env.CURRENCY,
45
- COOP_SHORT_NAME: process.env.COOP_SHORT_NAME,
46
- SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
47
- SITE_IMAGE: process.env.SITE_IMAGE,
48
- STORAGE_URL: process.env.STORAGE_URL,
49
- UPLOAD_URL: process.env.UPLOAD_URL,
44
+ NODE_ENV: process.env.NODE_ENV as string,
45
+ BACKEND_URL: process.env.BACKEND_URL as string,
46
+ CHAIN_URL: process.env.CHAIN_URL as string,
47
+ CHAIN_ID: process.env.CHAIN_ID as string,
48
+ CURRENCY: process.env.CURRENCY as string,
49
+ COOP_SHORT_NAME: process.env.COOP_SHORT_NAME as string,
50
+ SITE_DESCRIPTION: process.env.SITE_DESCRIPTION as string,
51
+ SITE_IMAGE: process.env.SITE_IMAGE as string,
52
+ STORAGE_URL: process.env.STORAGE_URL as string,
53
+ UPLOAD_URL: process.env.UPLOAD_URL as string,
54
+ TIMEZONE: process.env.TIMEZONE || 'Europe/Moscow',
50
55
  CLIENT: process.env.CLIENT as unknown as boolean,
51
56
  SERVER: process.env.SERVER as unknown as boolean,
52
- VUE_ROUTER_MODE: process.env.VUE_ROUTER_MODE,
53
- VUE_ROUTER_BASE: process.env.VUE_ROUTER_BASE
57
+ VUE_ROUTER_MODE: process.env.VUE_ROUTER_MODE as string,
58
+ VUE_ROUTER_BASE: process.env.VUE_ROUTER_BASE as string,
59
+ NOVU_APP_ID: process.env.NOVU_APP_ID as string,
60
+ NOVU_BACKEND_URL: process.env.NOVU_BACKEND_URL as string,
61
+ NOVU_SOCKET_URL: process.env.NOVU_SOCKET_URL as string,
54
62
  };
55
63
  }
56
64
 
@@ -61,7 +69,7 @@ function getEnv(): EnvVars {
61
69
 
62
70
  // Запасной вариант, если ничего не сработало
63
71
  console.warn('Не удалось получить переменные окружения!');
64
- return {};
72
+ return {} as EnvVars;
65
73
  }
66
74
 
67
75
  // Экспортируем переменные окружения
@@ -0,0 +1 @@
1
+ export { useMeetStatus } from './useMeetStatus'
@@ -0,0 +1,96 @@
1
+ import { computed } from 'vue'
2
+ import type { IMeet } from 'src/entities/Meet'
3
+ import moment from 'moment-with-locales-es6'
4
+ import { BASIC_STATUS_MAP, EXTENDED_STATUS_MAP, SPECIAL_STATUSES } from 'src/shared/lib/consts'
5
+ import { formatDateToLocalTimezone, formatDateFromNow } from 'src/shared/lib/utils/dates/timezone'
6
+
7
+ export function useMeetStatus(meet: IMeet | null) {
8
+ // Базовый статус собрания
9
+ const basicStatus = computed(() => {
10
+ if (!meet?.processing?.meet?.status) return 'Неизвестный статус'
11
+ return BASIC_STATUS_MAP[meet.processing.meet.status] || 'Неизвестный статус'
12
+ })
13
+
14
+ // Расширенный статус собрания
15
+ const extendedStatus = computed(() => {
16
+ if (!meet?.processing?.extendedStatus) return 'Неизвестный статус'
17
+ return EXTENDED_STATUS_MAP[meet.processing.extendedStatus] || 'Неизвестный статус'
18
+ })
19
+
20
+ // Даты собрания в локальном часовом поясе
21
+ const formattedOpenDate = computed(() => {
22
+ if (!meet?.processing?.meet?.open_at) return ''
23
+ return formatDateToLocalTimezone(meet.processing.meet.open_at)
24
+ })
25
+
26
+ const formattedCloseDate = computed(() => {
27
+ if (!meet?.processing?.meet?.close_at) return ''
28
+ return formatDateToLocalTimezone(meet.processing.meet.close_at)
29
+ })
30
+
31
+ // Относительное время до/после собрания
32
+ const isVotingNotStarted = computed(() => {
33
+ if (!meet?.processing?.meet?.open_at) return false
34
+ return moment().isBefore(moment(meet.processing.meet.open_at))
35
+ })
36
+
37
+ const isVotingEnded = computed(() => {
38
+ if (!meet?.processing?.meet?.close_at) return false
39
+ return moment().isAfter(moment(meet.processing.meet.close_at))
40
+ })
41
+
42
+ const isVotingInProgress = computed(() => {
43
+ if (!meet?.processing?.meet?.open_at || !meet?.processing?.meet?.close_at) return false
44
+ const now = moment()
45
+ return now.isAfter(moment(meet.processing.meet.open_at)) && now.isBefore(moment(meet.processing.meet.close_at))
46
+ })
47
+
48
+ // Проверяем специальные статусы
49
+ const hasSpecialStatus = computed(() => {
50
+ if (!meet?.processing?.extendedStatus) return false
51
+ return SPECIAL_STATUSES.includes(meet.processing.extendedStatus)
52
+ })
53
+
54
+ const relativeOpenTime = computed(() => {
55
+ if (!meet?.processing?.meet?.open_at || hasSpecialStatus.value) return ''
56
+
57
+ const openMoment = moment(meet.processing.meet.open_at)
58
+ const now = moment()
59
+
60
+ if (now.isBefore(openMoment)) {
61
+ return `Собрание начнется ${formatDateFromNow(meet.processing.meet.open_at)}`
62
+ } else {
63
+ // Проверяем, закончилось ли уже собрание
64
+ if (isVotingEnded.value) {
65
+ return relativeCloseTime.value
66
+ }
67
+ return `Собрание началось ${formatDateFromNow(meet.processing.meet.open_at)}`
68
+ }
69
+ })
70
+
71
+ const relativeCloseTime = computed(() => {
72
+ if (!meet?.processing?.meet?.close_at || hasSpecialStatus.value) return ''
73
+
74
+ const closeMoment = moment(meet.processing.meet.close_at)
75
+ const now = moment()
76
+
77
+ if (now.isBefore(closeMoment)) {
78
+ return `Собрание завершится ${formatDateFromNow(meet.processing.meet.close_at)}`
79
+ } else {
80
+ return `Собрание завершилось ${formatDateFromNow(meet.processing.meet.close_at)}`
81
+ }
82
+ })
83
+
84
+ return {
85
+ basicStatus,
86
+ extendedStatus,
87
+ formattedOpenDate,
88
+ formattedCloseDate,
89
+ isVotingNotStarted,
90
+ isVotingEnded,
91
+ isVotingInProgress,
92
+ hasSpecialStatus,
93
+ relativeOpenTime,
94
+ relativeCloseTime
95
+ }
96
+ }
@@ -0,0 +1 @@
1
+ export * from './meet-statuses'
@@ -0,0 +1,114 @@
1
+ import { Zeus } from '@coopenomics/sdk'
2
+
3
+ // Описания расширенных статусов собрания
4
+ export const EXTENDED_STATUS_MAP: Record<Zeus.ExtendedMeetStatus, string> = {
5
+ 'NONE': 'Неопределенное состояние',
6
+ 'CREATED': 'Собрание создано. Ожидаем утверждения даты проведения.',
7
+ 'AUTHORIZED': 'Дата общего собрания утверждена. Ожидаем начала собрания.',
8
+ 'ONRESTART': 'Получено предложение новой даты общего собрания. Ожидаем утверждения.',
9
+ 'PRECLOSED': 'Получена подпись секретаря собрания на протоколе. Ожидаем подписи председателя.',
10
+ 'CLOSED': 'Собрание успешно завершено',
11
+ 'WAITING_FOR_OPENING': 'Собрание открывается',
12
+ 'VOTING_IN_PROGRESS': 'Собрание идет и завершится',
13
+ 'EXPIRED_NO_QUORUM': 'Кворум собрания не собран. Ожидаем предложения даты нового собрания.',
14
+ 'VOTING_COMPLETED': 'Собрание успешно завершено. Ожидаем подписи секретаря на протоколе.'
15
+ }
16
+
17
+ // Описания базовых статусов собрания
18
+ export const BASIC_STATUS_MAP: Record<string, string> = {
19
+ 'created': 'Ожидание решения совета',
20
+ 'authorized': 'Утверждено',
21
+ 'preclosed': 'На закрытии',
22
+ 'closed': 'Закрыто'
23
+ }
24
+
25
+ // Статусы, требующие специальной обработки времени
26
+ export const SPECIAL_STATUSES: Zeus.ExtendedMeetStatus[] = [
27
+ Zeus.ExtendedMeetStatus.ONRESTART,
28
+ Zeus.ExtendedMeetStatus.EXPIRED_NO_QUORUM,
29
+ Zeus.ExtendedMeetStatus.CLOSED,
30
+ Zeus.ExtendedMeetStatus.PRECLOSED,
31
+ Zeus.ExtendedMeetStatus.CREATED
32
+ ]
33
+
34
+ // Конфигурация для баннеров статусов собраний
35
+ // @property {string} class - CSS-класс для стилизации
36
+ // @property {boolean} needTime - Нужно ли отображать время
37
+ // @property {string} color - Цвет баннера
38
+ // @property {boolean} outline - Использовать ли контурный стиль
39
+ // @property {string} icon - иконка
40
+ export const STATUS_BANNER_CONFIG: Record<
41
+ Zeus.ExtendedMeetStatus,
42
+ { class: string; needTime: boolean; color: string; outline: boolean; icon: string }
43
+ > = {
44
+ NONE: {
45
+ class: 'text-grey-8',
46
+ needTime: false,
47
+ color: 'grey-4',
48
+ outline: true,
49
+ icon: 'help_outline'
50
+ },
51
+ WAITING_FOR_OPENING: {
52
+ class: 'text-grey-8',
53
+ needTime: true,
54
+ color: 'grey-4',
55
+ outline: true,
56
+ icon: 'hourglass_empty'
57
+ },
58
+ VOTING_IN_PROGRESS: {
59
+ class: 'text-primary',
60
+ needTime: true,
61
+ color: 'primary',
62
+ outline: true,
63
+ icon: 'how_to_vote'
64
+ },
65
+ VOTING_COMPLETED: {
66
+ class: 'text-primary',
67
+ needTime: false,
68
+ color: 'primary',
69
+ outline: true,
70
+ icon: 'hourglass_bottom'
71
+ },
72
+ CLOSED: {
73
+ class: 'text-primary',
74
+ needTime: false,
75
+ color: 'primary',
76
+ outline: true,
77
+ icon: 'check_circle'
78
+ },
79
+ CREATED: {
80
+ class: 'text-primary',
81
+ needTime: false,
82
+ color: 'primary',
83
+ outline: true,
84
+ icon: 'fiber_new'
85
+ },
86
+ AUTHORIZED: {
87
+ class: 'text-primary',
88
+ needTime: false,
89
+ color: 'primary',
90
+ outline: true,
91
+ icon: 'verified_user'
92
+ },
93
+ ONRESTART: {
94
+ class: 'text-primary',
95
+ needTime: false,
96
+ color: 'primary',
97
+ outline: true,
98
+ icon: 'sync_problem'
99
+ },
100
+ PRECLOSED: {
101
+ class: 'text-primary',
102
+ needTime: false,
103
+ color: 'primary',
104
+ outline: true,
105
+ icon: 'schedule'
106
+ },
107
+ EXPIRED_NO_QUORUM: {
108
+ class: 'text-primary',
109
+ needTime: false,
110
+ color: 'primary',
111
+ outline: true,
112
+ icon: 'group_off'
113
+ }
114
+ }
@@ -4,6 +4,7 @@ import { useGlobalStore } from 'src/shared/store';
4
4
  import type { IDocument, IMetaDocument } from 'src/shared/lib/types/document';
5
5
  import type { Cooperative } from 'cooptypes';
6
6
  import { Classes } from '@coopenomics/sdk';
7
+ import type { ISignedDocument2 } from 'src/entities/Document/model';
7
8
 
8
9
  export type ZGeneratedDocument = Cooperative.Document.ZGeneratedDocument
9
10
 
@@ -17,6 +18,7 @@ export const useSignDocument = () => {
17
18
  document: ZGeneratedDocument,
18
19
  account: string,
19
20
  signatureId = 1,
21
+ existingSignedDocuments?: ISignedDocument2[],
20
22
  ): Promise<Cooperative.Document.ISignedDocument2> => {
21
23
  if (!document)
22
24
  throw new Error('Документ на подпись не предоставлен')
@@ -30,7 +32,7 @@ export const useSignDocument = () => {
30
32
 
31
33
 
32
34
  // Получаем доступ к Document классу из SDK
33
- return await docSigner.signDocument(document, account, signatureId);
35
+ return await docSigner.signDocument(document, account, signatureId, existingSignedDocuments);
34
36
  }
35
37
 
36
38
  return {
@@ -67,7 +69,7 @@ export class DigitalDocument {
67
69
  const docSigner = new Classes.Document(wifKey);
68
70
 
69
71
  // Подписываем документ с использованием SDK
70
- const signedDoc = await docSigner.signDocument<T>(this.data, account, signatureId);
72
+ const signedDoc = await docSigner.signDocument(this.data, account, signatureId);
71
73
 
72
74
  this.signedDocument = signedDoc;
73
75
 
@@ -0,0 +1,6 @@
1
+ import type { Zeus } from '@coopenomics/sdk'
2
+
3
+ export type IUserCertificateUnion = Zeus.ModelTypes['UserCertificateUnion']
4
+ export type IIndividualCertificate = Zeus.ModelTypes['IndividualCertificate']
5
+ export type IEntrepreneurCertificate = Zeus.ModelTypes['EntrepreneurCertificate']
6
+ export type IOrganizationCertificate = Zeus.ModelTypes['OrganizationCertificate']
@@ -1,4 +1,4 @@
1
- import type { Zeus } from '@coopenomics/sdk/index';
1
+ import type { Zeus } from '@coopenomics/sdk'
2
2
 
3
3
  // Добавляю реэкспорты типов документов
4
4
  export type IChainDocument2 = Zeus.ModelTypes['SignedBlockchainDocument']
@@ -0,0 +1,24 @@
1
+ export interface IWorkspaceRouteMeta {
2
+ title: string
3
+ icon: string
4
+ roles: string[]
5
+ agreements?: string[]
6
+ conditions?: string
7
+ [key: string]: any
8
+ }
9
+
10
+ export interface IWorkspaceRoute {
11
+ path: string
12
+ name: string
13
+ component?: any
14
+ meta?: IWorkspaceRouteMeta
15
+ children?: IWorkspaceRoute[]
16
+ [key: string]: any
17
+ }
18
+
19
+ export interface IWorkspaceConfig {
20
+ workspace: string
21
+ title?: string
22
+ defaultRoute?: string // Имя маршрута для перехода по умолчанию
23
+ routes: IWorkspaceRoute[]
24
+ }
@@ -0,0 +1,5 @@
1
+ export * from './timezone'
2
+ export { default as moment } from './moment'
3
+ export { formatToFromNow } from './formatToFromNow'
4
+ export { formatToHumanDate } from './formatToHumanDate'
5
+ export { validateDateWithinRange } from './validateDateWithinRange'
@@ -1,6 +1,47 @@
1
- import moment from 'moment-with-locales-es6';
1
+ import moment from 'moment-timezone';
2
2
 
3
- // Устанавливаем локаль
3
+ // Добавляем русскую локализацию вручную
4
+ moment.updateLocale('ru', {
5
+ months: 'Январь_Февраль_Март_Апрель_Май_Июнь_Июль_Август_Сентябрь_Октябрь_Ноябрь_Декабрь'.split('_'),
6
+ monthsShort: 'янв_фев_мар_апр_май_июн_июл_авг_сен_окт_ноя_дек'.split('_'),
7
+ weekdays: 'Воскресенье_Понедельник_Вторник_Среда_Четверг_Пятница_Суббота'.split('_'),
8
+ weekdaysShort: 'Вс_Пн_Вт_Ср_Чт_Пт_Сб'.split('_'),
9
+ weekdaysMin: 'Вс_Пн_Вт_Ср_Чт_Пт_Сб'.split('_'),
10
+ longDateFormat: {
11
+ LT: 'HH:mm',
12
+ LTS: 'HH:mm:ss',
13
+ L: 'DD.MM.YYYY',
14
+ LL: 'D MMMM YYYY',
15
+ LLL: 'D MMMM YYYY HH:mm',
16
+ LLLL: 'dddd, D MMMM YYYY HH:mm'
17
+ },
18
+ calendar: {
19
+ sameDay: '[Сегодня в] LT',
20
+ nextDay: '[Завтра в] LT',
21
+ nextWeek: '[В] dddd [в] LT',
22
+ lastDay: '[Вчера в] LT',
23
+ lastWeek: '[В прошлую] dddd [в] LT',
24
+ sameElse: 'L'
25
+ },
26
+ relativeTime: {
27
+ future: 'через %s',
28
+ past: '%s назад',
29
+ s: 'несколько секунд',
30
+ ss: '%d секунд',
31
+ m: 'минуту',
32
+ mm: '%d минуты',
33
+ h: 'час',
34
+ hh: '%d час',
35
+ d: 'день',
36
+ dd: '%d дней',
37
+ M: 'месяц',
38
+ MM: '%d месяцев',
39
+ y: 'год',
40
+ yy: '%d лет'
41
+ }
42
+ });
43
+
44
+ // Устанавливаем локаль по умолчанию
4
45
  moment.locale('ru');
5
46
 
6
47
  export default moment;
@@ -0,0 +1,75 @@
1
+ import moment from './moment'
2
+ import { env } from 'src/shared/config/Environment'
3
+
4
+ // Дефолтный часовой пояс - московский
5
+ const DEFAULT_TIMEZONE = 'Europe/Moscow'
6
+
7
+ /**
8
+ * Получает часовой пояс из .env или возвращает московский по умолчанию
9
+ */
10
+ export function getTimezone(): string {
11
+ return env.TIMEZONE || DEFAULT_TIMEZONE
12
+ }
13
+
14
+ /**
15
+ * Получает читаемое название часового пояса для отображения в UI
16
+ */
17
+ export function getTimezoneLabel(): string {
18
+ const tz = getTimezone()
19
+ return tz === 'Europe/Moscow' ? 'МСК' : tz
20
+ }
21
+
22
+ /**
23
+ * Преобразует дату из UTC в локальный часовой пояс для отображения
24
+ * @param utcDate - дата в UTC (из блокчейна)
25
+ * @param format - формат отображения
26
+ */
27
+ export function formatDateToLocalTimezone(utcDate: string | Date | unknown, format = 'DD.MM.YYYY HH:mm'): string {
28
+ if (!utcDate || typeof utcDate !== 'string' && !(utcDate instanceof Date)) return ''
29
+
30
+ const timezone = getTimezone()
31
+ return moment.utc(utcDate).tz(timezone).format(format)
32
+ }
33
+
34
+ /**
35
+ * Преобразует локальную дату (из формы) в UTC для отправки в блокчейн
36
+ * @param localDate - дата в локальном часовом поясе
37
+ * @param format - формат входной даты
38
+ */
39
+ export function convertLocalDateToUTC(localDate: string, format = 'YYYY-MM-DDTHH:mm'): string {
40
+ if (!localDate) return ''
41
+
42
+ const timezone = getTimezone()
43
+ return moment.tz(localDate, format, timezone).utc().format()
44
+ }
45
+
46
+ /**
47
+ * Получает текущую дату в локальном часовом поясе для форм (формат datetime-local)
48
+ * @param offsetMinutes - смещение от текущего времени в минутах
49
+ */
50
+ export function getCurrentLocalDateForForm(offsetMinutes = 0): string {
51
+ const timezone = getTimezone()
52
+ return moment().tz(timezone).add(offsetMinutes, 'minutes').format('YYYY-MM-DDTHH:mm')
53
+ }
54
+
55
+ /**
56
+ * Форматирует дату для отображения "от сейчас" с учетом часового пояса
57
+ * @param utcDate - дата в UTC
58
+ */
59
+ export function formatDateFromNow(utcDate: string | Date | unknown): string {
60
+ if (!utcDate || typeof utcDate !== 'string' && !(utcDate instanceof Date)) return ''
61
+
62
+ const timezone = getTimezone()
63
+ return moment.utc(utcDate).tz(timezone).fromNow()
64
+ }
65
+
66
+ /**
67
+ * Получает дату в локальном часовом поясе для форм (формат datetime-local) с заданным смещением в днях и временем
68
+ * @param offsetDays - смещение от текущей даты в днях
69
+ * @param hour - час (0-23)
70
+ * @param minute - минута (0-59)
71
+ */
72
+ export function getFutureDateForForm(offsetDays = 0, hour = 0, minute = 0): string {
73
+ const timezone = getTimezone()
74
+ return moment().tz(timezone).add(offsetDays, 'days').set({ hour, minute, second: 0, millisecond: 0 }).format('YYYY-MM-DDTHH:mm')
75
+ }
@@ -0,0 +1,108 @@
1
+ import {
2
+ IIndividualCertificate,
3
+ IEntrepreneurCertificate,
4
+ IOrganizationCertificate
5
+ } from '../types/certificate'
6
+
7
+ /**
8
+ * Определяет тип сертификата и возвращает строку с именем пользователя
9
+ */
10
+ export function getNameFromCertificate(
11
+ certificate: IIndividualCertificate | IEntrepreneurCertificate | IOrganizationCertificate | null | undefined
12
+ ): string {
13
+ if (!certificate) return ''
14
+
15
+ // Определение типа сертификата
16
+ if (isOrganizationCertificate(certificate)) {
17
+ // Для организаций возвращаем короткое имя
18
+ return certificate.short_name
19
+ } else if (isIndividualCertificate(certificate) || isEntrepreneurCertificate(certificate)) {
20
+ // Для физ. лиц и ИП возвращаем ФИО
21
+ return formatFullName(certificate)
22
+ }
23
+
24
+ return ''
25
+ }
26
+
27
+ /**
28
+ * Определяет тип сертификата и возвращает сокращенное имя (Фамилия И.О.)
29
+ */
30
+ export function getShortNameFromCertificate(
31
+ certificate: IIndividualCertificate | IEntrepreneurCertificate | IOrganizationCertificate | null | undefined
32
+ ): string {
33
+ if (!certificate) return ''
34
+
35
+ // Определение типа сертификата
36
+ if (isOrganizationCertificate(certificate)) {
37
+ // Для организаций возвращаем короткое имя
38
+ return certificate.short_name
39
+ } else if (isIndividualCertificate(certificate) || isEntrepreneurCertificate(certificate)) {
40
+ // Для физ. лиц и ИП возвращаем сокращенное ФИО
41
+ return formatShortName(certificate)
42
+ }
43
+
44
+ return ''
45
+ }
46
+
47
+ /**
48
+ * Проверяет, является ли сертификат сертификатом физического лица
49
+ */
50
+ function isIndividualCertificate(
51
+ certificate: any
52
+ ): certificate is IIndividualCertificate {
53
+ return certificate.first_name !== undefined &&
54
+ certificate.last_name !== undefined &&
55
+ !('inn' in certificate) &&
56
+ !('ogrn' in certificate)
57
+ }
58
+
59
+ /**
60
+ * Проверяет, является ли сертификат сертификатом ИП
61
+ */
62
+ function isEntrepreneurCertificate(
63
+ certificate: any
64
+ ): certificate is IEntrepreneurCertificate {
65
+ return certificate.first_name !== undefined &&
66
+ certificate.last_name !== undefined &&
67
+ 'inn' in certificate &&
68
+ !('ogrn' in certificate)
69
+ }
70
+
71
+ /**
72
+ * Проверяет, является ли сертификат сертификатом организации
73
+ */
74
+ function isOrganizationCertificate(
75
+ certificate: any
76
+ ): certificate is IOrganizationCertificate {
77
+ return 'short_name' in certificate && 'inn' in certificate && 'ogrn' in certificate
78
+ }
79
+
80
+ /**
81
+ * Форматирует ФИО из объекта с полями first_name, last_name и middle_name
82
+ */
83
+ function formatFullName(
84
+ data: { first_name: string; last_name: string; middle_name?: string | null }
85
+ ): string {
86
+ const { last_name, first_name, middle_name } = data
87
+
88
+ if (middle_name) {
89
+ return `${last_name} ${first_name} ${middle_name}`
90
+ }
91
+
92
+ return `${last_name} ${first_name}`
93
+ }
94
+
95
+ /**
96
+ * Форматирует сокращенное ФИО из объекта с полями first_name, last_name и middle_name
97
+ * Возвращает "Фамилия И.О."
98
+ */
99
+ function formatShortName(
100
+ data: { first_name: string; last_name: string; middle_name?: string | null }
101
+ ): string {
102
+ const { last_name, first_name, middle_name } = data
103
+
104
+ const firstInitial = first_name.charAt(0).toUpperCase()
105
+ const middleInitial = middle_name ? middle_name.charAt(0).toUpperCase() + '.' : ''
106
+
107
+ return `${last_name} ${firstInitial}.${middleInitial}`
108
+ }
@@ -6,3 +6,4 @@ export * from './isDomainRule'
6
6
  export * from './validatePersonalNameRule'
7
7
  export * from './account'
8
8
  export * from './theme'
9
+ export * from './parseLinks'
@@ -0,0 +1,10 @@
1
+ // Преобразует ссылки вида @https://... или https://... или http://... в интерактивные <a>
2
+ export function parseLinks(text = ''): string {
3
+ if (!text) return ''
4
+ return text
5
+ .replace(/@?(https?:\/\/[^\s]+)/g, (match, url) => {
6
+ const cleanUrl = url.startsWith('http') ? url : url.slice(1)
7
+ return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer">${cleanUrl}</a>`
8
+ })
9
+ .replace(/\n/g, '<br>')
10
+ }