@coopenomics/desktop 2025.5.14 → 2025.6.14

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 (113) 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/src/app/App.vue +1 -0
  6. package/src/app/providers/routes/index.ts +12 -0
  7. package/src/app/styles/app.scss +1 -1
  8. package/src/app/styles/style.css +11 -0
  9. package/src/css/quasar.variables.scss +1 -1
  10. package/src/entities/Desktop/model/store.ts +68 -0
  11. package/src/entities/Desktop/model/types.ts +7 -0
  12. package/src/entities/Document/model/types.ts +1 -1
  13. package/src/entities/Meet/api/index.ts +2 -28
  14. package/src/entities/Meet/model/store.ts +7 -22
  15. package/src/entities/Wallet/api/index.ts +3 -3
  16. package/src/env.d.ts +1 -0
  17. package/src/features/Meet/CloseMeetWithDecision/model/index.ts +119 -17
  18. package/src/features/Meet/CreateMeet/model/index.ts +51 -9
  19. package/src/features/Meet/CreateMeet/ui/CreateMeet.vue +37 -6
  20. package/src/features/Meet/CreateMeet/ui/CreateMeetForm.vue +87 -65
  21. package/src/features/Meet/GenerateSovietDecision/model/index.ts +14 -4
  22. package/src/features/Meet/RestartMeet/model/index.ts +121 -3
  23. package/src/features/Meet/RestartMeet/ui/RestartMeet.vue +4 -6
  24. package/src/features/Meet/RestartMeet/ui/RestartMeetForm.vue +64 -28
  25. package/src/features/Meet/SignNotification/index.ts +2 -0
  26. package/src/features/Meet/SignNotification/model/index.ts +137 -0
  27. package/src/features/Meet/SignNotification/ui/SignNotificationButton.vue +61 -0
  28. package/src/features/Meet/SignNotification/ui/index.ts +1 -0
  29. package/src/features/Meet/VoteOnMeet/model/composable.ts +180 -0
  30. package/src/features/Meet/VoteOnMeet/model/index.ts +2 -17
  31. package/src/features/Meet/VoteOnMeet/model/types.ts +4 -0
  32. package/src/features/Meet/index.ts +6 -0
  33. package/src/features/User/LoginRedirect/ui/LoginRedirectPage.vue +15 -0
  34. package/src/features/User/LoginRedirect/ui/index.ts +1 -0
  35. package/src/features/User/LoginUser/ui/LoginForm/LoginForm.vue +38 -1
  36. package/src/features/User/LoginWithRedirect/ui/LoginRedirectForm/LoginRedirectForm.vue +0 -0
  37. package/src/pages/Cooperative/ListOfMeets/ui/ListOfMeetsPage.vue +22 -46
  38. package/src/pages/Cooperative/MeetDetails/ui/MeetDetailsPage.vue +84 -28
  39. package/src/pages/PermissionDenied/PermissionDenied.vue +1 -1
  40. package/src/processes/init-app/index.ts +12 -5
  41. package/src/processes/navigation-guard-setup/index.ts +37 -5
  42. package/src/processes/process-decisions/index.ts +7 -6
  43. package/src/shared/config/Environment.ts +10 -2
  44. package/src/shared/lib/composables/index.ts +1 -0
  45. package/src/shared/lib/composables/useMeetStatus.ts +96 -0
  46. package/src/shared/lib/consts/index.ts +1 -0
  47. package/src/shared/lib/consts/meet-statuses.ts +114 -0
  48. package/src/shared/lib/document/model/entity.ts +4 -2
  49. package/src/shared/lib/types/certificate/index.ts +6 -0
  50. package/src/shared/lib/types/document/index.ts +1 -1
  51. package/src/shared/lib/types/workspace.ts +24 -0
  52. package/src/shared/lib/utils/dates/index.ts +5 -0
  53. package/src/shared/lib/utils/dates/moment.ts +43 -2
  54. package/src/shared/lib/utils/dates/timezone.ts +75 -0
  55. package/src/shared/lib/utils/getNameFromCertificate.ts +108 -0
  56. package/src/shared/lib/utils/index.ts +1 -0
  57. package/src/shared/lib/utils/parseLinks.ts +10 -0
  58. package/src/shared/ui/AgendaNumberAvatar/AgendaNumberAvatar.vue +12 -0
  59. package/src/shared/ui/AgendaNumberAvatar/index.ts +1 -0
  60. package/src/shared/ui/BaseDocument/BaseDocument.vue +37 -8
  61. package/src/shared/ui/ExpandableDocument/ExpandableDocument.vue +49 -0
  62. package/src/shared/ui/ExpandableDocument/index.ts +1 -0
  63. package/src/shared/ui/MeetInfoCard/index.ts +1 -0
  64. package/src/shared/ui/MeetInfoCard/ui/MeetInfoCard.vue +62 -0
  65. package/src/shared/ui/MeetStatusBanner/index.ts +1 -0
  66. package/src/shared/ui/MeetStatusBanner/ui/MeetStatusBanner.vue +94 -0
  67. package/src/shared/ui/index.ts +1 -0
  68. package/src/widgets/Cooperative/Documents/ListOfDocuments/ui/DocumentsTable.vue +3 -3
  69. package/src/widgets/Cooperative/Orders/ListOfOrders/ui/ListOfOrdersWidget.vue +2 -2
  70. package/src/widgets/Cooperative/Payments/ListOfPayments/ui/ListOfPaymentsWidget.vue +2 -2
  71. package/src/widgets/Desktop/WorkspaceMenu/WorkspaceMenu.vue +74 -82
  72. package/src/widgets/Header/CommonHeader/CooperativeSettingsHeader.vue +1 -1
  73. package/src/widgets/Header/CommonHeader/ExtstoreHeader.vue +1 -1
  74. package/src/widgets/Header/CommonHeader/MainHeader.vue +6 -0
  75. package/src/widgets/Header/CommonHeader/UserSettingsHeader.vue +1 -1
  76. package/src/widgets/Meets/MeetCardsList/index.ts +1 -0
  77. package/src/widgets/Meets/MeetCardsList/ui/MeetCardsList.vue +55 -0
  78. package/src/widgets/Meets/MeetDetailsActions/MeetDetailsActions.vue +33 -15
  79. package/src/widgets/Meets/MeetDetailsAgenda/MeetDetailsAgenda.vue +34 -15
  80. package/src/widgets/Meets/MeetDetailsInfo/MeetDetailsInfo.vue +27 -0
  81. package/src/widgets/Meets/MeetDetailsInfo/index.ts +1 -0
  82. package/src/widgets/Meets/MeetDetailsResults/MeetDetailsResults.vue +129 -0
  83. package/src/widgets/Meets/MeetDetailsResults/index.ts +1 -0
  84. package/src/widgets/Meets/MeetDetailsVoting/MeetDetailsVoting.vue +221 -62
  85. package/src/widgets/Meets/MeetQuorumIndicator/MeetQuorumIndicator.vue +0 -0
  86. package/src/widgets/Meets/MeetQuorumIndicator/index.ts +1 -0
  87. package/src/widgets/Meets/MeetQuorumIndicator/ui/MeetQuorumIndicator.vue +35 -0
  88. package/src/widgets/Meets/MeetsTable/ui/MeetsTable.vue +56 -65
  89. package/src/widgets/NotificationCenter/NotificationCenter.vue +90 -0
  90. package/src/widgets/NotificationCenter/index.ts +1 -0
  91. package/src/widgets/Participants/ui/ParticipantsTable.vue +1 -1
  92. package/src/widgets/Questions/ui/QuestionsTable/QuestionsTable.vue +22 -10
  93. package/src/widgets/Questions/ui/VotingButtons/VotingButtons.vue +59 -14
  94. package/src/widgets/Registrator/AlreadyRegistered/AlreadyRegistered.vue +1 -1
  95. package/src/widgets/User/PaymentMethods/ui/PaymentMethods.vue +0 -1
  96. package/src-ssr/middlewares/injectEnv.ts +5 -1
  97. package/src/features/Meet/GenerateAgenda/index.ts +0 -1
  98. package/src/features/Meet/GenerateAgenda/model/index.ts +0 -18
  99. package/src/features/Meet/GenerateBallot/index.ts +0 -1
  100. package/src/features/Meet/GenerateBallot/model/index.ts +0 -18
  101. package/src/features/Meet/GenerateNotification/index.ts +0 -1
  102. package/src/features/Meet/GenerateNotification/model/index.ts +0 -18
  103. package/src/features/Meet/MeetDetailsManagement/index.ts +0 -1
  104. package/src/features/Meet/MeetDetailsManagement/model/index.ts +0 -121
  105. package/src/pages/Cooperative/ListOfMeets/model/index.ts +0 -1
  106. package/src/pages/Cooperative/ListOfMeets/model/model.ts +0 -117
  107. package/src/widgets/Meets/MeetDetailsActions/model.ts +0 -46
  108. package/src/widgets/Meets/MeetDetailsHeader/MeetDetailsHeader.vue +0 -40
  109. package/src/widgets/Meets/MeetDetailsHeader/index.ts +0 -1
  110. package/src/widgets/Meets/MeetDetailsVoting/model.ts +0 -117
  111. package/src/widgets/Meets/MeetInfoCard/ui/MeetInfoCard.vue +0 -38
  112. package/src/widgets/Meets/MeetInfoCard/ui/index.ts +0 -1
  113. /package/src/{widgets/Meets/MeetInfoCard → features/User/LoginRedirect}/index.ts +0 -0
@@ -1,35 +1,39 @@
1
1
  <template lang="pug">
2
- q-card(flat class="card-container q-pa-md" v-if="canManageMeet")
2
+ div
3
3
  div.row.q-col-gutter-md
4
4
  div.col-12.col-md-auto(v-if="canCloseBySecretary")
5
5
  q-btn(
6
- color="negative"
7
- icon="fa-solid fa-door-closed"
8
- label="Закрыть собрание (Секретарь)"
6
+ color="primary"
7
+ icon="fa-solid fa-signature"
8
+ label="Подписать протокол"
9
9
  @click="closeMeetBySecretary"
10
10
  :loading="isProcessing"
11
11
  )
12
12
  div.col-12.col-md-auto(v-if="canCloseByPresider")
13
13
  q-btn(
14
- color="negative"
15
- icon="fa-solid fa-door-closed"
16
- label="Закрыть собрание (Председатель)"
14
+ color="primary"
15
+ icon="fa-solid fa-stamp"
16
+ label="Утвердить протокол"
17
17
  @click="closeMeetByPresider"
18
18
  :loading="isProcessing"
19
19
  )
20
20
  div.col-12.col-md-auto(v-if="canRestartMeet")
21
21
  RestartMeet(
22
- :meet="meet"
23
22
  show-button
24
23
  @restart="handleRestartMeet"
24
+ :loading="isProcessing"
25
25
  )
26
+
26
27
  </template>
27
28
 
28
29
  <script setup lang="ts">
29
- import { ref } from 'vue'
30
+ import { ref, onMounted, watch } from 'vue'
31
+ import { useRouter } from 'vue-router'
32
+ import { useCloseMeet } from 'src/features/Meet/CloseMeetWithDecision/model'
33
+ import { useRestartMeet } from 'src/features/Meet/RestartMeet/model'
30
34
  import { RestartMeet } from 'src/features/Meet/RestartMeet'
35
+ import { useMeetStore } from 'src/entities/Meet'
31
36
  import type { IMeet } from 'src/entities/Meet'
32
- import { useMeetDetailsManagement } from 'src/features/Meet/MeetDetailsManagement'
33
37
 
34
38
  const props = defineProps<{
35
39
  meet: IMeet
@@ -38,14 +42,28 @@ const props = defineProps<{
38
42
  }>()
39
43
 
40
44
  const isProcessing = ref(false)
45
+ const meetStore = useMeetStore()
46
+ const router = useRouter()
47
+
48
+ // Устанавливаем текущее собрание в store при монтировании и обновлении пропсов
49
+ onMounted(() => {
50
+ meetStore.setCurrentMeet(props.meet)
51
+ })
52
+
53
+ watch(() => props.meet, (newMeet) => {
54
+ meetStore.setCurrentMeet(newMeet)
55
+ }, { deep: true })
41
56
 
42
57
  const {
43
- canManageMeet,
44
58
  canCloseBySecretary,
45
59
  canCloseByPresider,
46
- canRestartMeet,
47
60
  closeMeetBySecretary,
48
- closeMeetByPresider,
61
+ closeMeetByPresider
62
+ } = useCloseMeet(isProcessing)
63
+
64
+ const {
65
+ canRestartMeet,
49
66
  handleRestartMeet
50
- } = useMeetDetailsManagement(props.meet, props.coopname, props.meetHash, isProcessing)
51
- </script>
67
+ } = useRestartMeet(router, isProcessing)
68
+
69
+ </script>
@@ -1,29 +1,48 @@
1
1
  <template lang="pug">
2
- q-card(flat class="card-container q-pa-md")
3
- div.text-h6.q-mb-md Вопросы повестки
2
+ div
3
+ div.row.justify-center
4
+ div.text-h6.q-mt-md.full-width.text-center Повестка
4
5
 
5
- div.row.q-col-gutter-md
6
- div.col-12.col-md-6(v-for="(item, index) in meetAgendaItems" :key="index")
7
- q-card(flat bordered)
8
- q-card-section
9
- div.text-h6 {{ item.title }}
10
- q-separator.q-my-sm
11
- div.text-body1 {{ item.context }}
12
- q-separator.q-my-sm
13
- div.text-subtitle1.text-weight-bold Решение
14
- div.text-body2 {{ item.decision }}
6
+ div.col-12.col-md-12(v-for="(item, index) in meetAgendaItems" :key="index")
7
+ q-card(flat bordered).q-mb-md.q-mt-md.q-pt-md
8
+ q-card-section.q-pa-xs
9
+ div.q-mb-xs.flex.items-start.q-mb-lg.q-pa-xs
10
+ AgendaNumberAvatar(:number="index + 1" class="q-ma-md")
11
+ div.col
12
+ div.text-body1.text-weight-medium.q-mb-2 {{ item.title }}
13
+ div.text-caption.q-mb-1.q-mt-md
14
+ span.text-weight-bold Проект решения:
15
+ span.q-ml-xs {{ item.decision }}
16
+ div.text-caption.q-mt-md
17
+ span.text-weight-bold Приложения:
18
+ span.q-ml-xs(v-if="item.context" v-html="parseLinks(item.context)")
19
+ span.q-ml-xs(v-else) —
20
+ div.row.justify-center
21
+ SignNotificationButton(
22
+ v-if="coopname && meetHash"
23
+ :coopname="coopname"
24
+ :meetHash="meetHash"
25
+ )
15
26
  </template>
16
27
 
17
28
  <script setup lang="ts">
18
- import type { IMeet } from 'src/entities/Meet'
19
29
  import { computed } from 'vue'
30
+ import type { IMeet } from 'src/entities/Meet'
31
+ import { AgendaNumberAvatar } from 'src/shared/ui/AgendaNumberAvatar'
32
+ import { SignNotificationButton } from 'src/features/Meet/SignNotification/ui'
33
+ import { parseLinks } from 'src/shared/lib/utils'
20
34
 
21
35
  const props = defineProps<{
22
- meet: IMeet
36
+ meet: IMeet,
37
+ coopname?: string,
38
+ meetHash?: string
23
39
  }>()
24
40
 
41
+ const coopname = computed(() => props.coopname || '')
42
+ const meetHash = computed(() => props.meetHash || '')
43
+
25
44
  const meetAgendaItems = computed(() => {
26
45
  if (!props.meet) return []
27
46
  return props.meet.processing?.questions || []
28
47
  })
29
- </script>
48
+ </script>
@@ -0,0 +1,27 @@
1
+ <template lang="pug">
2
+ div
3
+ q-card(flat bordered).q-pa-md
4
+ MeetInfoCard(:meet="meet")
5
+ // Слот для действий (кнопки)
6
+ div.q-mt-md(v-if="$slots.actions")
7
+ slot(name="actions")
8
+ </template>
9
+
10
+ <script setup lang="ts">
11
+ import { MeetInfoCard } from 'src/shared/ui/MeetInfoCard'
12
+ import type { IMeet } from 'src/entities/Meet'
13
+
14
+ defineProps<{
15
+ meet: IMeet
16
+ }>()
17
+
18
+ </script>
19
+
20
+ <style lang="scss" scoped>
21
+ @import 'src/shared/ui/CardStyles/index.scss';
22
+
23
+ .meet-status {
24
+ font-size: 14px;
25
+ padding: 4px 8px;
26
+ }
27
+ </style>
@@ -0,0 +1 @@
1
+ export { default as MeetDetailsInfo } from './MeetDetailsInfo.vue'
@@ -0,0 +1,129 @@
1
+ <template lang="pug">
2
+ div(flat)
3
+ div.text-center.text-h6.q-mb-md РЕЗУЛЬТАТЫ
4
+
5
+ div.row.justify-center
6
+ // Отображаем документ собрания, если он есть
7
+ ExpandableDocument(
8
+ v-if="!!meet?.processed?.decisionAggregate"
9
+ :documentAggregate="meet.processed.decisionAggregate"
10
+ title="Протокол решения общего собрания пайщиков"
11
+ )
12
+ div.col-12.col-md-12(v-for="(item, index) in meetAgendaItems" :key="index")
13
+ q-card(flat bordered).q-mb-md
14
+ q-card-section
15
+ div.row
16
+ div.col-12.col-md-auto.flex.justify-center.q-pa-md
17
+ AgendaNumberAvatar(:number="item.number")
18
+ div.col-12.col-md
19
+
20
+ div.row.justify-between.items-center
21
+ div.text-h6 {{ item.title }}
22
+ q-badge(
23
+ :color="getResultBadgeColor(item)"
24
+ :label="getResultText(item)"
25
+ :icon="getResultIcon(item)"
26
+ floating
27
+ class="text-weight-bold"
28
+ )
29
+
30
+ q-separator.q-my-sm
31
+ div.text-body1(v-html="parseLinks(item.context)")
32
+ q-separator.q-my-sm
33
+
34
+ //- div.text-subtitle1.text-weight-bold Решение
35
+ //- div.text-body2 {{ item.decision }}
36
+ //- q-separator.q-my-sm
37
+
38
+ div.row.q-col-gutter-sm.q-mt-sm
39
+ // Новый push-дизайн карточек голосования
40
+ div.col-12.col-md-4
41
+ q-card(flat bordered :class="'q-pa-md q-mb-sm shadow-2 flex flex-center ' + getCardClass('for')")
42
+ q-card-section(class="text-center")
43
+ q-icon(name="thumb_up" :color="getCardSemanticColor('for')" size="32px")
44
+ div(:class="'text-weight-bold q-mt-sm text-' + getCardSemanticColor('for')") ЗА
45
+ div(:class="'text-h5 q-mt-xs text-' + getCardSemanticColor('for')") {{ item.votes_for }}
46
+ div.col-12.col-md-4
47
+ q-card(flat bordered :class="'q-pa-md q-mb-sm shadow-2 flex flex-center ' + getCardClass('against')")
48
+ q-card-section(class="text-center")
49
+ q-icon(name="thumb_down" :color="getCardSemanticColor('against')" size="32px")
50
+ div(:class="'text-weight-bold q-mt-sm text-' + getCardSemanticColor('against')") ПРОТИВ
51
+ div(:class="'text-h5 q-mt-xs text-' + getCardSemanticColor('against')") {{ item.votes_against }}
52
+
53
+
54
+ div.col-12.col-md-4
55
+ q-card(flat bordered :class="'q-pa-md q-mb-sm shadow-2 flex flex-center ' + getCardClass('abstained')")
56
+ q-card-section(class="text-center")
57
+ q-icon(name="pan_tool" :color="getCardSemanticColor('abstained')" size="32px")
58
+ div(:class="'text-weight-bold q-mt-sm text-' + getCardSemanticColor('abstained')") ВОЗДЕРЖАЛИСЬ
59
+ div(:class="'text-h5 q-mt-xs text-' + getCardSemanticColor('abstained')") {{ item.votes_abstained }}
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import type { IMeet } from 'src/entities/Meet'
64
+ import { computed } from 'vue'
65
+ import { ExpandableDocument } from 'src/shared/ui'
66
+ import { AgendaNumberAvatar } from 'src/shared/ui/AgendaNumberAvatar'
67
+ import { useQuasar } from 'quasar'
68
+ import { parseLinks } from 'src/shared/lib/utils'
69
+
70
+ const $q = useQuasar()
71
+ const isDark = computed(() => $q.dark.isActive)
72
+
73
+ const props = defineProps<{
74
+ meet: IMeet
75
+ }>()
76
+
77
+ const meetAgendaItems = computed(() => {
78
+ if (!props.meet || !props.meet.processed?.results) return []
79
+ return props.meet.processed.results || []
80
+ })
81
+
82
+ // Классы для карточек голосования с учётом темы
83
+ const getCardClass = (type: 'for' | 'against' | 'abstained') => {
84
+ if (type === 'for') return isDark.value ? 'bg-green-10 card-border-light' : 'bg-green-1'
85
+ if (type === 'against') return isDark.value ? 'bg-red-10 card-border-light' : 'bg-red-1'
86
+ if (type === 'abstained') return isDark.value ? 'bg-grey-9 card-border-light' : 'bg-grey-2'
87
+ return ''
88
+ }
89
+
90
+ // Цвет иконки и текста для карточки с учетом темы
91
+ const getCardSemanticColor = (type: 'for' | 'against' | 'abstained') => {
92
+ if (isDark.value) {
93
+ // В темной теме используем светлые цвета для контраста
94
+ if (type === 'for') return 'green-3'
95
+ if (type === 'against') return 'red-3'
96
+ if (type === 'abstained') return 'grey-4'
97
+ } else {
98
+ // В светлой теме используем стандартные цвета
99
+ if (type === 'for') return 'positive'
100
+ if (type === 'against') return 'negative'
101
+ if (type === 'abstained') return 'grey'
102
+ }
103
+ return ''
104
+ }
105
+
106
+ // Получение текста результата
107
+ const getResultText = (question: any) => {
108
+ if (question.accepted === undefined) return 'Нет данных'
109
+ return question.accepted ? 'ПРИНЯТО' : 'ОТКЛОНЕНО'
110
+ }
111
+
112
+ // Добавляю функции для иконки и цвета результата
113
+ const getResultIcon = (question: any) => {
114
+ if (question.accepted === undefined) return 'help_outline'
115
+ return question.accepted ? 'check_circle' : 'cancel'
116
+ }
117
+
118
+ const getResultBadgeColor = (question: any) => {
119
+ if (question.accepted === undefined) return 'grey-5'
120
+ return question.accepted ? 'positive' : 'negative'
121
+ }
122
+ </script>
123
+
124
+ <style scoped>
125
+ /* Светлая рамка для карточек в тёмном режиме */
126
+ .card-border-light {
127
+ border: 1.5px solid #fff2;
128
+ }
129
+ </style>
@@ -0,0 +1 @@
1
+ export { default as MeetDetailsResults } from './MeetDetailsResults.vue'
@@ -1,59 +1,86 @@
1
1
  <template lang="pug">
2
- q-card(flat class="card-container q-pa-md" v-if="canVote")
3
- div.text-h6.q-mb-md Голосование
4
-
5
- div.row.q-col-gutter-md
6
- div.col-12.col-md-6(v-for="(item, index) in meetAgendaItems" :key="index")
7
- q-card(flat bordered)
8
- q-card-section
9
- div.text-h6 {{ item.title }}
10
- q-separator.q-my-sm
11
- div.text-body1 {{ item.context }}
12
- q-separator.q-my-sm
13
- div.text-subtitle1.q-mb-sm Ваш голос:
14
- div.row.q-col-gutter-sm
15
- div.col-4
16
- q-radio(
17
- v-model="votes[index]"
18
- val="for"
19
- label="ЗА"
20
- color="positive"
21
- )
22
- div.col-4
23
- q-radio(
24
- v-model="votes[index]"
25
- val="against"
26
- label="ПРОТИВ"
27
- color="negative"
28
- )
29
- div.col-4
30
- q-radio(
31
- v-model="votes[index]"
32
- val="abstained"
33
- label="ВОЗДЕРЖАЛСЯ"
34
- color="grey"
35
- )
36
-
37
- div.row.justify-center.q-mt-lg
38
- q-btn.q-px-xl(
39
- color="primary"
40
- label="ГОЛОСОВАТЬ"
41
- size="lg"
42
- :loading="isVoting"
43
- @click="submitVote"
44
- :disable="!allVotesSelected"
45
- )
46
-
47
- q-banner(rounded class="bg-blue-1 text-blue-8 q-mt-md" v-if="isVotingNotStarted")
48
- div.text-center.text-subtitle1 Голосование еще не началось. Дата начала: {{ formattedOpenDate }}
49
- q-banner(rounded class="bg-blue-1 text-blue-8 q-mt-md" v-else-if="isVotingEnded")
50
- div.text-center.text-subtitle1 Голосование уже завершено. Дата окончания: {{ formattedCloseDate }}
2
+ div
3
+ // Баннер для уже проголосовавших пользователей
4
+ q-banner.q-mb-md(
5
+ v-if="meet?.processing?.isVoted"
6
+ rounded
7
+ color="positive"
8
+ text-color="white"
9
+ ).text-center
10
+ template(#avatar)
11
+ q-icon(name="how_to_vote" color="primary" size="50px" class="q-mt-md q-mb-md")
12
+ div.text-body1.text-weight-medium Вы уже приняли участие в голосовании. Ваш голос принят и учтен.
13
+
14
+
15
+ div(v-if="!meet?.processing?.isVoted", flat)
16
+ q-card-section.text-center
17
+ .text-h6 Голосование
18
+ q-card(flat bordered v-for="(item, index) in meetAgendaItems", :key="index").q-mb-md.q-pt-md
19
+ q-card-section.q-pa-xs
20
+ div.q-mb-xs.flex.items-start.q-mb-lg.q-pa-xs
21
+ AgendaNumberAvatar(:number="index + 1" class="q-ma-md")
22
+ div.col
23
+ div.text-body1.text-weight-medium.q-mb-2 {{ item.title }}
24
+ div.text-caption.q-mb-1.q-mt-md
25
+ span.text-weight-bold Проект решения:
26
+ span.q-ml-xs {{ item.decision }}
27
+ div.text-caption.q-mt-md
28
+ span.text-weight-bold Приложения:
29
+ span.q-ml-xs(v-if="item.context" v-html="parseLinks(item.context)")
30
+ span.q-ml-xs(v-else) —
31
+ q-separator.q-my-md
32
+ .text-subtitle1.q-mb-sm Ваш голос:
33
+ .row.q-col-gutter-sm
34
+ .col-12.col-md-4
35
+
36
+ label.vote-radio-wrapper.positive
37
+ q-radio(
38
+ v-model="votes[index]",
39
+ val="for",
40
+ color="positive",
41
+ size="lg",
42
+ label="ЗА"
43
+ )
44
+ .col-12.col-md-4
45
+ label.vote-radio-wrapper.negative
46
+ q-radio(
47
+ v-model="votes[index]",
48
+ val="against",
49
+ color="negative",
50
+ size="lg",
51
+ label="ПРОТИВ"
52
+ )
53
+ .col-12.col-md-4
54
+ label.vote-radio-wrapper.grey
55
+ q-radio(
56
+ v-model="votes[index]",
57
+ val="abstained",
58
+ color="grey",
59
+ size="lg",
60
+ label="ВОЗДЕРЖАЛСЯ"
61
+ )
62
+
63
+ q-separator
64
+ q-card-actions(align="center").q-pa-md
65
+ q-btn.q-px-xl(
66
+ color="primary",
67
+ label="ГОЛОСОВАТЬ",
68
+ size="lg",
69
+ :loading="isVoting",
70
+ @click="submitVote",
71
+ :disable="!allVotesSelected"
72
+ )
51
73
  </template>
52
74
 
53
75
  <script setup lang="ts">
54
- import { ref } from 'vue'
76
+ import { ref, onMounted, onUnmounted } from 'vue'
77
+ import { AgendaNumberAvatar } from 'src/shared/ui/AgendaNumberAvatar'
55
78
  import type { IMeet } from 'src/entities/Meet'
56
- import { useMeetDetailsVoting } from './model'
79
+ import { useSessionStore } from 'src/entities/Session'
80
+ import { FailAlert, SuccessAlert } from 'src/shared/api'
81
+ import { useSignDocument } from 'src/shared/lib/document'
82
+ import { useVoteOnMeet, type IVoteOnMeetInput } from 'src/features/Meet/VoteOnMeet'
83
+ import { parseLinks } from 'src/shared/lib/utils'
57
84
 
58
85
  const props = defineProps<{
59
86
  meet: IMeet
@@ -61,17 +88,149 @@ const props = defineProps<{
61
88
  meetHash: string
62
89
  }>()
63
90
 
64
- const isVoting = ref(false)
65
- const votes = ref<Record<number, 'for' | 'against' | 'abstained'>>({})
66
-
67
91
  const {
68
- canVote,
92
+ votes,
69
93
  meetAgendaItems,
70
94
  allVotesSelected,
71
- isVotingNotStarted,
72
- isVotingEnded,
73
- formattedOpenDate,
74
- formattedCloseDate,
75
- submitVote
76
- } = useMeetDetailsVoting(props.meet, props.coopname, props.meetHash, votes, isVoting)
77
- </script>
95
+ setMeet,
96
+ voteOnMeet,
97
+ resetVotes,
98
+ generateBallot
99
+ } = useVoteOnMeet()
100
+
101
+ const sessionStore = useSessionStore()
102
+ const { signDocument } = useSignDocument()
103
+
104
+ const isVoting = ref(false)
105
+
106
+ // Устанавливаем собрание из пропсов при монтировании
107
+ onMounted(() => {
108
+ // Устанавливаем собрание из пропсов в композабл
109
+ setMeet(props.meet)
110
+
111
+ // Сбрасываем голоса при открытии компонента
112
+ resetVotes()
113
+ })
114
+
115
+ // Очищаем ссылку на собрание при размонтировании
116
+ onUnmounted(() => {
117
+ setMeet(null)
118
+ resetVotes()
119
+ })
120
+
121
+ const submitVote = async () => {
122
+ if (!allVotesSelected.value) return
123
+
124
+ isVoting.value = true
125
+ try {
126
+ const generatedBallot = await generateBallot({
127
+ coopname: props.coopname,
128
+ username: sessionStore.username,
129
+ meet_hash: props.meetHash,
130
+ answers: meetAgendaItems.value.map((question, index) => ({
131
+ id: question.id.toString(),
132
+ number: question.number.toString(),
133
+ vote: votes.value[index]
134
+ }))
135
+ })
136
+
137
+ const signedBallot = await signDocument(generatedBallot, sessionStore.username)
138
+
139
+ const vote: IVoteOnMeetInput = {
140
+ coopname: props.coopname,
141
+ hash: props.meetHash,
142
+ ballot: signedBallot,
143
+ username: sessionStore.username,
144
+ votes: meetAgendaItems.value.map((item, index) => ({
145
+ question_id: item.id,
146
+ vote: votes.value[index]
147
+ }))
148
+ }
149
+
150
+ await voteOnMeet(vote)
151
+ SuccessAlert('Ваш голос успешно отправлен')
152
+ } catch (error: any) {
153
+ console.error(error)
154
+ FailAlert(error)
155
+ } finally {
156
+ isVoting.value = false
157
+ }
158
+ }
159
+ </script>
160
+
161
+ <style scoped>
162
+ .vote-radio-wrapper {
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ background: #f5f7fa;
167
+ border-radius: 16px;
168
+ box-shadow: 0 2px 12px 0 rgba(0,0,0,0.07);
169
+ padding: 18px 8px 12px 8px;
170
+ margin-bottom: 8px;
171
+ transition: box-shadow 0.2s, background 0.2s, color 0.2s;
172
+ color: black;
173
+ cursor: pointer;
174
+ }
175
+ .vote-radio-wrapper.positive {
176
+ background: #e8f5e9;
177
+ }
178
+ .vote-radio-wrapper.negative {
179
+ background: #ffebee;
180
+ }
181
+ .vote-radio-wrapper.grey {
182
+ background: #eceff1;
183
+ }
184
+ .vote-radio-wrapper:hover {
185
+ box-shadow: 0 4px 24px 0 rgba(0,0,0,0.13);
186
+ background: #e3eafc;
187
+ }
188
+ .vote-icon {
189
+ margin-left: 10px;
190
+ }
191
+ .q-radio__label {
192
+ font-size: 1.2em;
193
+ font-weight: bold;
194
+ }
195
+
196
+ /* --- DARK THEME SUPPORT --- */
197
+ .body--dark .vote-radio-wrapper,
198
+ .q-dark .vote-radio-wrapper {
199
+ background: #23272f;
200
+ color: #fff;
201
+ }
202
+ .body--dark .vote-radio-wrapper.positive,
203
+ .q-dark .vote-radio-wrapper.positive {
204
+ background: #295b36;
205
+ }
206
+ .body--dark .vote-radio-wrapper.negative,
207
+ .q-dark .vote-radio-wrapper.negative {
208
+ background: #5b2323;
209
+ }
210
+ .body--dark .vote-radio-wrapper.grey,
211
+ .q-dark .vote-radio-wrapper.grey {
212
+ background: #2c313a;
213
+ }
214
+ .body--dark .q-radio__label,
215
+ .q-dark .q-radio__label {
216
+ color: #fff;
217
+ }
218
+
219
+ /* Кастомизация кружка radio для тёмной темы */
220
+ .body--dark :deep(.vote-radio-wrapper .q-radio__inner),
221
+ .q-dark :deep(.vote-radio-wrapper .q-radio__inner) {
222
+ border-color: #fff !important;
223
+ }
224
+ .body--dark :deep(.vote-radio-wrapper.positive .q-radio__inner),
225
+ .q-dark :deep(.vote-radio-wrapper.positive .q-radio__inner) {
226
+ border-color: #4caf50 !important;
227
+ }
228
+ .body--dark :deep(.vote-radio-wrapper.negative .q-radio__inner),
229
+ .q-dark :deep(.vote-radio-wrapper.negative .q-radio__inner) {
230
+ border-color: #f44336 !important;
231
+ }
232
+ .body--dark :deep(.vote-radio-wrapper.grey .q-radio__inner),
233
+ .q-dark :deep(.vote-radio-wrapper.grey .q-radio__inner) {
234
+ border-color: #b0bec5 !important;
235
+ }
236
+ </style>
@@ -0,0 +1 @@
1
+ export { default as MeetQuorumIndicator } from './ui/MeetQuorumIndicator.vue'
@@ -0,0 +1,35 @@
1
+ <template lang="pug">
2
+ div(flat bordered).info-card.hover
3
+ q-card(flat bordered).q-pa-md
4
+ div.text-h6.q-mb-xs.full-width.text-center Явка
5
+
6
+ div.row
7
+ div.col-12
8
+ q-linear-progress(
9
+ :value="(meet.processing?.meet?.current_quorum_percent ?? 0) / 100"
10
+ :buffer="(meet.processing?.meet?.quorum_percent ?? 0) / 100"
11
+ track-color="lime"
12
+ size="50px"
13
+ rounded
14
+ ).bg-grey
15
+ div.absolute-full.flex.flex-center
16
+ div.text-center
17
+ div.text-white.text-weight-medium(style="font-size: 22px; line-height: 1.2;")
18
+ | {{ (Math.round((meet.processing?.meet?.current_quorum_percent ?? 0) * 10) / 10).toFixed(1) }}%
19
+ div.q-mt-xs.full-width.text-center.text-caption.text-grey-7
20
+ | Собрание состоится при явке не менее {{ meet.processing?.meet?.quorum_percent ?? 0 }}% участников
21
+
22
+
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import type { IMeet } from 'src/entities/Meet'
27
+
28
+ defineProps<{
29
+ meet: IMeet
30
+ }>()
31
+ </script>
32
+
33
+ <style lang="scss" scoped>
34
+ @import 'src/shared/ui/CardStyles/index.scss';
35
+ </style>