odant-work 1.0.0 → 1.0.2

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.
@@ -51,6 +51,74 @@ function _derivePacketsKey(from, to, fallback = '*') {
51
51
  return fallback || '*';
52
52
  }
53
53
 
54
+ function _daysInMonth(y, m) {
55
+ return new Date(y, m, 0).getDate();
56
+ }
57
+
58
+ function _weekBounds(dt) {
59
+ const dow = dt.getDay();
60
+ const diffToMon = dow === 0 ? -6 : 1 - dow;
61
+ const mon = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate() + diffToMon);
62
+ const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6);
63
+ return { from: _day(mon), to: _day(sun) };
64
+ }
65
+
66
+ function _boundsFromPacketsKey(key, defaultMode = 'month') {
67
+ const k = String(key || '').trim();
68
+ if (!k || k === '*')
69
+ return { from: '', to: '' };
70
+ if (k.startsWith('period:')) {
71
+ const [, s = '', e = ''] = k.split(':');
72
+ return { from: s, to: e };
73
+ }
74
+ if (k === 'now' || k.startsWith('now:')) {
75
+ const gran = k.startsWith('now:') ? k.slice(4) : defaultMode;
76
+ const now = new Date();
77
+ const y = now.getFullYear(), m = now.getMonth() + 1;
78
+ if (gran === 'day') {
79
+ const d = _day(now);
80
+ return { from: d, to: d };
81
+ }
82
+ if (gran === 'week')
83
+ return _weekBounds(now);
84
+ if (gran === 'quarter') {
85
+ const q = Math.floor((m - 1) / 3) + 1;
86
+ const m1 = String((q - 1) * 3 + 1).padStart(2, '0');
87
+ const m2 = (q - 1) * 3 + 3;
88
+ return { from: `${y}-${m1}-01`, to: `${y}-${String(m2).padStart(2, '0')}-${String(_daysInMonth(y, m2)).padStart(2, '0')}` };
89
+ }
90
+ if (gran === 'year')
91
+ return { from: `${y}-01-01`, to: `${y}-12-31` };
92
+ const ms = String(m).padStart(2, '0');
93
+ return { from: `${y}-${ms}-01`, to: `${y}-${ms}-${String(_daysInMonth(y, m)).padStart(2, '0')}` };
94
+ }
95
+ const clean = k.replace(/\*+$/, '');
96
+ if (/^\d{4}$/.test(clean))
97
+ return { from: `${clean}-01-01`, to: `${clean}-12-31` };
98
+ const qm = /^(\d{4})-q([1-4])$/i.exec(clean);
99
+ if (qm) {
100
+ const y = +qm[1], q = +qm[2];
101
+ const m1 = String((q - 1) * 3 + 1).padStart(2, '0');
102
+ const m2 = (q - 1) * 3 + 3;
103
+ return { from: `${y}-${m1}-01`, to: `${y}-${String(m2).padStart(2, '0')}-${String(_daysInMonth(y, m2)).padStart(2, '0')}` };
104
+ }
105
+ const mm = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(clean);
106
+ if (mm) {
107
+ const y = +mm[1], m = +mm[2];
108
+ return { from: `${clean}-01`, to: `${clean}-${String(_daysInMonth(y, m)).padStart(2, '0')}` };
109
+ }
110
+ if (clean.startsWith('week:')) {
111
+ const monIso = clean.slice(5);
112
+ if (/^\d{4}-\d{2}-\d{2}$/.test(monIso)) {
113
+ const [y, m, d] = monIso.split('-').map(Number);
114
+ return _weekBounds(new Date(y, m - 1, d));
115
+ }
116
+ }
117
+ if (/^\d{4}-\d{2}-\d{2}$/.test(clean))
118
+ return { from: clean, to: clean };
119
+ return { from: '', to: '' };
120
+ }
121
+
54
122
  const money = v => (Number(v) ? Number(v).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '');
55
123
 
56
124
  export default {
@@ -449,14 +517,23 @@ export default {
449
517
  if (!hasNavPeriod && (this.from || this.to))
450
518
  return;
451
519
  const saved = _readSavedPeriod();
452
- const src = hasNavPeriod ? nav : saved;
520
+ const pk = typeof this.packetsKey === 'string' ? this.packetsKey : '';
521
+ const src = hasNavPeriod ? nav : (saved || (pk && pk !== '*' ? { packetsKey: pk } : null));
453
522
  if (!src)
454
523
  return;
455
524
  this._initializingPackets = true;
456
525
  try {
457
- const from = String(src.from || '');
458
- const to = String(src.to || '');
459
- const key = _derivePacketsKey(from, to, typeof src.packetsKey === 'string' ? src.packetsKey : '*');
526
+ const rawKey = typeof src.packetsKey === 'string' && src.packetsKey ? src.packetsKey : (pk || '*');
527
+ let from = String(src.from || '');
528
+ let to = String(src.to || '');
529
+ if ((!from && !to && rawKey !== '*') || rawKey === 'now' || rawKey.startsWith('now:')) {
530
+ const derived = _boundsFromPacketsKey(rawKey, 'month');
531
+ if (derived.from && derived.to) {
532
+ from = derived.from;
533
+ to = derived.to;
534
+ }
535
+ }
536
+ const key = _derivePacketsKey(from, to, rawKey);
460
537
  this.from = from;
461
538
  this.to = to;
462
539
  this._packetFiltered = !!(from || to);
@@ -526,7 +603,9 @@ export default {
526
603
  this.packetsStart = r.start;
527
604
  this.packetsEnd = r.end;
528
605
  this.packetDays = Array.isArray(r.days) && r.days.length ? r.days : null;
529
- if (this._packetFiltered && ((this.to && this.to < r.start) || (this.from && this.from > r.end))) {
606
+ const keyStr = typeof this.packetsKey === 'string' ? this.packetsKey : '';
607
+ const isNowKey = keyStr === 'now' || keyStr.startsWith('now:');
608
+ if (this._packetFiltered && !isNowKey && this.to && this.to < r.start) {
530
609
  this._initializingPackets = true;
531
610
  try {
532
611
  this._packetFiltered = false;
@@ -53,6 +53,74 @@ function _derivePacketsKey(from, to, fallback = '*') {
53
53
  return fallback || '*';
54
54
  }
55
55
 
56
+ function _daysInMonth(y, m) {
57
+ return new Date(y, m, 0).getDate();
58
+ }
59
+
60
+ function _weekBounds(dt) {
61
+ const dow = dt.getDay();
62
+ const diffToMon = dow === 0 ? -6 : 1 - dow;
63
+ const mon = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate() + diffToMon);
64
+ const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6);
65
+ return { from: _day(mon), to: _day(sun) };
66
+ }
67
+
68
+ function _boundsFromPacketsKey(key, defaultMode = 'month') {
69
+ const k = String(key || '').trim();
70
+ if (!k || k === '*')
71
+ return { from: '', to: '' };
72
+ if (k.startsWith('period:')) {
73
+ const [, s = '', e = ''] = k.split(':');
74
+ return { from: s, to: e };
75
+ }
76
+ if (k === 'now' || k.startsWith('now:')) {
77
+ const gran = k.startsWith('now:') ? k.slice(4) : defaultMode;
78
+ const now = new Date();
79
+ const y = now.getFullYear(), m = now.getMonth() + 1;
80
+ if (gran === 'day') {
81
+ const d = _day(now);
82
+ return { from: d, to: d };
83
+ }
84
+ if (gran === 'week')
85
+ return _weekBounds(now);
86
+ if (gran === 'quarter') {
87
+ const q = Math.floor((m - 1) / 3) + 1;
88
+ const m1 = String((q - 1) * 3 + 1).padStart(2, '0');
89
+ const m2 = (q - 1) * 3 + 3;
90
+ return { from: `${y}-${m1}-01`, to: `${y}-${String(m2).padStart(2, '0')}-${String(_daysInMonth(y, m2)).padStart(2, '0')}` };
91
+ }
92
+ if (gran === 'year')
93
+ return { from: `${y}-01-01`, to: `${y}-12-31` };
94
+ const ms = String(m).padStart(2, '0');
95
+ return { from: `${y}-${ms}-01`, to: `${y}-${ms}-${String(_daysInMonth(y, m)).padStart(2, '0')}` };
96
+ }
97
+ const clean = k.replace(/\*+$/, '');
98
+ if (/^\d{4}$/.test(clean))
99
+ return { from: `${clean}-01-01`, to: `${clean}-12-31` };
100
+ const qm = /^(\d{4})-q([1-4])$/i.exec(clean);
101
+ if (qm) {
102
+ const y = +qm[1], q = +qm[2];
103
+ const m1 = String((q - 1) * 3 + 1).padStart(2, '0');
104
+ const m2 = (q - 1) * 3 + 3;
105
+ return { from: `${y}-${m1}-01`, to: `${y}-${String(m2).padStart(2, '0')}-${String(_daysInMonth(y, m2)).padStart(2, '0')}` };
106
+ }
107
+ const mm = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(clean);
108
+ if (mm) {
109
+ const y = +mm[1], m = +mm[2];
110
+ return { from: `${clean}-01`, to: `${clean}-${String(_daysInMonth(y, m)).padStart(2, '0')}` };
111
+ }
112
+ if (clean.startsWith('week:')) {
113
+ const monIso = clean.slice(5);
114
+ if (/^\d{4}-\d{2}-\d{2}$/.test(monIso)) {
115
+ const [y, m, d] = monIso.split('-').map(Number);
116
+ return _weekBounds(new Date(y, m - 1, d));
117
+ }
118
+ }
119
+ if (/^\d{4}-\d{2}-\d{2}$/.test(clean))
120
+ return { from: clean, to: clean };
121
+ return { from: '', to: '' };
122
+ }
123
+
56
124
  export default {
57
125
  imports: 'oda//button, oda//packets',
58
126
  template: /*html*/`
@@ -489,14 +557,23 @@ export default {
489
557
  if (!hasNavPeriod && (this.from || this.to))
490
558
  return;
491
559
  const saved = _readSavedPeriod();
492
- const src = hasNavPeriod ? nav : saved;
560
+ const pk = typeof this.packetsKey === 'string' ? this.packetsKey : '';
561
+ const src = hasNavPeriod ? nav : (saved || (pk && pk !== '*' ? { packetsKey: pk } : null));
493
562
  if (!src)
494
563
  return;
495
564
  this._initializingPackets = true;
496
565
  try {
497
- const from = String(src.from || '');
498
- const to = String(src.to || '');
499
- const key = _derivePacketsKey(from, to, typeof src.packetsKey === 'string' ? src.packetsKey : '*');
566
+ const rawKey = typeof src.packetsKey === 'string' && src.packetsKey ? src.packetsKey : (pk || '*');
567
+ let from = String(src.from || '');
568
+ let to = String(src.to || '');
569
+ if ((!from && !to && rawKey !== '*') || rawKey === 'now' || rawKey.startsWith('now:')) {
570
+ const derived = _boundsFromPacketsKey(rawKey, 'month');
571
+ if (derived.from && derived.to) {
572
+ from = derived.from;
573
+ to = derived.to;
574
+ }
575
+ }
576
+ const key = _derivePacketsKey(from, to, rawKey);
500
577
  this.from = from;
501
578
  this.to = to;
502
579
  this._packetFiltered = !!(from || to);
@@ -573,7 +650,9 @@ export default {
573
650
  this.packetsStart = r.start;
574
651
  this.packetsEnd = r.end;
575
652
  this.packetDays = Array.isArray(r.days) && r.days.length ? r.days : null;
576
- if (this._packetFiltered && ((this.to && this.to < r.start) || (this.from && this.from > r.end))) {
653
+ const keyStr = typeof this.packetsKey === 'string' ? this.packetsKey : '';
654
+ const isNowKey = keyStr === 'now' || keyStr.startsWith('now:');
655
+ if (this._packetFiltered && !isNowKey && this.to && this.to < r.start) {
577
656
  this._initializingPackets = true;
578
657
  try {
579
658
  this._packetFiltered = false;
@@ -0,0 +1,5 @@
1
+ {
2
+ "publicKey": "MCowBQYDK2VwAyEAWUaWWCwtQMu6P1vyw67NV4k1n+u2rWlKsYzqwMZWLBs=",
3
+ "privateKey": "MC4CAQAwBQYDK2VwBCIEIJ11hrseEgp+y+025hchZFmZDdMppBxHNQIU18YFCd6q",
4
+ "created": 1793258170482
5
+ }
@@ -0,0 +1,182 @@
1
+ {
2
+ "licenses": {
3
+ "WORK-PAK-300A0DD0": {
4
+ "licenseId": "WORK-PAK-300A0DD0",
5
+ "issuedAt": 1793262666169,
6
+ "updatedAt": 1793271130179,
7
+ "issuer": "work.odant.org",
8
+ "customer": {
9
+ "name": "ООО «Маслоторг»",
10
+ "inn": "",
11
+ "email": "admin@maslotorg.ru",
12
+ "phone": ""
13
+ },
14
+ "hardware": {
15
+ "hwid": "PAK-3C564FAE8C0E",
16
+ "nodeFingerprint": "",
17
+ "sku": "WORK-PAK-495-192-4T",
18
+ "cpu": "AMD Ryzen™ AI Max+ PRO 495 (Zen 5, 16 ядер / 32 потока, до 5.2 ГГц, кэш 80 МБ)",
19
+ "ramGb": 192,
20
+ "ssdTb": 4,
21
+ "warrantyYears": 1,
22
+ "warrantyUntil": 1824798666169,
23
+ "warrantyProvider": "Партнёр-интегратор WORK"
24
+ },
25
+ "partner": null,
26
+ "baseSeats": 10,
27
+ "perpetualBase": true,
28
+ "addonSubscriptions": [],
29
+ "support": {
30
+ "tier": "business",
31
+ "includedUntil": 1824798666169,
32
+ "ticketsUsed": 1,
33
+ "bindCode": "BIND-4D86AF"
34
+ },
35
+ "protocol": "WORK-LIC-v1",
36
+ "issuerPublicKey": "MCowBQYDK2VwAyEAWUaWWCwtQMu6P1vyw67NV4k1n+u2rWlKsYzqwMZWLBs=",
37
+ "signature": "JJ4b3YNiKbXO4enxRDYCVrE0+ZOSCpnaG268vW/RE/ucAFx+tJi9Xy4dLjfHogBxVw9Y5sAdivwCovq3yKTMBg=="
38
+ }
39
+ },
40
+ "orders": {
41
+ "ORD-MVTAMUDM-0F96": {
42
+ "orderId": "ORD-MVTAMUDM-0F96",
43
+ "licenseId": "WORK-PAK-300A0DD0",
44
+ "partnerId": "",
45
+ "partnerDiscountPercent": 0,
46
+ "partnerMarginRub": 0,
47
+ "includeBasePak": false,
48
+ "tier": "business",
49
+ "tierName": "Business",
50
+ "addonSeats": 10,
51
+ "years": 1,
52
+ "amountRub": 135000,
53
+ "items": [
54
+ {
55
+ "title": "Годовая лицензия ODANT-WORK на доп. рабочее место (уровень Business, 1 г., скидка −10% за объём)",
56
+ "quantity": 10,
57
+ "unitPriceRub": 13500,
58
+ "totalRub": 135000
59
+ }
60
+ ],
61
+ "customer": {
62
+ "name": "ООО «Маслоторг»",
63
+ "inn": "7701234567",
64
+ "email": "admin@maslotorg.ru",
65
+ "phone": ""
66
+ },
67
+ "status": "pending",
68
+ "paymentId": "yk_sandbox_2874a3734d40ab1b",
69
+ "confirmationUrl": "http://localhost:8020/pay/sandbox/ORD-MVTAMUDM-0F96",
70
+ "returnUrl": "https://work.odant.org",
71
+ "createdAt": 1793263243594,
72
+ "paidAt": null,
73
+ "yookassaMode": "sandbox"
74
+ },
75
+ "ORD-FR-MVTAMUET-FFAD": {
76
+ "orderId": "ORD-FR-MVTAMUET-FFAD",
77
+ "kind": "franchise_reservation",
78
+ "reservationId": "RES-TA-8EFAA5",
79
+ "licenseId": "RES-TA-8EFAA5",
80
+ "regionCode": "RU-TA",
81
+ "regionName": "Республика Татарстан",
82
+ "tierName": "Резерв франшизы (Республика Татарстан)",
83
+ "addonSeats": 1,
84
+ "years": 1,
85
+ "amountRub": 100000,
86
+ "franchiseFeeRub": 6500000,
87
+ "items": [
88
+ {
89
+ "title": "Невозвратный залог за резервирование региональной франшизы ODANT-WORK (Республика Татарстан, стоимость франшизы 6 500 000 ₽)",
90
+ "quantity": 1,
91
+ "unitPriceRub": 100000,
92
+ "totalRub": 100000
93
+ }
94
+ ],
95
+ "customer": {
96
+ "name": "ООО «Волга-Интеграция»",
97
+ "inn": "1655123456",
98
+ "email": "partner@volga-it.ru",
99
+ "phone": ""
100
+ },
101
+ "status": "pending",
102
+ "paymentId": "yk_sandbox_fae9588353cc1267",
103
+ "confirmationUrl": "http://localhost:8020/pay/sandbox/ORD-FR-MVTAMUET-FFAD",
104
+ "returnUrl": "https://work.odant.org",
105
+ "createdAt": 1793263243636,
106
+ "paidAt": null,
107
+ "yookassaMode": "sandbox"
108
+ }
109
+ },
110
+ "tickets": {
111
+ "TCK-0000-28BE": {
112
+ "ticketId": "TCK-0000-28BE",
113
+ "licenseId": "WORK-PAK-300A0DD0",
114
+ "customerName": "ООО «Маслоторг»",
115
+ "channel": "max",
116
+ "chatId": "client-101",
117
+ "userName": "Пользователь MAX",
118
+ "tier": "business",
119
+ "tierName": "Business",
120
+ "reactionMinutes": 120,
121
+ "reactionLabel": "до 2 часов",
122
+ "slaDeadlineAt": 1793270443784,
123
+ "status": "open",
124
+ "createdAt": 1793263243784,
125
+ "firstReplyAt": null,
126
+ "slaMet": null,
127
+ "messages": [
128
+ {
129
+ "from": "client",
130
+ "author": "Пользователь MAX",
131
+ "text": "Подскажите, как синхронизировать сертификат после докупки 10 мест?",
132
+ "time": 1793263243784
133
+ }
134
+ ]
135
+ }
136
+ },
137
+ "botBindings": {
138
+ "max:client-101": {
139
+ "channel": "max",
140
+ "chatId": "client-101",
141
+ "userName": "Пользователь MAX",
142
+ "licenseId": "WORK-PAK-300A0DD0",
143
+ "boundAt": 1793263243683
144
+ }
145
+ },
146
+ "partners": {},
147
+ "reservations": {
148
+ "RES-TA-8EFAA5": {
149
+ "reservationId": "RES-TA-8EFAA5",
150
+ "orderId": "ORD-FR-MVTAMUET-FFAD",
151
+ "regionCode": "RU-TA",
152
+ "regionName": "Республика Татарстан",
153
+ "district": "ПФО",
154
+ "companyName": "ООО «Волга-Интеграция»",
155
+ "inn": "1655123456",
156
+ "email": "partner@volga-it.ru",
157
+ "phone": "",
158
+ "contactPerson": "",
159
+ "franchiseFeeRub": 6500000,
160
+ "nonRefundableDepositRub": 100000,
161
+ "remainingFranchiseFeeRub": 6400000,
162
+ "status": "reserved_pending_deposit",
163
+ "depositPaid": false,
164
+ "createdAt": 1793263243636,
165
+ "expiresAt": 1794472843636
166
+ }
167
+ },
168
+ "certificates": {},
169
+ "users": {
170
+ "admin@maslotorg.ru": {
171
+ "userId": "USR-4DB3960551",
172
+ "email": "admin@maslotorg.ru",
173
+ "name": "ООО «Маслоторг»",
174
+ "companyName": "ООО «Маслоторг»",
175
+ "inn": "",
176
+ "licenseId": "WORK-PAK-300A0DD0",
177
+ "sessionKey": "wks_d0839169d8c9db42e8815c031140eb7c",
178
+ "createdAt": 1793262666168,
179
+ "lastLoginAt": 1793262666168
180
+ }
181
+ }
182
+ }
@@ -406,6 +406,12 @@ ODA({
406
406
  this._nowItem.idleLabel = NOW_IDLE_LABELS[mode] || 'Месяц';
407
407
  this._nowItem.label = this._nowItem.idleLabel;
408
408
  }
409
+ },
410
+ _onValueChanged(value) {
411
+ if (!this._allItem) return;
412
+ const currentKey = this._selectionKey(this.selection);
413
+ if (value === currentKey) return;
414
+ this._applySelection(this._itemsByKey(value) ?? [this._allItem]);
409
415
  }
410
416
  },
411
417
  _hasCustomDays: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odant-work",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "WORK — платформа цифровой организации (учётная система уровня 1С, документооборот, чаты и локальный мультимодальный ИИ)",
5
5
  "type": "module",
6
6
  "main": "sources/work.js",
@@ -14,7 +14,7 @@
14
14
  "test": "node --test \"tests/**/*.test.js\"",
15
15
  "test:ci": "npm test",
16
16
  "setup-mirror": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/setup-svn-mirror.ps1",
17
- "publish": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/svn-publish.ps1",
17
+ "svn:publish": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/svn-publish.ps1",
18
18
  "sync": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/svn-sync.ps1",
19
19
  "mirror": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/mirror-to-git.ps1",
20
20
  "test:ui": "node tests/ui/run.mjs",
@@ -473,6 +473,179 @@ function withTargetPaths(db, schema) {
473
473
  return { ...schema, fields: walk(schema.fields), overlay: walk(schema.overlay) };
474
474
  }
475
475
 
476
+ /**
477
+ * Собрать аналитические признаки по полям прилинкованного справочника слота счёта
478
+ * (Наименование + поля схемы $catalog, вложенных структур $struct и расширений OVERLAY у $virtual)
479
+ * со списком реально существующих значений для панели «Анализ счёта».
480
+ */
481
+ function buildCatalogAnalyticFields(db, slotField) {
482
+ const slotId = slotField.id;
483
+ const slotLabel = slotField.label || slotId;
484
+ const targetPath = slotField.targetPath || slotField.target;
485
+ const targetSchema = slotField.target ? withTargetPaths(db, classSchema(db, slotField.target)) : null;
486
+ if (!targetSchema) {
487
+ return [{
488
+ id: slotId,
489
+ spec: slotId,
490
+ slotId,
491
+ fieldId: 'name',
492
+ label: 'Наименование',
493
+ slotLabel,
494
+ isObject: true,
495
+ type: 'link',
496
+ catalog: targetPath,
497
+ options: [],
498
+ }];
499
+ }
500
+ const isVirt = targetSchema.type === '$virtual';
501
+ const regCid = isVirt ? (targetSchema.source?.registry || targetSchema.cid) : targetSchema.cid;
502
+ const descCids = descendants(db, regCid);
503
+
504
+ let objRows = [];
505
+ if (descCids.length) {
506
+ objRows = db.prepare(
507
+ `SELECT id, cid, body FROM objects WHERE cid IN (${descCids.map(() => '?').join(',')}) AND status <> 'deleted' AND ext_of IS NULL ORDER BY updated DESC LIMIT 300`
508
+ ).all(...descCids);
509
+ }
510
+ const extByObj = new Map();
511
+ if (isVirt) {
512
+ const virtCids = descendants(db, targetSchema.cid);
513
+ if (virtCids.length) {
514
+ for (const r of db.prepare(
515
+ `SELECT ext_of, body FROM objects WHERE cid IN (${virtCids.map(() => '?').join(',')}) AND status <> 'deleted' AND ext_of IS NOT NULL LIMIT 300`
516
+ ).all(...virtCids)) {
517
+ try {
518
+ extByObj.set(r.ext_of, JSON.parse(r.body || '{}'));
519
+ }
520
+ catch { /* ignore */ }
521
+ }
522
+ }
523
+ }
524
+
525
+ const parsedObjs = objRows.map(r => {
526
+ let body = {};
527
+ try { body = JSON.parse(r.body || '{}'); } catch { /* ignore */ }
528
+ const title = String(body.name || body.label || body.title || r.id).trim();
529
+ return { id: r.id, title, body, ext: extByObj.get(r.id) || null };
530
+ });
531
+
532
+ const objectOptions = parsedObjs
533
+ .map(o => ({ value: o.id, label: o.title || o.id }))
534
+ .sort((a, b) => a.label.localeCompare(b.label, 'ru', { numeric: true }));
535
+
536
+ const fieldDefs = [];
537
+ const seenFieldIds = new Set(['name', 'time']);
538
+ const collectFields = (list, isOverlay = false, prefix = '', prefixLabel = '') => {
539
+ for (const f of list || []) {
540
+ if (!f?.id || f.type === 'table')
541
+ continue;
542
+ const fullId = prefix ? `${prefix}.${f.id}` : f.id;
543
+ const fullLabel = prefixLabel ? `${prefixLabel}: ${f.label || f.id}` : (f.label || f.id);
544
+ if (f.type === 'struct' && Array.isArray(f.fields) && f.fields.length) {
545
+ collectFields(f.fields, isOverlay, fullId, fullLabel);
546
+ continue;
547
+ }
548
+ if (seenFieldIds.has(fullId))
549
+ continue;
550
+ seenFieldIds.add(fullId);
551
+ fieldDefs.push({
552
+ fieldId: fullId,
553
+ label: fullLabel,
554
+ type: f.type || 'string',
555
+ target: f.target,
556
+ values: f.values || f.options,
557
+ isOverlay,
558
+ });
559
+ }
560
+ };
561
+
562
+ collectFields(targetSchema.fields, false);
563
+ if (isVirt)
564
+ collectFields(targetSchema.overlay, true);
565
+ for (const cid of descCids) {
566
+ if (cid === targetSchema.cid)
567
+ continue;
568
+ const cs = classSchema(db, cid);
569
+ if (cs)
570
+ collectFields(cs.fields, false);
571
+ }
572
+
573
+ const refIdsToResolve = new Set();
574
+ const rawValuesByField = new Map();
575
+ for (const fd of fieldDefs) {
576
+ const vals = new Set();
577
+ for (const o of parsedObjs) {
578
+ let v;
579
+ if (fd.isOverlay && o.ext) {
580
+ v = o.ext[fd.fieldId];
581
+ }
582
+ else {
583
+ let cur = o.body;
584
+ for (const part of fd.fieldId.split('.')) {
585
+ if (!cur || typeof cur !== 'object') { cur = undefined; break; }
586
+ cur = cur[part];
587
+ }
588
+ v = cur;
589
+ }
590
+ if (v === undefined || v === null || v === '' || typeof v === 'object')
591
+ continue;
592
+ const strVal = String(v).trim();
593
+ if (!strVal)
594
+ continue;
595
+ vals.add(strVal);
596
+ if (fd.type === 'link')
597
+ refIdsToResolve.add(strVal);
598
+ }
599
+ rawValuesByField.set(fd.fieldId, [...vals]);
600
+ }
601
+
602
+ const refNames = refIdsToResolve.size ? namesOf(db, [...refIdsToResolve]).objects : {};
603
+
604
+ const out = [{
605
+ id: slotId,
606
+ spec: slotId,
607
+ slotId,
608
+ fieldId: 'name',
609
+ label: 'Наименование',
610
+ slotLabel,
611
+ isObject: true,
612
+ type: 'link',
613
+ catalog: targetPath,
614
+ options: objectOptions,
615
+ }];
616
+
617
+ for (const fd of fieldDefs) {
618
+ const rawVals = rawValuesByField.get(fd.fieldId) || [];
619
+ let options = [];
620
+ if (fd.type === 'boolean') {
621
+ options = [{ value: '1', label: 'Да' }, { value: '0', label: 'Нет' }];
622
+ }
623
+ else if (fd.type === 'enum' && Array.isArray(fd.values) && fd.values.length) {
624
+ options = fd.values.map(v => (v && typeof v === 'object'
625
+ ? { value: String(v.id ?? v.value), label: String(v.label ?? v.id ?? v.value) }
626
+ : { value: String(v), label: String(v) }));
627
+ }
628
+ else {
629
+ options = rawVals.map(v => ({
630
+ value: v,
631
+ label: fd.type === 'link' ? (refNames[v] || v) : v,
632
+ })).sort((a, b) => a.label.localeCompare(b.label, 'ru', { numeric: true }));
633
+ }
634
+ out.push({
635
+ id: `${slotId}.${fd.fieldId}`,
636
+ spec: `${slotId}.${fd.fieldId}`,
637
+ slotId,
638
+ fieldId: fd.fieldId,
639
+ label: fd.label,
640
+ slotLabel,
641
+ isObject: false,
642
+ type: fd.type,
643
+ options,
644
+ });
645
+ }
646
+ return out;
647
+ }
648
+
476
649
  /**
477
650
  * Поддерево класса для форм: `classes` (cid → описание потомков), `leaves` (листья для создания
478
651
  * объектов в группе) и `slots` (слоты аналитик счёта или журнала по всему поддереву счетов).
@@ -508,7 +681,13 @@ function schemaSubtree(db, cls) {
508
681
  for (const f of analyticSlots(s || { fields: [] })) {
509
682
  if (!seenSlots.has(f.id)) {
510
683
  seenSlots.add(f.id);
511
- slots.push({ id: f.id, label: f.label || f.id, target: f.target, targetPath: f.targetPath || f.target });
684
+ slots.push({
685
+ id: f.id,
686
+ label: f.label || f.id,
687
+ target: f.target,
688
+ targetPath: f.targetPath || f.target,
689
+ fields: buildCatalogAnalyticFields(db, f),
690
+ });
512
691
  }
513
692
  }
514
693
  }