shared-ritm 1.2.130 → 1.2.131

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 (135) hide show
  1. package/README.md +103 -103
  2. package/dist/index.css +1 -1
  3. package/dist/shared-ritm.es.js +3042 -2991
  4. package/dist/shared-ritm.umd.js +146 -146
  5. package/dist/types/api/services/PhotoService.d.ts +40 -0
  6. package/dist/types/api/services/ScheduleService.d.ts +13 -0
  7. package/dist/types/api/services/VideoService.d.ts +18 -9
  8. package/dist/types/api/types/Api_Schedule.d.ts +26 -0
  9. package/dist/types/api/types/Api_Video.d.ts +4 -0
  10. package/dist/types/index.d.ts +3 -1
  11. package/package.json +64 -64
  12. package/src/App.vue +2461 -2461
  13. package/src/api/services/AuthService.ts +53 -53
  14. package/src/api/services/BrigadesService.ts +32 -32
  15. package/src/api/services/CommentsService.ts +24 -24
  16. package/src/api/services/ControlsService.ts +73 -73
  17. package/src/api/services/EquipmentService.ts +29 -29
  18. package/src/api/services/FileService.ts +17 -17
  19. package/src/api/services/GanttService.ts +17 -17
  20. package/src/api/services/InstrumentsService.ts +63 -63
  21. package/src/api/services/MetricsService.ts +110 -110
  22. package/src/api/services/ModulesService.ts +27 -27
  23. package/src/api/services/ProjectsService.ts +83 -83
  24. package/src/api/services/RepairsService.ts +123 -123
  25. package/src/api/services/ScheduleService.ts +34 -0
  26. package/src/api/services/SearchService.ts +22 -22
  27. package/src/api/services/TasksService.ts +145 -145
  28. package/src/api/services/UserService.ts +101 -101
  29. package/src/api/services/VideoService.ts +103 -71
  30. package/src/api/settings/ApiService.ts +124 -124
  31. package/src/api/types/Api_Auth.ts +86 -86
  32. package/src/api/types/Api_Brigades.ts +36 -36
  33. package/src/api/types/Api_Comment.ts +40 -40
  34. package/src/api/types/Api_Controls.ts +93 -93
  35. package/src/api/types/Api_Equipment.ts +3 -3
  36. package/src/api/types/Api_Files.ts +7 -7
  37. package/src/api/types/Api_Instruments.ts +136 -136
  38. package/src/api/types/Api_Modules.ts +21 -21
  39. package/src/api/types/Api_Projects.ts +60 -60
  40. package/src/api/types/Api_Repairs.ts +117 -117
  41. package/src/api/types/Api_Schedule.ts +29 -0
  42. package/src/api/types/Api_Search.ts +77 -77
  43. package/src/api/types/Api_Service.ts +9 -9
  44. package/src/api/types/Api_Tasks.ts +319 -319
  45. package/src/api/types/Api_User.ts +117 -117
  46. package/src/api/types/Api_Video.ts +145 -140
  47. package/src/common/app-button/AppButton.vue +173 -173
  48. package/src/common/app-checkbox/AppCheckbox.vue +26 -26
  49. package/src/common/app-date-picker/AppDatePicker.vue +81 -81
  50. package/src/common/app-datepicker/AppDatepicker.vue +165 -165
  51. package/src/common/app-dialogs/AppConfirmDialog.vue +99 -99
  52. package/src/common/app-dropdown/AppDropdown.vue +37 -37
  53. package/src/common/app-icon/AppIcon.vue +108 -108
  54. package/src/common/app-input/AppInput.vue +148 -148
  55. package/src/common/app-input-new/AppInputNew.vue +175 -175
  56. package/src/common/app-input-search/AppInputSearch.vue +174 -174
  57. package/src/common/app-layout/AppLayout.vue +84 -84
  58. package/src/common/app-layout/components/AppLayoutHeader.vue +273 -273
  59. package/src/common/app-layout/components/AppLayoutPage.vue +16 -16
  60. package/src/common/app-loader/index.vue +43 -43
  61. package/src/common/app-page-layout/AppPageLayout.vue +122 -122
  62. package/src/common/app-select/AppSelect.vue +157 -157
  63. package/src/common/app-sheet/AppSheet.vue +120 -120
  64. package/src/common/app-sheet-new/AppSheetNew.vue +246 -246
  65. package/src/common/app-sidebar/AppSidebar.vue +168 -168
  66. package/src/common/app-sidebar/components/SidebarMenu.vue +37 -37
  67. package/src/common/app-sidebar/components/SidebarMenuItem.vue +148 -148
  68. package/src/common/app-table/AppTable.vue +308 -308
  69. package/src/common/app-table/AppTableLayout.vue +137 -137
  70. package/src/common/app-table/components/ModalSelect.vue +281 -281
  71. package/src/common/app-table/components/TableModal.vue +356 -356
  72. package/src/common/app-table/components/TablePagination.vue +152 -152
  73. package/src/common/app-table/controllers/useBaseTable.ts +45 -45
  74. package/src/common/app-table/controllers/useColumnSelector.ts +38 -38
  75. package/src/common/app-table/controllers/useTableModel.ts +102 -102
  76. package/src/common/app-toggle/AppToggle.vue +24 -24
  77. package/src/common/app-wrapper/AppWrapper.vue +28 -28
  78. package/src/global.d.ts +1 -1
  79. package/src/icons/components/arrow-down-icon.vue +25 -25
  80. package/src/icons/components/arrow-frame-icon.vue +19 -19
  81. package/src/icons/components/arrow-square.vue +22 -22
  82. package/src/icons/components/table-filter-icon.vue +30 -30
  83. package/src/icons/dialogs/RemoveIcon.vue +12 -12
  84. package/src/icons/dialogs/SafetyIcon.vue +12 -12
  85. package/src/icons/header/NotificationIcon.vue +18 -18
  86. package/src/icons/header/PersonIcon.vue +11 -11
  87. package/src/icons/header/SettingIcon.vue +14 -14
  88. package/src/icons/header/flashIcon.vue +24 -24
  89. package/src/icons/header/searchStatusIcon.vue +24 -24
  90. package/src/icons/header/smallCapsIcon.vue +34 -34
  91. package/src/icons/sidebar/assign-module-icon.vue +36 -36
  92. package/src/icons/sidebar/instrument-history-icon.vue +32 -32
  93. package/src/icons/sidebar/instrument-order-icon.vue +38 -38
  94. package/src/icons/sidebar/instrument-work-zone-icon.vue +18 -18
  95. package/src/icons/sidebar/instruments-icon.vue +45 -45
  96. package/src/icons/sidebar/logo-icon.vue +15 -15
  97. package/src/icons/sidebar/logout-icon.vue +13 -13
  98. package/src/icons/sidebar/modules-icon.vue +16 -16
  99. package/src/icons/sidebar/notifications-icon.vue +24 -24
  100. package/src/icons/sidebar/order-icon.vue +44 -44
  101. package/src/icons/sidebar/pass-icon.vue +38 -38
  102. package/src/icons/sidebar/positions-icon.vue +42 -42
  103. package/src/icons/sidebar/preorder-icon.vue +19 -19
  104. package/src/icons/sidebar/projects-icon.vue +31 -31
  105. package/src/icons/sidebar/repair-object-icon.vue +18 -18
  106. package/src/icons/sidebar/repairs-icon.vue +20 -20
  107. package/src/icons/sidebar/roles-icon.vue +26 -26
  108. package/src/icons/sidebar/status-history-icon.vue +24 -24
  109. package/src/icons/sidebar/tasks-icon.vue +28 -28
  110. package/src/icons/sidebar/tasks_tasks-icon.vue +39 -39
  111. package/src/icons/sidebar/tasks_today-icon.vue +27 -27
  112. package/src/icons/sidebar/teams-icon.vue +32 -32
  113. package/src/icons/sidebar/user-icon.vue +18 -18
  114. package/src/icons/sidebar/users-icon.vue +46 -46
  115. package/src/icons/sidebar/videosources-icon.vue +19 -19
  116. package/src/icons/sidebar/videowall-icon.vue +13 -13
  117. package/src/icons/sidebar/videozones-icon.vue +21 -21
  118. package/src/icons/sidebar/warehouses-icon.vue +43 -43
  119. package/src/icons/sidebar/workshop-icon.vue +100 -100
  120. package/src/icons/sidebar/workzones-icon.vue +22 -22
  121. package/src/icons/task/attention-icon.vue +13 -13
  122. package/src/icons/task/clock-icon.vue +10 -10
  123. package/src/icons/task/delete-icon.vue +10 -10
  124. package/src/icons/task/fire-icon.vue +16 -16
  125. package/src/index.ts +126 -123
  126. package/src/main.ts +28 -28
  127. package/src/quasar-user-options.ts +17 -17
  128. package/src/router/index.ts +10 -10
  129. package/src/shared/styles/general.css +124 -124
  130. package/src/shims-vue.d.ts +5 -5
  131. package/src/styles/variables.sass +12 -12
  132. package/src/utils/confirm.ts +12 -12
  133. package/src/utils/faceApiHelper.ts +132 -132
  134. package/src/utils/helpers.ts +59 -59
  135. package/src/utils/notification.ts +9 -9
@@ -1,356 +1,356 @@
1
- <template>
2
- <q-dialog :model-value="modelValue" @update:model-value="toggleModal">
3
- <q-card style="min-width: 700px">
4
- <q-card-section>
5
- <div class="modal-title">{{ title }}</div>
6
- </q-card-section>
7
-
8
- <q-card-section>
9
- <q-form ref="formRef" @submit.prevent="submit">
10
- <div v-for="field in fields" :key="field.key" class="field-wrapper">
11
- <label v-if="field.type === 'text'" class="field-label">
12
- {{ field.label }}
13
- <span v-if="field.rules?.length && mode !== 'view'" class="required">*</span>
14
- </label>
15
-
16
- <q-input
17
- v-if="field.type === 'text'"
18
- v-model="formData[field.key]"
19
- :rules="field.rules"
20
- :readonly="mode === 'view' || (field.key === 'uuid' && mode === 'edit')"
21
- filled
22
- :placeholder="field.placeholder"
23
- >
24
- <template #append>
25
- <q-icon
26
- v-if="mode !== 'view' && formData[field.key] && !(field.key === 'uuid' && mode === 'edit')"
27
- name="close"
28
- class="cursor-pointer clear-input"
29
- @click="() => handleClear(field.key)"
30
- />
31
- <q-btn
32
- v-if="field.key === 'uuid' && mode === 'create'"
33
- flat
34
- no-caps
35
- label="UUID"
36
- size="sm"
37
- class="q-ml-sm uuid-btn"
38
- @click="generateUuid"
39
- />
40
- <q-icon
41
- v-else-if="field.key === 'uuid' && mode !== 'create'"
42
- name="content_copy"
43
- class="cursor-pointer q-ml-sm copy-icon"
44
- color="primary"
45
- @click="copyToClipboard(formData[field.key])"
46
- />
47
- </template>
48
- </q-input>
49
-
50
- <app-modal-select
51
- v-else-if="field.type === 'select'"
52
- v-model="formData[field.key]"
53
- :options="filteredOptions[field.key] || field.options"
54
- :rules="field.rules"
55
- :placeholder="mode === 'view' ? '' : field.placeholder"
56
- :multiple="true"
57
- :show-chip="true"
58
- :is-disabled="mode === 'view'"
59
- :loading="field.loading"
60
- :label="field.label"
61
- :is-show-required="mode !== 'view'"
62
- empty-text="Ничего не найдено"
63
- option-label="label"
64
- option-value="value"
65
- chip-color="#e9eff9"
66
- @update:search="val => field.onSearch?.(val)"
67
- @update:scroll="() => field.onScroll?.()"
68
- @clear="
69
- () => {
70
- handleClear(field.key)
71
- nextTick(() => formRef.value?.validate())
72
- }
73
- "
74
- />
75
- </div>
76
- </q-form>
77
- </q-card-section>
78
-
79
- <q-card-actions align="center">
80
- <q-btn v-if="mode === 'view'" class="remove" flat label="Удалить" :loading="loading" @click="emit('delete')" />
81
- <q-btn
82
- v-if="mode !== 'view'"
83
- class="confirm"
84
- flat
85
- :label="mode === 'edit' ? 'Сохранить' : 'Создать'"
86
- :disable="isSubmitDisabled"
87
- :loading="loading"
88
- @click="submit"
89
- />
90
- <q-btn v-else class="confirm" flat label="Редактировать" :disable="loading" @click="$emit('edit')" />
91
- <q-btn class="cancel" flat label="Закрыть" :disable="loading" @click="close" />
92
- </q-card-actions>
93
- </q-card>
94
- </q-dialog>
95
- </template>
96
-
97
- <script setup lang="ts">
98
- import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
99
- import { useQuasar } from 'quasar'
100
- import AppModalSelect from '../components/ModalSelect.vue'
101
- import { notificationSettings } from '@/utils/notification'
102
- import { isEqual, normalizeValue, uuidv4 } from '@/utils/helpers'
103
-
104
- type ModalMode = 'view' | 'edit' | 'create'
105
- type FieldType = 'text' | 'select'
106
-
107
- interface FieldOption {
108
- label: string
109
- value: string
110
- }
111
- interface FieldSchema {
112
- key: string
113
- label: string
114
- type: FieldType
115
- rules?: ((val: any) => boolean | string)[]
116
- options?: FieldOption[]
117
- placeholder?: string
118
- onSearch?: (val: string) => void
119
- onScroll?: () => void
120
- loading?: boolean
121
- }
122
-
123
- const props = defineProps<{
124
- modelValue: boolean
125
- title: string
126
- mode: ModalMode
127
- fields: FieldSchema[]
128
- initialData?: Record<string, any>
129
- loading?: boolean
130
- }>()
131
- const emit = defineEmits<{
132
- (e: 'update:modelValue', val: boolean, hasChanges?: boolean): void
133
- (e: 'submit', data: Record<string, any>): void
134
- (e: 'edit'): void
135
- (e: 'delete'): void
136
- }>()
137
-
138
- const $q = useQuasar()
139
- const formRef = ref()
140
- const formData = ref<Record<string, any>>({})
141
- const filteredOptions = ref<Record<string, FieldOption[]>>({})
142
- const isSubmitDisabled = computed(() => {
143
- if (props.mode === 'view') return false
144
-
145
- const hasEmptyRequired = props.fields.some(field => {
146
- const isRequired = field.rules?.some(rule => rule('') !== true)
147
- const val = formData.value[field.key]
148
- const normalized = normalizeValue(val)
149
-
150
- const empty =
151
- normalized === null ||
152
- normalized === undefined ||
153
- (typeof normalized === 'string' && normalized.trim() === '') ||
154
- (Array.isArray(normalized) && normalized.length === 0)
155
-
156
- return isRequired && empty
157
- })
158
-
159
- if (hasEmptyRequired) return true
160
-
161
- const hasChanges = props.fields.some(field => {
162
- const key = field.key
163
- const current = normalizeValue(formData.value[key])
164
- const initial = normalizeValue(props.initialData?.[key])
165
- return !isEqual(current, initial)
166
- })
167
-
168
- return !hasChanges
169
- })
170
- function toggleModal(val: boolean) {
171
- if (val) {
172
- emit('update:modelValue', val)
173
- } else {
174
- close()
175
- }
176
- }
177
- function checkChanges() {
178
- if (props.mode === 'view') {
179
- return false
180
- }
181
-
182
- if (!props.initialData || !Object.keys(props.initialData).length) {
183
- return props.fields?.some(
184
- field =>
185
- formData.value?.[field.key] &&
186
- !(Array.isArray(formData.value?.[field.key]) && !formData.value?.[field.key].length),
187
- )
188
- }
189
-
190
- return props.fields?.some(field => !isEqual(formData.value?.[field.key], props.initialData?.[field.key]))
191
- }
192
- function close() {
193
- emit('update:modelValue', false, checkChanges())
194
- }
195
- function submit() {
196
- formRef.value?.validate().then(ok => {
197
- if (!ok) return
198
-
199
- const changed: Record<string, any> = {}
200
-
201
- for (const field of props.fields) {
202
- const key = field.key
203
- const current = formData.value[key]
204
- const initial = props.initialData?.[key]
205
-
206
- const normalizedCurrent = field.type === 'select' ? normalizeValue(current) : current
207
- const normalizedInitial = field.type === 'select' ? normalizeValue(initial) : initial
208
-
209
- if (!isEqual(normalizedCurrent, normalizedInitial)) {
210
- changed[key] = normalizedCurrent
211
- }
212
- }
213
-
214
- emit('submit', changed)
215
- })
216
- }
217
- function handleClear(key: string) {
218
- const field = props.fields.find(f => f.key === key)
219
- formData.value[key] = field?.type === 'select' ? [] : ''
220
-
221
- nextTick(() => {
222
- formRef.value?.validate()
223
- })
224
- }
225
- function generateUuid() {
226
- formData.value.uuid = uuidv4()
227
- }
228
- function copyToClipboard(text: string) {
229
- navigator.clipboard.writeText(text).then(() => {
230
- $q.notify(notificationSettings('success', 'UUID скопирован'))
231
- })
232
- }
233
-
234
- watch(
235
- () => props.modelValue,
236
- val => {
237
- if (val) {
238
- formData.value = { ...(props.initialData ?? {}) }
239
- }
240
- },
241
- { immediate: true },
242
- )
243
- </script>
244
-
245
- <style lang="scss">
246
- .custom-select-menu {
247
- max-height: 250px !important;
248
- overflow-y: auto !important;
249
- }
250
- </style>
251
- <style scoped lang="scss">
252
- .uuid-btn {
253
- height: 32px;
254
- padding: 0 10px;
255
- border: 1px solid #3f8cff;
256
- color: #3f8cff;
257
- font-weight: 700;
258
- font-size: 14px;
259
- background: white;
260
- border-radius: 6px;
261
- }
262
-
263
- .q-card {
264
- border-radius: 12px;
265
- background: #fff;
266
- box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
267
- font-family: NunitoSansFont, sans-serif;
268
-
269
- .modal-title {
270
- color: #1d425d;
271
- text-align: center;
272
- font-size: 32px;
273
- font-weight: 700;
274
- padding: 18px 29px;
275
- }
276
-
277
- .q-card__section {
278
- padding: 0;
279
- }
280
-
281
- .q-form {
282
- padding: 0 29px;
283
- }
284
-
285
- .field-wrapper {
286
- display: flex;
287
- flex-direction: column;
288
- margin-bottom: 15px;
289
- }
290
-
291
- .field-label {
292
- font-size: 14px;
293
- font-weight: 700;
294
- color: #7d8592;
295
- }
296
- ::v-deep(.q-placeholder) {
297
- color: #7d8592;
298
- }
299
- ::v-deep(.q-field__control) {
300
- border-radius: 8px;
301
- border: 1px solid #d8e0f0;
302
- background: #fff;
303
- box-shadow: 0px 1px 2px 0px rgba(184, 200, 224, 0.22);
304
- }
305
-
306
- ::v-deep(.q-field--filled .q-field__control:before) {
307
- background: #fff !important;
308
- border: none;
309
- }
310
-
311
- ::v-deep(.q-field--with-bottom) {
312
- padding-bottom: 0;
313
- }
314
-
315
- ::v-deep(.q-field__bottom) {
316
- padding: 0;
317
- }
318
-
319
- .clear-input {
320
- color: #d8e0f0;
321
- }
322
-
323
- .required {
324
- color: #f65160;
325
- font-weight: bold;
326
- }
327
-
328
- &__actions {
329
- padding: 18px 0;
330
- border-radius: 0px 0px 16px 16px;
331
- background: #f4f9fd;
332
-
333
- button {
334
- padding: 13px 37px;
335
- border-radius: 4px;
336
- font-size: 16px;
337
- font-weight: 700;
338
- text-transform: none;
339
-
340
- &.remove {
341
- border: 1px solid #f65160;
342
- color: #f65160;
343
- }
344
- &.confirm {
345
- background: #3f8cff;
346
- color: #fff;
347
- }
348
-
349
- &.cancel {
350
- color: #3f8cff;
351
- border: 1px solid #3f8cff;
352
- }
353
- }
354
- }
355
- }
356
- </style>
1
+ <template>
2
+ <q-dialog :model-value="modelValue" @update:model-value="toggleModal">
3
+ <q-card style="min-width: 700px">
4
+ <q-card-section>
5
+ <div class="modal-title">{{ title }}</div>
6
+ </q-card-section>
7
+
8
+ <q-card-section>
9
+ <q-form ref="formRef" @submit.prevent="submit">
10
+ <div v-for="field in fields" :key="field.key" class="field-wrapper">
11
+ <label v-if="field.type === 'text'" class="field-label">
12
+ {{ field.label }}
13
+ <span v-if="field.rules?.length && mode !== 'view'" class="required">*</span>
14
+ </label>
15
+
16
+ <q-input
17
+ v-if="field.type === 'text'"
18
+ v-model="formData[field.key]"
19
+ :rules="field.rules"
20
+ :readonly="mode === 'view' || (field.key === 'uuid' && mode === 'edit')"
21
+ filled
22
+ :placeholder="field.placeholder"
23
+ >
24
+ <template #append>
25
+ <q-icon
26
+ v-if="mode !== 'view' && formData[field.key] && !(field.key === 'uuid' && mode === 'edit')"
27
+ name="close"
28
+ class="cursor-pointer clear-input"
29
+ @click="() => handleClear(field.key)"
30
+ />
31
+ <q-btn
32
+ v-if="field.key === 'uuid' && mode === 'create'"
33
+ flat
34
+ no-caps
35
+ label="UUID"
36
+ size="sm"
37
+ class="q-ml-sm uuid-btn"
38
+ @click="generateUuid"
39
+ />
40
+ <q-icon
41
+ v-else-if="field.key === 'uuid' && mode !== 'create'"
42
+ name="content_copy"
43
+ class="cursor-pointer q-ml-sm copy-icon"
44
+ color="primary"
45
+ @click="copyToClipboard(formData[field.key])"
46
+ />
47
+ </template>
48
+ </q-input>
49
+
50
+ <app-modal-select
51
+ v-else-if="field.type === 'select'"
52
+ v-model="formData[field.key]"
53
+ :options="filteredOptions[field.key] || field.options"
54
+ :rules="field.rules"
55
+ :placeholder="mode === 'view' ? '' : field.placeholder"
56
+ :multiple="true"
57
+ :show-chip="true"
58
+ :is-disabled="mode === 'view'"
59
+ :loading="field.loading"
60
+ :label="field.label"
61
+ :is-show-required="mode !== 'view'"
62
+ empty-text="Ничего не найдено"
63
+ option-label="label"
64
+ option-value="value"
65
+ chip-color="#e9eff9"
66
+ @update:search="val => field.onSearch?.(val)"
67
+ @update:scroll="() => field.onScroll?.()"
68
+ @clear="
69
+ () => {
70
+ handleClear(field.key)
71
+ nextTick(() => formRef.value?.validate())
72
+ }
73
+ "
74
+ />
75
+ </div>
76
+ </q-form>
77
+ </q-card-section>
78
+
79
+ <q-card-actions align="center">
80
+ <q-btn v-if="mode === 'view'" class="remove" flat label="Удалить" :loading="loading" @click="emit('delete')" />
81
+ <q-btn
82
+ v-if="mode !== 'view'"
83
+ class="confirm"
84
+ flat
85
+ :label="mode === 'edit' ? 'Сохранить' : 'Создать'"
86
+ :disable="isSubmitDisabled"
87
+ :loading="loading"
88
+ @click="submit"
89
+ />
90
+ <q-btn v-else class="confirm" flat label="Редактировать" :disable="loading" @click="$emit('edit')" />
91
+ <q-btn class="cancel" flat label="Закрыть" :disable="loading" @click="close" />
92
+ </q-card-actions>
93
+ </q-card>
94
+ </q-dialog>
95
+ </template>
96
+
97
+ <script setup lang="ts">
98
+ import { ref, watch, defineProps, defineEmits, nextTick, computed } from 'vue'
99
+ import { useQuasar } from 'quasar'
100
+ import AppModalSelect from '../components/ModalSelect.vue'
101
+ import { notificationSettings } from '@/utils/notification'
102
+ import { isEqual, normalizeValue, uuidv4 } from '@/utils/helpers'
103
+
104
+ type ModalMode = 'view' | 'edit' | 'create'
105
+ type FieldType = 'text' | 'select'
106
+
107
+ interface FieldOption {
108
+ label: string
109
+ value: string
110
+ }
111
+ interface FieldSchema {
112
+ key: string
113
+ label: string
114
+ type: FieldType
115
+ rules?: ((val: any) => boolean | string)[]
116
+ options?: FieldOption[]
117
+ placeholder?: string
118
+ onSearch?: (val: string) => void
119
+ onScroll?: () => void
120
+ loading?: boolean
121
+ }
122
+
123
+ const props = defineProps<{
124
+ modelValue: boolean
125
+ title: string
126
+ mode: ModalMode
127
+ fields: FieldSchema[]
128
+ initialData?: Record<string, any>
129
+ loading?: boolean
130
+ }>()
131
+ const emit = defineEmits<{
132
+ (e: 'update:modelValue', val: boolean, hasChanges?: boolean): void
133
+ (e: 'submit', data: Record<string, any>): void
134
+ (e: 'edit'): void
135
+ (e: 'delete'): void
136
+ }>()
137
+
138
+ const $q = useQuasar()
139
+ const formRef = ref()
140
+ const formData = ref<Record<string, any>>({})
141
+ const filteredOptions = ref<Record<string, FieldOption[]>>({})
142
+ const isSubmitDisabled = computed(() => {
143
+ if (props.mode === 'view') return false
144
+
145
+ const hasEmptyRequired = props.fields.some(field => {
146
+ const isRequired = field.rules?.some(rule => rule('') !== true)
147
+ const val = formData.value[field.key]
148
+ const normalized = normalizeValue(val)
149
+
150
+ const empty =
151
+ normalized === null ||
152
+ normalized === undefined ||
153
+ (typeof normalized === 'string' && normalized.trim() === '') ||
154
+ (Array.isArray(normalized) && normalized.length === 0)
155
+
156
+ return isRequired && empty
157
+ })
158
+
159
+ if (hasEmptyRequired) return true
160
+
161
+ const hasChanges = props.fields.some(field => {
162
+ const key = field.key
163
+ const current = normalizeValue(formData.value[key])
164
+ const initial = normalizeValue(props.initialData?.[key])
165
+ return !isEqual(current, initial)
166
+ })
167
+
168
+ return !hasChanges
169
+ })
170
+ function toggleModal(val: boolean) {
171
+ if (val) {
172
+ emit('update:modelValue', val)
173
+ } else {
174
+ close()
175
+ }
176
+ }
177
+ function checkChanges() {
178
+ if (props.mode === 'view') {
179
+ return false
180
+ }
181
+
182
+ if (!props.initialData || !Object.keys(props.initialData).length) {
183
+ return props.fields?.some(
184
+ field =>
185
+ formData.value?.[field.key] &&
186
+ !(Array.isArray(formData.value?.[field.key]) && !formData.value?.[field.key].length),
187
+ )
188
+ }
189
+
190
+ return props.fields?.some(field => !isEqual(formData.value?.[field.key], props.initialData?.[field.key]))
191
+ }
192
+ function close() {
193
+ emit('update:modelValue', false, checkChanges())
194
+ }
195
+ function submit() {
196
+ formRef.value?.validate().then(ok => {
197
+ if (!ok) return
198
+
199
+ const changed: Record<string, any> = {}
200
+
201
+ for (const field of props.fields) {
202
+ const key = field.key
203
+ const current = formData.value[key]
204
+ const initial = props.initialData?.[key]
205
+
206
+ const normalizedCurrent = field.type === 'select' ? normalizeValue(current) : current
207
+ const normalizedInitial = field.type === 'select' ? normalizeValue(initial) : initial
208
+
209
+ if (!isEqual(normalizedCurrent, normalizedInitial)) {
210
+ changed[key] = normalizedCurrent
211
+ }
212
+ }
213
+
214
+ emit('submit', changed)
215
+ })
216
+ }
217
+ function handleClear(key: string) {
218
+ const field = props.fields.find(f => f.key === key)
219
+ formData.value[key] = field?.type === 'select' ? [] : ''
220
+
221
+ nextTick(() => {
222
+ formRef.value?.validate()
223
+ })
224
+ }
225
+ function generateUuid() {
226
+ formData.value.uuid = uuidv4()
227
+ }
228
+ function copyToClipboard(text: string) {
229
+ navigator.clipboard.writeText(text).then(() => {
230
+ $q.notify(notificationSettings('success', 'UUID скопирован'))
231
+ })
232
+ }
233
+
234
+ watch(
235
+ () => props.modelValue,
236
+ val => {
237
+ if (val) {
238
+ formData.value = { ...(props.initialData ?? {}) }
239
+ }
240
+ },
241
+ { immediate: true },
242
+ )
243
+ </script>
244
+
245
+ <style lang="scss">
246
+ .custom-select-menu {
247
+ max-height: 250px !important;
248
+ overflow-y: auto !important;
249
+ }
250
+ </style>
251
+ <style scoped lang="scss">
252
+ .uuid-btn {
253
+ height: 32px;
254
+ padding: 0 10px;
255
+ border: 1px solid #3f8cff;
256
+ color: #3f8cff;
257
+ font-weight: 700;
258
+ font-size: 14px;
259
+ background: white;
260
+ border-radius: 6px;
261
+ }
262
+
263
+ .q-card {
264
+ border-radius: 12px;
265
+ background: #fff;
266
+ box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);
267
+ font-family: NunitoSansFont, sans-serif;
268
+
269
+ .modal-title {
270
+ color: #1d425d;
271
+ text-align: center;
272
+ font-size: 32px;
273
+ font-weight: 700;
274
+ padding: 18px 29px;
275
+ }
276
+
277
+ .q-card__section {
278
+ padding: 0;
279
+ }
280
+
281
+ .q-form {
282
+ padding: 0 29px;
283
+ }
284
+
285
+ .field-wrapper {
286
+ display: flex;
287
+ flex-direction: column;
288
+ margin-bottom: 15px;
289
+ }
290
+
291
+ .field-label {
292
+ font-size: 14px;
293
+ font-weight: 700;
294
+ color: #7d8592;
295
+ }
296
+ ::v-deep(.q-placeholder) {
297
+ color: #7d8592;
298
+ }
299
+ ::v-deep(.q-field__control) {
300
+ border-radius: 8px;
301
+ border: 1px solid #d8e0f0;
302
+ background: #fff;
303
+ box-shadow: 0px 1px 2px 0px rgba(184, 200, 224, 0.22);
304
+ }
305
+
306
+ ::v-deep(.q-field--filled .q-field__control:before) {
307
+ background: #fff !important;
308
+ border: none;
309
+ }
310
+
311
+ ::v-deep(.q-field--with-bottom) {
312
+ padding-bottom: 0;
313
+ }
314
+
315
+ ::v-deep(.q-field__bottom) {
316
+ padding: 0;
317
+ }
318
+
319
+ .clear-input {
320
+ color: #d8e0f0;
321
+ }
322
+
323
+ .required {
324
+ color: #f65160;
325
+ font-weight: bold;
326
+ }
327
+
328
+ &__actions {
329
+ padding: 18px 0;
330
+ border-radius: 0px 0px 16px 16px;
331
+ background: #f4f9fd;
332
+
333
+ button {
334
+ padding: 13px 37px;
335
+ border-radius: 4px;
336
+ font-size: 16px;
337
+ font-weight: 700;
338
+ text-transform: none;
339
+
340
+ &.remove {
341
+ border: 1px solid #f65160;
342
+ color: #f65160;
343
+ }
344
+ &.confirm {
345
+ background: #3f8cff;
346
+ color: #fff;
347
+ }
348
+
349
+ &.cancel {
350
+ color: #3f8cff;
351
+ border: 1px solid #3f8cff;
352
+ }
353
+ }
354
+ }
355
+ }
356
+ </style>