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.
@@ -56,6 +56,74 @@ function _derivePacketsKey(from, to, fallback = '*') {
56
56
  return fallback || '*';
57
57
  }
58
58
 
59
+ function _daysInMonth(y, m) {
60
+ return new Date(y, m, 0).getDate();
61
+ }
62
+
63
+ function _weekBounds(dt) {
64
+ const dow = dt.getDay();
65
+ const diffToMon = dow === 0 ? -6 : 1 - dow;
66
+ const mon = new Date(dt.getFullYear(), dt.getMonth(), dt.getDate() + diffToMon);
67
+ const sun = new Date(mon.getFullYear(), mon.getMonth(), mon.getDate() + 6);
68
+ return { from: _day(mon), to: _day(sun) };
69
+ }
70
+
71
+ function _boundsFromPacketsKey(key, defaultMode = 'month') {
72
+ const k = String(key || '').trim();
73
+ if (!k || k === '*')
74
+ return { from: '', to: '' };
75
+ if (k.startsWith('period:')) {
76
+ const [, s = '', e = ''] = k.split(':');
77
+ return { from: s, to: e };
78
+ }
79
+ if (k === 'now' || k.startsWith('now:')) {
80
+ const gran = k.startsWith('now:') ? k.slice(4) : defaultMode;
81
+ const now = new Date();
82
+ const y = now.getFullYear(), m = now.getMonth() + 1;
83
+ if (gran === 'day') {
84
+ const d = _day(now);
85
+ return { from: d, to: d };
86
+ }
87
+ if (gran === 'week')
88
+ return _weekBounds(now);
89
+ if (gran === 'quarter') {
90
+ const q = Math.floor((m - 1) / 3) + 1;
91
+ const m1 = String((q - 1) * 3 + 1).padStart(2, '0');
92
+ const m2 = (q - 1) * 3 + 3;
93
+ return { from: `${y}-${m1}-01`, to: `${y}-${String(m2).padStart(2, '0')}-${String(_daysInMonth(y, m2)).padStart(2, '0')}` };
94
+ }
95
+ if (gran === 'year')
96
+ return { from: `${y}-01-01`, to: `${y}-12-31` };
97
+ const ms = String(m).padStart(2, '0');
98
+ return { from: `${y}-${ms}-01`, to: `${y}-${ms}-${String(_daysInMonth(y, m)).padStart(2, '0')}` };
99
+ }
100
+ const clean = k.replace(/\*+$/, '');
101
+ if (/^\d{4}$/.test(clean))
102
+ return { from: `${clean}-01-01`, to: `${clean}-12-31` };
103
+ const qm = /^(\d{4})-q([1-4])$/i.exec(clean);
104
+ if (qm) {
105
+ const y = +qm[1], q = +qm[2];
106
+ const m1 = String((q - 1) * 3 + 1).padStart(2, '0');
107
+ const m2 = (q - 1) * 3 + 3;
108
+ return { from: `${y}-${m1}-01`, to: `${y}-${String(m2).padStart(2, '0')}-${String(_daysInMonth(y, m2)).padStart(2, '0')}` };
109
+ }
110
+ const mm = /^(\d{4})-(0[1-9]|1[0-2])$/.exec(clean);
111
+ if (mm) {
112
+ const y = +mm[1], m = +mm[2];
113
+ return { from: `${clean}-01`, to: `${clean}-${String(_daysInMonth(y, m)).padStart(2, '0')}` };
114
+ }
115
+ if (clean.startsWith('week:')) {
116
+ const monIso = clean.slice(5);
117
+ if (/^\d{4}-\d{2}-\d{2}$/.test(monIso)) {
118
+ const [y, m, d] = monIso.split('-').map(Number);
119
+ return _weekBounds(new Date(y, m - 1, d));
120
+ }
121
+ }
122
+ if (/^\d{4}-\d{2}-\d{2}$/.test(clean))
123
+ return { from: clean, to: clean };
124
+ return { from: '', to: '' };
125
+ }
126
+
59
127
  const money = v => (Number(v) ? Number(v).toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '');
60
128
  const qtyFmt = v => (Number(v) ? Number(v).toLocaleString('ru-RU', { maximumFractionDigits: 4 }) : '');
61
129
  /** Сальдо со знаком (Дт − Кт) текстом: «1 200,00 Дт», «300,00 Кт». */
@@ -304,32 +372,27 @@ export default {
304
372
  }
305
373
  .analysis-side {
306
374
  @apply --vertical;
307
- width: 240px;
375
+ width: 245px;
308
376
  flex-shrink: 0;
309
- gap: 6px;
310
- padding: 8px;
377
+ gap: 0;
378
+ padding: 0;
311
379
  background: light-dark(#f3f2ea, #22232a);
312
380
  border-right: 1px solid light-dark(#9a9990, #4a4c56);
313
- overflow-y: auto;
381
+ overflow: hidden;
314
382
  font-size: 12px;
315
383
  }
316
- .slot-card {
317
- @apply --vertical;
318
- gap: 4px;
319
- padding: 6px;
320
- background: light-dark(#fafaf7, #292a32);
321
- border: 1px solid light-dark(#c8c7be, #3e4049);
322
- border-radius: 3px;
323
- }
324
- .slot-head {
384
+ .analysis-side-top {
325
385
  @apply --horizontal;
326
386
  align-items: center;
327
387
  gap: 4px;
328
- font-weight: 600;
388
+ padding: 4px 6px;
389
+ border-bottom: 1px solid light-dark(#b8b7ae, #3e4049);
390
+ background: light-dark(#e8e7dc, #26272f);
391
+ flex-shrink: 0;
329
392
  }
330
393
  .slot-move-btn {
331
- padding: 1px 5px;
332
- font-size: 11px;
394
+ padding: 2px 7px;
395
+ font-size: 12px;
333
396
  line-height: 1.2;
334
397
  border: 1px solid light-dark(#b8b7ae, #4a4c56);
335
398
  border-radius: 2px;
@@ -337,40 +400,90 @@ export default {
337
400
  color: inherit;
338
401
  cursor: pointer;
339
402
  }
340
- .slot-sub {
341
- width: 100%;
342
- box-sizing: border-box;
343
- padding: 3px 6px;
344
- border: 1px solid light-dark(#b8b7ae, #3e4049);
345
- border-radius: 3px;
346
- background: light-dark(#fff, #1b1c20);
347
- color: inherit;
348
- font-size: 11.5px;
403
+ .slot-move-btn:hover {
404
+ background: light-dark(#eef2fa, #3d404c);
349
405
  }
350
- .analysis-main {
406
+ .features-grid {
351
407
  @apply --vertical;
352
408
  flex: 1;
353
- min-width: 0;
354
409
  min-height: 0;
410
+ overflow-y: auto;
411
+ background: light-dark(#ffffff, #1b1c20);
412
+ border-bottom: 1px solid light-dark(#b8b7ae, #3e4049);
413
+ }
414
+ .feat-head, .feat-row {
415
+ display: grid !important;
416
+ grid-template-columns: minmax(105px, 1.05fr) minmax(95px, 1fr);
417
+ align-items: stretch;
418
+ }
419
+ .feat-head {
420
+ background: light-dark(#e3e1d3, #2a2b32);
421
+ color: light-dark(#1f1f1f, #e2e8f0);
422
+ border-bottom: 1px solid light-dark(#9a9990, #4a4c56);
423
+ font-weight: 600;
424
+ font-size: 11.5px;
425
+ flex-shrink: 0;
426
+ }
427
+ .feat-row {
428
+ min-height: 24px;
429
+ border-bottom: 1px solid light-dark(#ecece8, #2c2d34);
430
+ cursor: pointer;
431
+ flex-shrink: 0;
432
+ }
433
+ .feat-row.selected {
434
+ background: light-dark(#dbe4f8, rgba(104, 130, 214, 0.28));
435
+ }
436
+ .feat-cell {
437
+ min-width: 0;
438
+ padding: 2px 5px;
439
+ display: flex;
440
+ align-items: center;
441
+ gap: 4px;
355
442
  overflow: hidden;
443
+ text-overflow: ellipsis;
444
+ white-space: nowrap;
445
+ border-right: 1px solid light-dark(#e2e2dc, #2f3038);
446
+ box-sizing: border-box;
356
447
  }
357
- .analysis-toolbar {
358
- @apply --horizontal;
448
+ .feat-cell:last-child {
449
+ border-right: none;
450
+ }
451
+ .feat-select {
452
+ width: 100%;
453
+ min-width: 0;
454
+ padding: 1px 3px;
455
+ font-size: 11.5px;
456
+ border: 1px solid transparent;
457
+ border-radius: 2px;
458
+ background: transparent;
459
+ color: light-dark(#1d4ed8, #93c5fd);
460
+ cursor: pointer;
461
+ outline: none;
462
+ }
463
+ .feat-select:focus, .feat-select:hover {
464
+ border-color: light-dark(#b8b7ae, #4a4c56);
465
+ background: light-dark(#fff, #24252c);
466
+ }
467
+ .analysis-side-bottom {
468
+ @apply --vertical;
469
+ gap: 3px;
470
+ padding: 6px 8px;
359
471
  flex-shrink: 0;
360
- gap: 8px;
472
+ background: light-dark(#f3f2ea, #22232a);
473
+ }
474
+ .analysis-side-bottom label {
475
+ display: flex;
361
476
  align-items: center;
362
- padding: 4px 8px;
363
- background: light-dark(#efeee4, #24252b);
364
- border-bottom: 1px solid light-dark(#b5b4ac, #3e4049);
477
+ gap: 5px;
478
+ cursor: pointer;
479
+ user-select: none;
365
480
  }
366
- .analysis-search {
367
- width: 240px;
368
- padding: 3px 8px;
369
- font-size: 12.5px;
370
- border: 1px solid light-dark(#b8b7ae, #3e4049);
371
- border-radius: 3px;
372
- background: light-dark(#fff, #1b1c20);
373
- color: inherit;
481
+ .analysis-main {
482
+ @apply --vertical;
483
+ flex: 1;
484
+ min-width: 0;
485
+ min-height: 0;
486
+ overflow: hidden;
374
487
  }
375
488
  .analysis-table {
376
489
  @apply --vertical;
@@ -378,11 +491,37 @@ export default {
378
491
  min-height: 0;
379
492
  overflow: hidden;
380
493
  }
381
- .analysis-table div.head-top, .analysis-table div.head, .analysis-table div.row {
494
+ .analysis-table div.head-top, .analysis-table div.head, .analysis-table div.filter-row, .analysis-table div.row {
382
495
  display: grid !important;
383
496
  grid-template-columns: minmax(210px, 2.5fr) repeat(6, minmax(90px, 1fr));
384
497
  align-items: stretch;
385
498
  }
499
+ .filter-row {
500
+ background: light-dark(#ffffff, #1f2026);
501
+ border-top: 1px solid light-dark(#b8b7ae, #3e4049);
502
+ min-height: 23px;
503
+ }
504
+ .filter-row .cell {
505
+ padding: 1px 5px;
506
+ gap: 4px;
507
+ }
508
+ .filter-mark {
509
+ font-size: 10.5px;
510
+ font-weight: 700;
511
+ color: light-dark(#555, #94a3b8);
512
+ flex-shrink: 0;
513
+ user-select: none;
514
+ }
515
+ .inline-filter-input {
516
+ flex: 1;
517
+ min-width: 0;
518
+ border: none;
519
+ outline: none;
520
+ background: transparent;
521
+ color: inherit;
522
+ font-size: 12px;
523
+ padding: 1px 2px;
524
+ }
386
525
  .analysis-table .grid-body div.row .cell.acc {
387
526
  background: light-dark(#f6f6f4, #222328);
388
527
  }
@@ -394,31 +533,12 @@ export default {
394
533
  }
395
534
 
396
535
  /* Экран 3: Обороты счёта (mode === 'turnovers') */
397
- .group-panel {
398
- @apply --vertical;
399
- width: 124px;
400
- flex-shrink: 0;
401
- gap: 4px;
402
- padding: 8px;
403
- background: light-dark(#f3f2ea, #22232a);
404
- border-right: 1px solid light-dark(#9a9990, #4a4c56);
405
- overflow-y: auto;
406
- font-size: 12.5px;
407
- }
408
- .group-panel label {
409
- display: flex;
410
- align-items: center;
411
- gap: 5px;
412
- padding: 1px 0;
413
- cursor: pointer;
414
- user-select: none;
415
- }
416
536
  .group-title {
417
537
  font-weight: 700;
418
538
  font-size: 11.5px;
419
539
  letter-spacing: 0.02em;
420
- color: light-dark(#333, #cbd5e1);
421
- margin-bottom: 4px;
540
+ color: light-dark(#555, #cbd5e1);
541
+ margin-bottom: 2px;
422
542
  }
423
543
  .periods-panel {
424
544
  @apply --vertical;
@@ -445,9 +565,31 @@ export default {
445
565
  }
446
566
  .docs-panel div.head, .docs-panel div.row {
447
567
  display: grid !important;
448
- grid-template-columns: 82px minmax(115px, 1.45fr) minmax(115px, 1.35fr) minmax(86px, 1fr) minmax(86px, 1fr);
568
+ grid-template-columns: 82px minmax(115px, 1.45fr) minmax(115px, 1.35fr) minmax(86px, 1fr) minmax(86px, 1fr) 30px;
449
569
  align-items: stretch;
450
570
  }
571
+ .cell.entry-col {
572
+ justify-content: center;
573
+ padding: 0 2px;
574
+ }
575
+ .entry-icon-btn {
576
+ display: inline-flex;
577
+ align-items: center;
578
+ justify-content: center;
579
+ width: 22px;
580
+ height: 20px;
581
+ padding: 0;
582
+ border: 1px solid transparent;
583
+ border-radius: 3px;
584
+ background: transparent;
585
+ color: light-dark(#444, #cbd5e1);
586
+ cursor: pointer;
587
+ }
588
+ .entry-icon-btn:hover {
589
+ border-color: light-dark(#b8b7ae, #4a4c56);
590
+ background: light-dark(#eef2fa, #2d3039);
591
+ color: light-dark(#1d4ed8, #93c5fd);
592
+ }
451
593
 
452
594
  /* Экран 4: Сборная карточка проводки */
453
595
  .entry-card {
@@ -503,8 +645,12 @@ export default {
503
645
 
504
646
  <div class="error" ~if="error">{{error}}</div>
505
647
 
506
- <!-- Компактная полоса отбора по аналитикам и подразделению в Оборотах и Проводках -->
507
- <div class="filter-strip" ~if="(slots?.length || units.length > 1) && mode !== 'analysis'">
648
+ <!-- Компактная полоса группировки, отбора по аналитикам и подразделению в Оборотах и Проводках -->
649
+ <div class="filter-strip" ~if="mode !== 'analysis' && (mode === 'turnovers' || slots?.length || units.length > 1)">
650
+ <span class="filter-strip-label" ~if="mode === 'turnovers'">Группировать:</span>
651
+ <select ~if="mode === 'turnovers'" title="Группировать" @change="setGroupBy($this.value)">
652
+ <option ~for="turnoverGroupOptions" :value="$for.item.id" :selected="$for.item.id === groupBy">{{$for.item.label}}</option>
653
+ </select>
508
654
  <span class="filter-strip-label" ~if="slots?.length">Аналитики:</span>
509
655
  <div class="filter-slot-item" ~for="slots">
510
656
  <oda-link-input dense :meta="$for.item" ::value="filters[$for.item.id]" @value-changed="onFilterChanged()"></oda-link-input>
@@ -563,32 +709,42 @@ export default {
563
709
  </div>
564
710
  </div>
565
711
 
566
- <!-- Экран 2 ODANT: Анализ счёта в разрезе аналитик -->
712
+ <!-- Экран 2 ODANT: Анализ счёта в разрезе аналитик (по полям прилинкованных справочников) -->
567
713
  <div class="analysis-layout" ~if="mode === 'analysis'">
568
714
  <div class="analysis-side">
569
- <div class="group-title">АНАЛИТИКИ СЧЁТА</div>
570
- <div class="slot-card" ~for="slots">
571
- <div class="slot-head">
572
- <input type="checkbox" :checked="isSlotActive($for.item.id)" @change="toggleSlot($for.item.id, $this.checked)">
573
- <span style="flex:1; overflow:hidden; text-overflow:ellipsis;">{{$for.item.label}}</span>
574
- <button type="button" class="slot-move-btn" title="Выше" @tap="moveSlot($for.index, -1)">↑</button>
575
- <button type="button" class="slot-move-btn" title="Ниже" @tap="moveSlot($for.index, 1)">↓</button>
715
+ <div class="analysis-side-top">
716
+ <button type="button" class="slot-move-btn" title="Поднять признак выше" @tap="moveFeature(-1)">↑</button>
717
+ <button type="button" class="slot-move-btn" title="Опустить признак ниже" @tap="moveFeature(1)">↓</button>
718
+ <div style="flex:1;"></div>
719
+ <button type="button" class="btn-action" @tap="reload()">Сформировать</button>
720
+ </div>
721
+ <div class="features-grid">
722
+ <div class="feat-head">
723
+ <div class="feat-cell">Аналитика</div>
724
+ <div class="feat-cell">Значение</div>
725
+ </div>
726
+ <div class="feat-row" ~for="analyticFeatures"
727
+ ~class="{selected: selectedFeatureId === $for.item.id}"
728
+ @tap="selectedFeatureId = $for.item.id">
729
+ <div class="feat-cell" :title="featureLabel($for.item)">
730
+ <input type="checkbox" :checked="isSlotActive($for.item.spec)" @change.stop="toggleSlot($for.item.spec, $this.checked)">
731
+ <span style="overflow:hidden; text-overflow:ellipsis;">{{featureLabel($for.item)}}</span>
732
+ </div>
733
+ <div class="feat-cell">
734
+ <select class="feat-select" :title="featureValueLabel($for.item)" @change="setFeatureValue($for.item, $this.value)">
735
+ <option value="" :selected="!featureValueOf($for.item)">Все</option>
736
+ <option ~for="$for.item.options" :value="$for.$for.item.value" :selected="$for.$for.item.value === featureValueOf($for.item)">{{$for.$for.item.label}}</option>
737
+ </select>
738
+ </div>
576
739
  </div>
577
- <oda-link-input dense :meta="$for.item" ::value="filters[$for.item.id]" @value-changed="onFilterChanged()"></oda-link-input>
578
- <input class="slot-sub" :value="subFilters[$for.item.id] || ''"
579
- placeholder="Субаналитика (поле:знач)"
580
- @change="setSubFilter($for.item.id, $this.value)">
581
740
  </div>
582
- <div class="group-title" style="margin-top:6px;">ЗНАЧЕНИЯ</div>
583
- <label><input type="radio" name="valMode" value="amount" :checked="valueMode === 'amount'" @change="setValueMode('amount')"> Сумма</label>
584
- <label><input type="radio" name="valMode" value="qty" :checked="valueMode === 'qty'" @change="setValueMode('qty')"> Количество</label>
741
+ <div class="analysis-side-bottom">
742
+ <div class="group-title">ЗНАЧЕНИЯ</div>
743
+ <label><input type="radio" name="valMode" value="amount" :checked="valueMode === 'amount'" @change="setValueMode('amount')"> Сумма</label>
744
+ <label><input type="radio" name="valMode" value="qty" :checked="valueMode === 'qty'" @change="setValueMode('qty')"> Количество</label>
745
+ </div>
585
746
  </div>
586
747
  <div class="analysis-main">
587
- <div class="analysis-toolbar">
588
- <input class="analysis-search" :value="analysisQuery" placeholder="Быстрый фильтр по аналитике…" @input="analysisQuery = $this.value">
589
- <div style="flex:1;"></div>
590
- <button type="button" class="btn-action" @tap="openTurnoversFromAnalysis(selectedAnalysisRow)">Обороты по аналитике</button>
591
- </div>
592
748
  <div class="analysis-table">
593
749
  <div class="head-wrap">
594
750
  <div class="head-top">
@@ -606,6 +762,18 @@ export default {
606
762
  <div class="cell num">Дебет</div>
607
763
  <div class="cell num">Кредит</div>
608
764
  </div>
765
+ <div class="filter-row">
766
+ <div class="cell acc">
767
+ <span class="filter-mark">a%c</span>
768
+ <input class="inline-filter-input" :value="analysisQuery" placeholder="Фильтр…" @input="analysisQuery = $this.value">
769
+ </div>
770
+ <div class="cell"><span class="filter-mark">=</span></div>
771
+ <div class="cell group-sep"><span class="filter-mark">=</span></div>
772
+ <div class="cell"><span class="filter-mark">=</span></div>
773
+ <div class="cell group-sep"><span class="filter-mark">=</span></div>
774
+ <div class="cell"><span class="filter-mark">=</span></div>
775
+ <div class="cell"><span class="filter-mark">=</span></div>
776
+ </div>
609
777
  </div>
610
778
  <div class="grid-body">
611
779
  <div class="row" ~for="analysisRows"
@@ -636,13 +804,6 @@ export default {
636
804
 
637
805
  <!-- Экран 3 ODANT: Двухпанельные Обороты счёта -->
638
806
  <div class="turnovers-layout" ~if="mode === 'turnovers'">
639
- <div class="group-panel">
640
- <div class="group-title">ГРУППИРОВАТЬ</div>
641
- <label ~for="turnoverGroupOptions">
642
- <input type="radio" name="turnoverGroup" :value="$for.item.id" :checked="groupBy === $for.item.id" @change="setGroupBy($for.item.id)">
643
- <span>{{$for.item.label}}</span>
644
- </label>
645
- </div>
646
807
  <div class="periods-panel">
647
808
  <div class="head-wrap">
648
809
  <div class="head">
@@ -687,6 +848,7 @@ export default {
687
848
  <div class="cell">Корр. счёт</div>
688
849
  <div class="cell num">Дебет</div>
689
850
  <div class="cell num">Кредит</div>
851
+ <div class="cell entry-col" title="Проводка"></div>
690
852
  </div>
691
853
  </div>
692
854
  <div class="grid-body">
@@ -696,6 +858,11 @@ export default {
696
858
  <div class="cell link" title="Открыть корреспондирующий счёт" @tap="openCorr($for.item)">{{$for.item.corrDir}}</div>
697
859
  <div class="cell num debit-col">{{$for.item.debit}}</div>
698
860
  <div class="cell num credit-col">{{$for.item.credit}}</div>
861
+ <div class="cell entry-col">
862
+ <button type="button" class="entry-icon-btn" title="Открыть проводку" @tap.stop="openEntry($for.item.entry)">
863
+ <oda-icon icon="icons:list" icon-size="16"></oda-icon>
864
+ </button>
865
+ </div>
699
866
  </div>
700
867
  </div>
701
868
  <div class="row total">
@@ -704,6 +871,7 @@ export default {
704
871
  <div class="cell"></div>
705
872
  <div class="cell num">{{turnoverDocsTotals.debit}}</div>
706
873
  <div class="cell num">{{turnoverDocsTotals.credit}}</div>
874
+ <div class="cell entry-col"></div>
707
875
  </div>
708
876
  </div>
709
877
  </div>
@@ -753,8 +921,13 @@ export default {
753
921
  turnoverGroupOptions: TURNOVER_GROUPS,
754
922
  selectedGroupKey: '',
755
923
  groups: [],
756
- /** Выбранные слоты для разворота в Анализе счёта */
924
+ /** Выбранные слоты/признаки для разворота в Анализе счёта */
757
925
  activeSlots: [],
926
+ /** Аналитические признаки по полям прилинкованных справочников */
927
+ analyticFeatures: [],
928
+ selectedFeatureId: '',
929
+ /** Значения фильтра по полям прилинкованных справочников: { 'slotId.fieldId': val } */
930
+ featureValues: {},
758
931
  /** Текстовые фильтры субаналитик: { слот: 'поле:значение' } */
759
932
  subFilters: {},
760
933
  analysisQuery: '',
@@ -930,6 +1103,61 @@ export default {
930
1103
  get analysisTotals() {
931
1104
  return this.analysisCells(this.sheetTotal);
932
1105
  },
1106
+ featureLabel(feat) {
1107
+ if (!feat)
1108
+ return '';
1109
+ if ((this.slots || []).length > 1 && feat.slotLabel)
1110
+ return `${feat.slotLabel}: ${feat.label || feat.id}`;
1111
+ return feat.label || feat.id;
1112
+ },
1113
+ featureValueOf(feat) {
1114
+ if (!feat)
1115
+ return '';
1116
+ if (feat.isObject)
1117
+ return String(this.filters?.[feat.slotId] || '');
1118
+ return String(this.featureValues?.[feat.id] || '');
1119
+ },
1120
+ featureValueLabel(feat) {
1121
+ const val = this.featureValueOf(feat);
1122
+ if (!val)
1123
+ return 'Все';
1124
+ const opt = (feat?.options || []).find(o => o.value === val);
1125
+ return opt?.label || val;
1126
+ },
1127
+ async setFeatureValue(feat, value) {
1128
+ if (!feat)
1129
+ return;
1130
+ const val = String(value || '');
1131
+ if (feat.isObject) {
1132
+ this.filters = { ...(this.filters || {}), [feat.slotId]: val };
1133
+ await this.onFilterChanged();
1134
+ return;
1135
+ }
1136
+ this.featureValues = { ...(this.featureValues || {}), [feat.id]: val };
1137
+ if (val && !(this.activeSlots || []).includes(feat.spec) && !(this.activeSlots || []).includes(feat.slotId)) {
1138
+ this.activeSlots = [...(this.activeSlots || []), feat.spec];
1139
+ }
1140
+ await this.reload();
1141
+ },
1142
+ async moveFeature(delta) {
1143
+ const list = [...(this.analyticFeatures || [])];
1144
+ const idx = list.findIndex(f => f.id === this.selectedFeatureId);
1145
+ if (idx < 0)
1146
+ return;
1147
+ const target = idx + delta;
1148
+ if (target < 0 || target >= list.length)
1149
+ return;
1150
+ const [moved] = list.splice(idx, 1);
1151
+ list.splice(target, 0, moved);
1152
+ this.analyticFeatures = list;
1153
+ const order = list.map(f => f.spec);
1154
+ this.activeSlots = (this.activeSlots || []).slice().sort((a, b) => {
1155
+ const ia = order.indexOf(a), ib = order.indexOf(b);
1156
+ return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
1157
+ });
1158
+ if (this.mode === 'analysis')
1159
+ await this.reload();
1160
+ },
933
1161
  isSlotActive(slotId) {
934
1162
  return (this.activeSlots || []).includes(slotId);
935
1163
  },
@@ -939,7 +1167,13 @@ export default {
939
1167
  cur.add(slotId);
940
1168
  else
941
1169
  cur.delete(slotId);
942
- this.activeSlots = [...cur];
1170
+ const order = (this.analyticFeatures || []).map(f => f.spec);
1171
+ this.activeSlots = [...cur].sort((a, b) => {
1172
+ const ia = order.indexOf(a), ib = order.indexOf(b);
1173
+ if (ia < 0 && ib < 0)
1174
+ return 0;
1175
+ return (ia < 0 ? 999 : ia) - (ib < 0 ? 999 : ib);
1176
+ });
943
1177
  if (this.mode === 'analysis')
944
1178
  await this.reload();
945
1179
  },
@@ -953,6 +1187,9 @@ export default {
953
1187
  this.slots = list;
954
1188
  const order = list.map(s => s.id);
955
1189
  this.activeSlots = (this.activeSlots || []).slice().sort((a, b) => order.indexOf(a) - order.indexOf(b));
1190
+ if (this.analyticFeatures?.length) {
1191
+ this.analyticFeatures = [...this.analyticFeatures].sort((a, b) => order.indexOf(a.slotId) - order.indexOf(b.slotId));
1192
+ }
956
1193
  if (this.mode === 'analysis')
957
1194
  await this.reload();
958
1195
  },
@@ -1090,14 +1327,23 @@ export default {
1090
1327
  if (!hasNavPeriod && (this.from || this.to))
1091
1328
  return;
1092
1329
  const saved = _readSavedPeriod();
1093
- const src = hasNavPeriod ? nav : saved;
1330
+ const pk = typeof this.packetsKey === 'string' ? this.packetsKey : '';
1331
+ const src = hasNavPeriod ? nav : (saved || (pk && pk !== '*' ? { packetsKey: pk } : null));
1094
1332
  if (!src)
1095
1333
  return;
1096
1334
  this._initializingPackets = true;
1097
1335
  try {
1098
- const from = String(src.from || '');
1099
- const to = String(src.to || '');
1100
- const key = _derivePacketsKey(from, to, typeof src.packetsKey === 'string' ? src.packetsKey : '*');
1336
+ const rawKey = typeof src.packetsKey === 'string' && src.packetsKey ? src.packetsKey : (pk || '*');
1337
+ let from = String(src.from || '');
1338
+ let to = String(src.to || '');
1339
+ if ((!from && !to && rawKey !== '*') || rawKey === 'now' || rawKey.startsWith('now:')) {
1340
+ const derived = _boundsFromPacketsKey(rawKey, 'month');
1341
+ if (derived.from && derived.to) {
1342
+ from = derived.from;
1343
+ to = derived.to;
1344
+ }
1345
+ }
1346
+ const key = _derivePacketsKey(from, to, rawKey);
1101
1347
  this.from = from;
1102
1348
  this.to = to;
1103
1349
  this._packetFiltered = !!(from || to);
@@ -1170,18 +1416,45 @@ export default {
1170
1416
  this.units = [];
1171
1417
  }
1172
1418
  },
1173
- /** Слоты аналитик счёта из схемы ядра (с учётом наследования). */
1419
+ /** Слоты аналитик счёта из схемы ядра (с учётом наследования) и поля прилинкованных справочников. */
1174
1420
  async loadSlots() {
1175
1421
  try {
1176
- const s = (await this.$item?.fetch('db_schema'))?.schema;
1422
+ const res = await this.$item?.fetch('db_schema');
1423
+ const s = res?.schema;
1177
1424
  const fields = Array.isArray(s?.fields) ? s.fields : [];
1178
1425
  this.slots = fields.filter(f => f?.analytic && f.type === 'link')
1179
1426
  .map(f => ({ id: f.id, label: f.label || f.id, type: 'Link', catalog: f.targetPath || f.target, placeholder: f.label || f.id }));
1180
1427
  this.activeSlots = this.slots.map(f => f.id);
1428
+ const serverSlots = Array.isArray(res?.slots) ? res.slots : [];
1429
+ const features = [];
1430
+ for (const slot of this.slots) {
1431
+ const srv = serverSlots.find(x => x.id === slot.id);
1432
+ if (Array.isArray(srv?.fields) && srv.fields.length) {
1433
+ features.push(...srv.fields);
1434
+ }
1435
+ else {
1436
+ features.push({
1437
+ id: slot.id,
1438
+ spec: slot.id,
1439
+ slotId: slot.id,
1440
+ fieldId: 'name',
1441
+ label: 'Наименование',
1442
+ slotLabel: slot.label,
1443
+ isObject: true,
1444
+ type: 'link',
1445
+ catalog: slot.catalog,
1446
+ options: [],
1447
+ });
1448
+ }
1449
+ }
1450
+ this.analyticFeatures = features;
1451
+ this.selectedFeatureId = features[0]?.id || '';
1181
1452
  }
1182
1453
  catch (e) {
1183
1454
  this.slots = [];
1184
1455
  this.activeSlots = [];
1456
+ this.analyticFeatures = [];
1457
+ this.selectedFeatureId = '';
1185
1458
  this.error = String(e?.message || e);
1186
1459
  }
1187
1460
  },
@@ -1200,7 +1473,17 @@ export default {
1200
1473
  for (const [slotId, raw] of Object.entries(this.subFilters || {})) {
1201
1474
  const parsed = parseSubFilterInput(raw);
1202
1475
  if (parsed)
1203
- out[slotId] = parsed;
1476
+ out[slotId] = { ...(out[slotId] || {}), ...parsed };
1477
+ }
1478
+ for (const feat of this.analyticFeatures || []) {
1479
+ if (feat.isObject)
1480
+ continue;
1481
+ const val = this.featureValues?.[feat.id];
1482
+ if (val !== undefined && val !== null && val !== '') {
1483
+ if (!out[feat.slotId])
1484
+ out[feat.slotId] = {};
1485
+ out[feat.slotId][feat.fieldId] = feat.type === 'boolean' ? (val === '1' || val === 'true' ? 1 : 0) : val;
1486
+ }
1204
1487
  }
1205
1488
  return out;
1206
1489
  },
@@ -1232,7 +1515,9 @@ export default {
1232
1515
  this.packetsStart = r.start;
1233
1516
  this.packetsEnd = r.end;
1234
1517
  this.packetDays = Array.isArray(r.days) && r.days.length ? r.days : null;
1235
- if (this._packetFiltered && ((this.to && this.to < r.start) || (this.from && this.from > r.end))) {
1518
+ const keyStr = typeof this.packetsKey === 'string' ? this.packetsKey : '';
1519
+ const isNowKey = keyStr === 'now' || keyStr.startsWith('now:');
1520
+ if (this._packetFiltered && !isNowKey && this.to && this.to < r.start) {
1236
1521
  this._initializingPackets = true;
1237
1522
  try {
1238
1523
  this._packetFiltered = false;