mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 18:13:46 +03:00
11259 lines
640 KiB
JavaScript
11259 lines
640 KiB
JavaScript
(() => {
|
|
var __create = Object.create;
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __getProtoOf = Object.getPrototypeOf;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
// file that has been converted to a CommonJS file using a Babel-
|
|
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
mod
|
|
));
|
|
|
|
// node_modules/qrcode/lib/can-promise.js
|
|
var require_can_promise = __commonJS({
|
|
"node_modules/qrcode/lib/can-promise.js"(exports, module) {
|
|
module.exports = function() {
|
|
return typeof Promise === "function" && Promise.prototype && Promise.prototype.then;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/utils.js
|
|
var require_utils = __commonJS({
|
|
"node_modules/qrcode/lib/core/utils.js"(exports) {
|
|
var toSJISFunction;
|
|
var CODEWORDS_COUNT = [
|
|
0,
|
|
// Not used
|
|
26,
|
|
44,
|
|
70,
|
|
100,
|
|
134,
|
|
172,
|
|
196,
|
|
242,
|
|
292,
|
|
346,
|
|
404,
|
|
466,
|
|
532,
|
|
581,
|
|
655,
|
|
733,
|
|
815,
|
|
901,
|
|
991,
|
|
1085,
|
|
1156,
|
|
1258,
|
|
1364,
|
|
1474,
|
|
1588,
|
|
1706,
|
|
1828,
|
|
1921,
|
|
2051,
|
|
2185,
|
|
2323,
|
|
2465,
|
|
2611,
|
|
2761,
|
|
2876,
|
|
3034,
|
|
3196,
|
|
3362,
|
|
3532,
|
|
3706
|
|
];
|
|
exports.getSymbolSize = function getSymbolSize(version) {
|
|
if (!version) throw new Error('"version" cannot be null or undefined');
|
|
if (version < 1 || version > 40) throw new Error('"version" should be in range from 1 to 40');
|
|
return version * 4 + 17;
|
|
};
|
|
exports.getSymbolTotalCodewords = function getSymbolTotalCodewords(version) {
|
|
return CODEWORDS_COUNT[version];
|
|
};
|
|
exports.getBCHDigit = function(data) {
|
|
let digit = 0;
|
|
while (data !== 0) {
|
|
digit++;
|
|
data >>>= 1;
|
|
}
|
|
return digit;
|
|
};
|
|
exports.setToSJISFunction = function setToSJISFunction(f) {
|
|
if (typeof f !== "function") {
|
|
throw new Error('"toSJISFunc" is not a valid function.');
|
|
}
|
|
toSJISFunction = f;
|
|
};
|
|
exports.isKanjiModeEnabled = function() {
|
|
return typeof toSJISFunction !== "undefined";
|
|
};
|
|
exports.toSJIS = function toSJIS(kanji) {
|
|
return toSJISFunction(kanji);
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/error-correction-level.js
|
|
var require_error_correction_level = __commonJS({
|
|
"node_modules/qrcode/lib/core/error-correction-level.js"(exports) {
|
|
exports.L = { bit: 1 };
|
|
exports.M = { bit: 0 };
|
|
exports.Q = { bit: 3 };
|
|
exports.H = { bit: 2 };
|
|
function fromString(string) {
|
|
if (typeof string !== "string") {
|
|
throw new Error("Param is not a string");
|
|
}
|
|
const lcStr = string.toLowerCase();
|
|
switch (lcStr) {
|
|
case "l":
|
|
case "low":
|
|
return exports.L;
|
|
case "m":
|
|
case "medium":
|
|
return exports.M;
|
|
case "q":
|
|
case "quartile":
|
|
return exports.Q;
|
|
case "h":
|
|
case "high":
|
|
return exports.H;
|
|
default:
|
|
throw new Error("Unknown EC Level: " + string);
|
|
}
|
|
}
|
|
exports.isValid = function isValid(level) {
|
|
return level && typeof level.bit !== "undefined" && level.bit >= 0 && level.bit < 4;
|
|
};
|
|
exports.from = function from(value, defaultValue) {
|
|
if (exports.isValid(value)) {
|
|
return value;
|
|
}
|
|
try {
|
|
return fromString(value);
|
|
} catch (e) {
|
|
return defaultValue;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/bit-buffer.js
|
|
var require_bit_buffer = __commonJS({
|
|
"node_modules/qrcode/lib/core/bit-buffer.js"(exports, module) {
|
|
function BitBuffer() {
|
|
this.buffer = [];
|
|
this.length = 0;
|
|
}
|
|
BitBuffer.prototype = {
|
|
get: function(index) {
|
|
const bufIndex = Math.floor(index / 8);
|
|
return (this.buffer[bufIndex] >>> 7 - index % 8 & 1) === 1;
|
|
},
|
|
put: function(num, length) {
|
|
for (let i = 0; i < length; i++) {
|
|
this.putBit((num >>> length - i - 1 & 1) === 1);
|
|
}
|
|
},
|
|
getLengthInBits: function() {
|
|
return this.length;
|
|
},
|
|
putBit: function(bit) {
|
|
const bufIndex = Math.floor(this.length / 8);
|
|
if (this.buffer.length <= bufIndex) {
|
|
this.buffer.push(0);
|
|
}
|
|
if (bit) {
|
|
this.buffer[bufIndex] |= 128 >>> this.length % 8;
|
|
}
|
|
this.length++;
|
|
}
|
|
};
|
|
module.exports = BitBuffer;
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/bit-matrix.js
|
|
var require_bit_matrix = __commonJS({
|
|
"node_modules/qrcode/lib/core/bit-matrix.js"(exports, module) {
|
|
function BitMatrix(size) {
|
|
if (!size || size < 1) {
|
|
throw new Error("BitMatrix size must be defined and greater than 0");
|
|
}
|
|
this.size = size;
|
|
this.data = new Uint8Array(size * size);
|
|
this.reservedBit = new Uint8Array(size * size);
|
|
}
|
|
BitMatrix.prototype.set = function(row, col, value, reserved) {
|
|
const index = row * this.size + col;
|
|
this.data[index] = value;
|
|
if (reserved) this.reservedBit[index] = true;
|
|
};
|
|
BitMatrix.prototype.get = function(row, col) {
|
|
return this.data[row * this.size + col];
|
|
};
|
|
BitMatrix.prototype.xor = function(row, col, value) {
|
|
this.data[row * this.size + col] ^= value;
|
|
};
|
|
BitMatrix.prototype.isReserved = function(row, col) {
|
|
return this.reservedBit[row * this.size + col];
|
|
};
|
|
module.exports = BitMatrix;
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/alignment-pattern.js
|
|
var require_alignment_pattern = __commonJS({
|
|
"node_modules/qrcode/lib/core/alignment-pattern.js"(exports) {
|
|
var getSymbolSize = require_utils().getSymbolSize;
|
|
exports.getRowColCoords = function getRowColCoords(version) {
|
|
if (version === 1) return [];
|
|
const posCount = Math.floor(version / 7) + 2;
|
|
const size = getSymbolSize(version);
|
|
const intervals = size === 145 ? 26 : Math.ceil((size - 13) / (2 * posCount - 2)) * 2;
|
|
const positions = [size - 7];
|
|
for (let i = 1; i < posCount - 1; i++) {
|
|
positions[i] = positions[i - 1] - intervals;
|
|
}
|
|
positions.push(6);
|
|
return positions.reverse();
|
|
};
|
|
exports.getPositions = function getPositions(version) {
|
|
const coords = [];
|
|
const pos = exports.getRowColCoords(version);
|
|
const posLength = pos.length;
|
|
for (let i = 0; i < posLength; i++) {
|
|
for (let j = 0; j < posLength; j++) {
|
|
if (i === 0 && j === 0 || // top-left
|
|
i === 0 && j === posLength - 1 || // bottom-left
|
|
i === posLength - 1 && j === 0) {
|
|
continue;
|
|
}
|
|
coords.push([pos[i], pos[j]]);
|
|
}
|
|
}
|
|
return coords;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/finder-pattern.js
|
|
var require_finder_pattern = __commonJS({
|
|
"node_modules/qrcode/lib/core/finder-pattern.js"(exports) {
|
|
var getSymbolSize = require_utils().getSymbolSize;
|
|
var FINDER_PATTERN_SIZE = 7;
|
|
exports.getPositions = function getPositions(version) {
|
|
const size = getSymbolSize(version);
|
|
return [
|
|
// top-left
|
|
[0, 0],
|
|
// top-right
|
|
[size - FINDER_PATTERN_SIZE, 0],
|
|
// bottom-left
|
|
[0, size - FINDER_PATTERN_SIZE]
|
|
];
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/mask-pattern.js
|
|
var require_mask_pattern = __commonJS({
|
|
"node_modules/qrcode/lib/core/mask-pattern.js"(exports) {
|
|
exports.Patterns = {
|
|
PATTERN000: 0,
|
|
PATTERN001: 1,
|
|
PATTERN010: 2,
|
|
PATTERN011: 3,
|
|
PATTERN100: 4,
|
|
PATTERN101: 5,
|
|
PATTERN110: 6,
|
|
PATTERN111: 7
|
|
};
|
|
var PenaltyScores = {
|
|
N1: 3,
|
|
N2: 3,
|
|
N3: 40,
|
|
N4: 10
|
|
};
|
|
exports.isValid = function isValid(mask) {
|
|
return mask != null && mask !== "" && !isNaN(mask) && mask >= 0 && mask <= 7;
|
|
};
|
|
exports.from = function from(value) {
|
|
return exports.isValid(value) ? parseInt(value, 10) : void 0;
|
|
};
|
|
exports.getPenaltyN1 = function getPenaltyN1(data) {
|
|
const size = data.size;
|
|
let points = 0;
|
|
let sameCountCol = 0;
|
|
let sameCountRow = 0;
|
|
let lastCol = null;
|
|
let lastRow = null;
|
|
for (let row = 0; row < size; row++) {
|
|
sameCountCol = sameCountRow = 0;
|
|
lastCol = lastRow = null;
|
|
for (let col = 0; col < size; col++) {
|
|
let module2 = data.get(row, col);
|
|
if (module2 === lastCol) {
|
|
sameCountCol++;
|
|
} else {
|
|
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5);
|
|
lastCol = module2;
|
|
sameCountCol = 1;
|
|
}
|
|
module2 = data.get(col, row);
|
|
if (module2 === lastRow) {
|
|
sameCountRow++;
|
|
} else {
|
|
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5);
|
|
lastRow = module2;
|
|
sameCountRow = 1;
|
|
}
|
|
}
|
|
if (sameCountCol >= 5) points += PenaltyScores.N1 + (sameCountCol - 5);
|
|
if (sameCountRow >= 5) points += PenaltyScores.N1 + (sameCountRow - 5);
|
|
}
|
|
return points;
|
|
};
|
|
exports.getPenaltyN2 = function getPenaltyN2(data) {
|
|
const size = data.size;
|
|
let points = 0;
|
|
for (let row = 0; row < size - 1; row++) {
|
|
for (let col = 0; col < size - 1; col++) {
|
|
const last = data.get(row, col) + data.get(row, col + 1) + data.get(row + 1, col) + data.get(row + 1, col + 1);
|
|
if (last === 4 || last === 0) points++;
|
|
}
|
|
}
|
|
return points * PenaltyScores.N2;
|
|
};
|
|
exports.getPenaltyN3 = function getPenaltyN3(data) {
|
|
const size = data.size;
|
|
let points = 0;
|
|
let bitsCol = 0;
|
|
let bitsRow = 0;
|
|
for (let row = 0; row < size; row++) {
|
|
bitsCol = bitsRow = 0;
|
|
for (let col = 0; col < size; col++) {
|
|
bitsCol = bitsCol << 1 & 2047 | data.get(row, col);
|
|
if (col >= 10 && (bitsCol === 1488 || bitsCol === 93)) points++;
|
|
bitsRow = bitsRow << 1 & 2047 | data.get(col, row);
|
|
if (col >= 10 && (bitsRow === 1488 || bitsRow === 93)) points++;
|
|
}
|
|
}
|
|
return points * PenaltyScores.N3;
|
|
};
|
|
exports.getPenaltyN4 = function getPenaltyN4(data) {
|
|
let darkCount = 0;
|
|
const modulesCount = data.data.length;
|
|
for (let i = 0; i < modulesCount; i++) darkCount += data.data[i];
|
|
const k = Math.abs(Math.ceil(darkCount * 100 / modulesCount / 5) - 10);
|
|
return k * PenaltyScores.N4;
|
|
};
|
|
function getMaskAt(maskPattern, i, j) {
|
|
switch (maskPattern) {
|
|
case exports.Patterns.PATTERN000:
|
|
return (i + j) % 2 === 0;
|
|
case exports.Patterns.PATTERN001:
|
|
return i % 2 === 0;
|
|
case exports.Patterns.PATTERN010:
|
|
return j % 3 === 0;
|
|
case exports.Patterns.PATTERN011:
|
|
return (i + j) % 3 === 0;
|
|
case exports.Patterns.PATTERN100:
|
|
return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0;
|
|
case exports.Patterns.PATTERN101:
|
|
return i * j % 2 + i * j % 3 === 0;
|
|
case exports.Patterns.PATTERN110:
|
|
return (i * j % 2 + i * j % 3) % 2 === 0;
|
|
case exports.Patterns.PATTERN111:
|
|
return (i * j % 3 + (i + j) % 2) % 2 === 0;
|
|
default:
|
|
throw new Error("bad maskPattern:" + maskPattern);
|
|
}
|
|
}
|
|
exports.applyMask = function applyMask(pattern, data) {
|
|
const size = data.size;
|
|
for (let col = 0; col < size; col++) {
|
|
for (let row = 0; row < size; row++) {
|
|
if (data.isReserved(row, col)) continue;
|
|
data.xor(row, col, getMaskAt(pattern, row, col));
|
|
}
|
|
}
|
|
};
|
|
exports.getBestMask = function getBestMask(data, setupFormatFunc) {
|
|
const numPatterns = Object.keys(exports.Patterns).length;
|
|
let bestPattern = 0;
|
|
let lowerPenalty = Infinity;
|
|
for (let p = 0; p < numPatterns; p++) {
|
|
setupFormatFunc(p);
|
|
exports.applyMask(p, data);
|
|
const penalty = exports.getPenaltyN1(data) + exports.getPenaltyN2(data) + exports.getPenaltyN3(data) + exports.getPenaltyN4(data);
|
|
exports.applyMask(p, data);
|
|
if (penalty < lowerPenalty) {
|
|
lowerPenalty = penalty;
|
|
bestPattern = p;
|
|
}
|
|
}
|
|
return bestPattern;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/error-correction-code.js
|
|
var require_error_correction_code = __commonJS({
|
|
"node_modules/qrcode/lib/core/error-correction-code.js"(exports) {
|
|
var ECLevel = require_error_correction_level();
|
|
var EC_BLOCKS_TABLE = [
|
|
// L M Q H
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
2,
|
|
2,
|
|
1,
|
|
2,
|
|
2,
|
|
4,
|
|
1,
|
|
2,
|
|
4,
|
|
4,
|
|
2,
|
|
4,
|
|
4,
|
|
4,
|
|
2,
|
|
4,
|
|
6,
|
|
5,
|
|
2,
|
|
4,
|
|
6,
|
|
6,
|
|
2,
|
|
5,
|
|
8,
|
|
8,
|
|
4,
|
|
5,
|
|
8,
|
|
8,
|
|
4,
|
|
5,
|
|
8,
|
|
11,
|
|
4,
|
|
8,
|
|
10,
|
|
11,
|
|
4,
|
|
9,
|
|
12,
|
|
16,
|
|
4,
|
|
9,
|
|
16,
|
|
16,
|
|
6,
|
|
10,
|
|
12,
|
|
18,
|
|
6,
|
|
10,
|
|
17,
|
|
16,
|
|
6,
|
|
11,
|
|
16,
|
|
19,
|
|
6,
|
|
13,
|
|
18,
|
|
21,
|
|
7,
|
|
14,
|
|
21,
|
|
25,
|
|
8,
|
|
16,
|
|
20,
|
|
25,
|
|
8,
|
|
17,
|
|
23,
|
|
25,
|
|
9,
|
|
17,
|
|
23,
|
|
34,
|
|
9,
|
|
18,
|
|
25,
|
|
30,
|
|
10,
|
|
20,
|
|
27,
|
|
32,
|
|
12,
|
|
21,
|
|
29,
|
|
35,
|
|
12,
|
|
23,
|
|
34,
|
|
37,
|
|
12,
|
|
25,
|
|
34,
|
|
40,
|
|
13,
|
|
26,
|
|
35,
|
|
42,
|
|
14,
|
|
28,
|
|
38,
|
|
45,
|
|
15,
|
|
29,
|
|
40,
|
|
48,
|
|
16,
|
|
31,
|
|
43,
|
|
51,
|
|
17,
|
|
33,
|
|
45,
|
|
54,
|
|
18,
|
|
35,
|
|
48,
|
|
57,
|
|
19,
|
|
37,
|
|
51,
|
|
60,
|
|
19,
|
|
38,
|
|
53,
|
|
63,
|
|
20,
|
|
40,
|
|
56,
|
|
66,
|
|
21,
|
|
43,
|
|
59,
|
|
70,
|
|
22,
|
|
45,
|
|
62,
|
|
74,
|
|
24,
|
|
47,
|
|
65,
|
|
77,
|
|
25,
|
|
49,
|
|
68,
|
|
81
|
|
];
|
|
var EC_CODEWORDS_TABLE = [
|
|
// L M Q H
|
|
7,
|
|
10,
|
|
13,
|
|
17,
|
|
10,
|
|
16,
|
|
22,
|
|
28,
|
|
15,
|
|
26,
|
|
36,
|
|
44,
|
|
20,
|
|
36,
|
|
52,
|
|
64,
|
|
26,
|
|
48,
|
|
72,
|
|
88,
|
|
36,
|
|
64,
|
|
96,
|
|
112,
|
|
40,
|
|
72,
|
|
108,
|
|
130,
|
|
48,
|
|
88,
|
|
132,
|
|
156,
|
|
60,
|
|
110,
|
|
160,
|
|
192,
|
|
72,
|
|
130,
|
|
192,
|
|
224,
|
|
80,
|
|
150,
|
|
224,
|
|
264,
|
|
96,
|
|
176,
|
|
260,
|
|
308,
|
|
104,
|
|
198,
|
|
288,
|
|
352,
|
|
120,
|
|
216,
|
|
320,
|
|
384,
|
|
132,
|
|
240,
|
|
360,
|
|
432,
|
|
144,
|
|
280,
|
|
408,
|
|
480,
|
|
168,
|
|
308,
|
|
448,
|
|
532,
|
|
180,
|
|
338,
|
|
504,
|
|
588,
|
|
196,
|
|
364,
|
|
546,
|
|
650,
|
|
224,
|
|
416,
|
|
600,
|
|
700,
|
|
224,
|
|
442,
|
|
644,
|
|
750,
|
|
252,
|
|
476,
|
|
690,
|
|
816,
|
|
270,
|
|
504,
|
|
750,
|
|
900,
|
|
300,
|
|
560,
|
|
810,
|
|
960,
|
|
312,
|
|
588,
|
|
870,
|
|
1050,
|
|
336,
|
|
644,
|
|
952,
|
|
1110,
|
|
360,
|
|
700,
|
|
1020,
|
|
1200,
|
|
390,
|
|
728,
|
|
1050,
|
|
1260,
|
|
420,
|
|
784,
|
|
1140,
|
|
1350,
|
|
450,
|
|
812,
|
|
1200,
|
|
1440,
|
|
480,
|
|
868,
|
|
1290,
|
|
1530,
|
|
510,
|
|
924,
|
|
1350,
|
|
1620,
|
|
540,
|
|
980,
|
|
1440,
|
|
1710,
|
|
570,
|
|
1036,
|
|
1530,
|
|
1800,
|
|
570,
|
|
1064,
|
|
1590,
|
|
1890,
|
|
600,
|
|
1120,
|
|
1680,
|
|
1980,
|
|
630,
|
|
1204,
|
|
1770,
|
|
2100,
|
|
660,
|
|
1260,
|
|
1860,
|
|
2220,
|
|
720,
|
|
1316,
|
|
1950,
|
|
2310,
|
|
750,
|
|
1372,
|
|
2040,
|
|
2430
|
|
];
|
|
exports.getBlocksCount = function getBlocksCount(version, errorCorrectionLevel) {
|
|
switch (errorCorrectionLevel) {
|
|
case ECLevel.L:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 0];
|
|
case ECLevel.M:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 1];
|
|
case ECLevel.Q:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 2];
|
|
case ECLevel.H:
|
|
return EC_BLOCKS_TABLE[(version - 1) * 4 + 3];
|
|
default:
|
|
return void 0;
|
|
}
|
|
};
|
|
exports.getTotalCodewordsCount = function getTotalCodewordsCount(version, errorCorrectionLevel) {
|
|
switch (errorCorrectionLevel) {
|
|
case ECLevel.L:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 0];
|
|
case ECLevel.M:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 1];
|
|
case ECLevel.Q:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 2];
|
|
case ECLevel.H:
|
|
return EC_CODEWORDS_TABLE[(version - 1) * 4 + 3];
|
|
default:
|
|
return void 0;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/galois-field.js
|
|
var require_galois_field = __commonJS({
|
|
"node_modules/qrcode/lib/core/galois-field.js"(exports) {
|
|
var EXP_TABLE = new Uint8Array(512);
|
|
var LOG_TABLE = new Uint8Array(256);
|
|
(function initTables() {
|
|
let x = 1;
|
|
for (let i = 0; i < 255; i++) {
|
|
EXP_TABLE[i] = x;
|
|
LOG_TABLE[x] = i;
|
|
x <<= 1;
|
|
if (x & 256) {
|
|
x ^= 285;
|
|
}
|
|
}
|
|
for (let i = 255; i < 512; i++) {
|
|
EXP_TABLE[i] = EXP_TABLE[i - 255];
|
|
}
|
|
})();
|
|
exports.log = function log(n) {
|
|
if (n < 1) throw new Error("log(" + n + ")");
|
|
return LOG_TABLE[n];
|
|
};
|
|
exports.exp = function exp(n) {
|
|
return EXP_TABLE[n];
|
|
};
|
|
exports.mul = function mul(x, y) {
|
|
if (x === 0 || y === 0) return 0;
|
|
return EXP_TABLE[LOG_TABLE[x] + LOG_TABLE[y]];
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/polynomial.js
|
|
var require_polynomial = __commonJS({
|
|
"node_modules/qrcode/lib/core/polynomial.js"(exports) {
|
|
var GF = require_galois_field();
|
|
exports.mul = function mul(p1, p2) {
|
|
const coeff = new Uint8Array(p1.length + p2.length - 1);
|
|
for (let i = 0; i < p1.length; i++) {
|
|
for (let j = 0; j < p2.length; j++) {
|
|
coeff[i + j] ^= GF.mul(p1[i], p2[j]);
|
|
}
|
|
}
|
|
return coeff;
|
|
};
|
|
exports.mod = function mod(divident, divisor) {
|
|
let result = new Uint8Array(divident);
|
|
while (result.length - divisor.length >= 0) {
|
|
const coeff = result[0];
|
|
for (let i = 0; i < divisor.length; i++) {
|
|
result[i] ^= GF.mul(divisor[i], coeff);
|
|
}
|
|
let offset = 0;
|
|
while (offset < result.length && result[offset] === 0) offset++;
|
|
result = result.slice(offset);
|
|
}
|
|
return result;
|
|
};
|
|
exports.generateECPolynomial = function generateECPolynomial(degree) {
|
|
let poly = new Uint8Array([1]);
|
|
for (let i = 0; i < degree; i++) {
|
|
poly = exports.mul(poly, new Uint8Array([1, GF.exp(i)]));
|
|
}
|
|
return poly;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/reed-solomon-encoder.js
|
|
var require_reed_solomon_encoder = __commonJS({
|
|
"node_modules/qrcode/lib/core/reed-solomon-encoder.js"(exports, module) {
|
|
var Polynomial = require_polynomial();
|
|
function ReedSolomonEncoder(degree) {
|
|
this.genPoly = void 0;
|
|
this.degree = degree;
|
|
if (this.degree) this.initialize(this.degree);
|
|
}
|
|
ReedSolomonEncoder.prototype.initialize = function initialize(degree) {
|
|
this.degree = degree;
|
|
this.genPoly = Polynomial.generateECPolynomial(this.degree);
|
|
};
|
|
ReedSolomonEncoder.prototype.encode = function encode(data) {
|
|
if (!this.genPoly) {
|
|
throw new Error("Encoder not initialized");
|
|
}
|
|
const paddedData = new Uint8Array(data.length + this.degree);
|
|
paddedData.set(data);
|
|
const remainder = Polynomial.mod(paddedData, this.genPoly);
|
|
const start = this.degree - remainder.length;
|
|
if (start > 0) {
|
|
const buff = new Uint8Array(this.degree);
|
|
buff.set(remainder, start);
|
|
return buff;
|
|
}
|
|
return remainder;
|
|
};
|
|
module.exports = ReedSolomonEncoder;
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/version-check.js
|
|
var require_version_check = __commonJS({
|
|
"node_modules/qrcode/lib/core/version-check.js"(exports) {
|
|
exports.isValid = function isValid(version) {
|
|
return !isNaN(version) && version >= 1 && version <= 40;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/regex.js
|
|
var require_regex = __commonJS({
|
|
"node_modules/qrcode/lib/core/regex.js"(exports) {
|
|
var numeric = "[0-9]+";
|
|
var alphanumeric = "[A-Z $%*+\\-./:]+";
|
|
var kanji = "(?:[u3000-u303F]|[u3040-u309F]|[u30A0-u30FF]|[uFF00-uFFEF]|[u4E00-u9FAF]|[u2605-u2606]|[u2190-u2195]|u203B|[u2010u2015u2018u2019u2025u2026u201Cu201Du2225u2260]|[u0391-u0451]|[u00A7u00A8u00B1u00B4u00D7u00F7])+";
|
|
kanji = kanji.replace(/u/g, "\\u");
|
|
var byte = "(?:(?![A-Z0-9 $%*+\\-./:]|" + kanji + ")(?:.|[\r\n]))+";
|
|
exports.KANJI = new RegExp(kanji, "g");
|
|
exports.BYTE_KANJI = new RegExp("[^A-Z0-9 $%*+\\-./:]+", "g");
|
|
exports.BYTE = new RegExp(byte, "g");
|
|
exports.NUMERIC = new RegExp(numeric, "g");
|
|
exports.ALPHANUMERIC = new RegExp(alphanumeric, "g");
|
|
var TEST_KANJI = new RegExp("^" + kanji + "$");
|
|
var TEST_NUMERIC = new RegExp("^" + numeric + "$");
|
|
var TEST_ALPHANUMERIC = new RegExp("^[A-Z0-9 $%*+\\-./:]+$");
|
|
exports.testKanji = function testKanji(str) {
|
|
return TEST_KANJI.test(str);
|
|
};
|
|
exports.testNumeric = function testNumeric(str) {
|
|
return TEST_NUMERIC.test(str);
|
|
};
|
|
exports.testAlphanumeric = function testAlphanumeric(str) {
|
|
return TEST_ALPHANUMERIC.test(str);
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/mode.js
|
|
var require_mode = __commonJS({
|
|
"node_modules/qrcode/lib/core/mode.js"(exports) {
|
|
var VersionCheck = require_version_check();
|
|
var Regex = require_regex();
|
|
exports.NUMERIC = {
|
|
id: "Numeric",
|
|
bit: 1 << 0,
|
|
ccBits: [10, 12, 14]
|
|
};
|
|
exports.ALPHANUMERIC = {
|
|
id: "Alphanumeric",
|
|
bit: 1 << 1,
|
|
ccBits: [9, 11, 13]
|
|
};
|
|
exports.BYTE = {
|
|
id: "Byte",
|
|
bit: 1 << 2,
|
|
ccBits: [8, 16, 16]
|
|
};
|
|
exports.KANJI = {
|
|
id: "Kanji",
|
|
bit: 1 << 3,
|
|
ccBits: [8, 10, 12]
|
|
};
|
|
exports.MIXED = {
|
|
bit: -1
|
|
};
|
|
exports.getCharCountIndicator = function getCharCountIndicator(mode, version) {
|
|
if (!mode.ccBits) throw new Error("Invalid mode: " + mode);
|
|
if (!VersionCheck.isValid(version)) {
|
|
throw new Error("Invalid version: " + version);
|
|
}
|
|
if (version >= 1 && version < 10) return mode.ccBits[0];
|
|
else if (version < 27) return mode.ccBits[1];
|
|
return mode.ccBits[2];
|
|
};
|
|
exports.getBestModeForData = function getBestModeForData(dataStr) {
|
|
if (Regex.testNumeric(dataStr)) return exports.NUMERIC;
|
|
else if (Regex.testAlphanumeric(dataStr)) return exports.ALPHANUMERIC;
|
|
else if (Regex.testKanji(dataStr)) return exports.KANJI;
|
|
else return exports.BYTE;
|
|
};
|
|
exports.toString = function toString(mode) {
|
|
if (mode && mode.id) return mode.id;
|
|
throw new Error("Invalid mode");
|
|
};
|
|
exports.isValid = function isValid(mode) {
|
|
return mode && mode.bit && mode.ccBits;
|
|
};
|
|
function fromString(string) {
|
|
if (typeof string !== "string") {
|
|
throw new Error("Param is not a string");
|
|
}
|
|
const lcStr = string.toLowerCase();
|
|
switch (lcStr) {
|
|
case "numeric":
|
|
return exports.NUMERIC;
|
|
case "alphanumeric":
|
|
return exports.ALPHANUMERIC;
|
|
case "kanji":
|
|
return exports.KANJI;
|
|
case "byte":
|
|
return exports.BYTE;
|
|
default:
|
|
throw new Error("Unknown mode: " + string);
|
|
}
|
|
}
|
|
exports.from = function from(value, defaultValue) {
|
|
if (exports.isValid(value)) {
|
|
return value;
|
|
}
|
|
try {
|
|
return fromString(value);
|
|
} catch (e) {
|
|
return defaultValue;
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/version.js
|
|
var require_version = __commonJS({
|
|
"node_modules/qrcode/lib/core/version.js"(exports) {
|
|
var Utils = require_utils();
|
|
var ECCode = require_error_correction_code();
|
|
var ECLevel = require_error_correction_level();
|
|
var Mode = require_mode();
|
|
var VersionCheck = require_version_check();
|
|
var G18 = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0;
|
|
var G18_BCH = Utils.getBCHDigit(G18);
|
|
function getBestVersionForDataLength(mode, length, errorCorrectionLevel) {
|
|
for (let currentVersion = 1; currentVersion <= 40; currentVersion++) {
|
|
if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, mode)) {
|
|
return currentVersion;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
function getReservedBitsCount(mode, version) {
|
|
return Mode.getCharCountIndicator(mode, version) + 4;
|
|
}
|
|
function getTotalBitsFromDataArray(segments, version) {
|
|
let totalBits = 0;
|
|
segments.forEach(function(data) {
|
|
const reservedBits = getReservedBitsCount(data.mode, version);
|
|
totalBits += reservedBits + data.getBitsLength();
|
|
});
|
|
return totalBits;
|
|
}
|
|
function getBestVersionForMixedData(segments, errorCorrectionLevel) {
|
|
for (let currentVersion = 1; currentVersion <= 40; currentVersion++) {
|
|
const length = getTotalBitsFromDataArray(segments, currentVersion);
|
|
if (length <= exports.getCapacity(currentVersion, errorCorrectionLevel, Mode.MIXED)) {
|
|
return currentVersion;
|
|
}
|
|
}
|
|
return void 0;
|
|
}
|
|
exports.from = function from(value, defaultValue) {
|
|
if (VersionCheck.isValid(value)) {
|
|
return parseInt(value, 10);
|
|
}
|
|
return defaultValue;
|
|
};
|
|
exports.getCapacity = function getCapacity(version, errorCorrectionLevel, mode) {
|
|
if (!VersionCheck.isValid(version)) {
|
|
throw new Error("Invalid QR Code version");
|
|
}
|
|
if (typeof mode === "undefined") mode = Mode.BYTE;
|
|
const totalCodewords = Utils.getSymbolTotalCodewords(version);
|
|
const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel);
|
|
const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8;
|
|
if (mode === Mode.MIXED) return dataTotalCodewordsBits;
|
|
const usableBits = dataTotalCodewordsBits - getReservedBitsCount(mode, version);
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return Math.floor(usableBits / 10 * 3);
|
|
case Mode.ALPHANUMERIC:
|
|
return Math.floor(usableBits / 11 * 2);
|
|
case Mode.KANJI:
|
|
return Math.floor(usableBits / 13);
|
|
case Mode.BYTE:
|
|
default:
|
|
return Math.floor(usableBits / 8);
|
|
}
|
|
};
|
|
exports.getBestVersionForData = function getBestVersionForData(data, errorCorrectionLevel) {
|
|
let seg;
|
|
const ecl = ECLevel.from(errorCorrectionLevel, ECLevel.M);
|
|
if (Array.isArray(data)) {
|
|
if (data.length > 1) {
|
|
return getBestVersionForMixedData(data, ecl);
|
|
}
|
|
if (data.length === 0) {
|
|
return 1;
|
|
}
|
|
seg = data[0];
|
|
} else {
|
|
seg = data;
|
|
}
|
|
return getBestVersionForDataLength(seg.mode, seg.getLength(), ecl);
|
|
};
|
|
exports.getEncodedBits = function getEncodedBits(version) {
|
|
if (!VersionCheck.isValid(version) || version < 7) {
|
|
throw new Error("Invalid QR Code version");
|
|
}
|
|
let d = version << 12;
|
|
while (Utils.getBCHDigit(d) - G18_BCH >= 0) {
|
|
d ^= G18 << Utils.getBCHDigit(d) - G18_BCH;
|
|
}
|
|
return version << 12 | d;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/format-info.js
|
|
var require_format_info = __commonJS({
|
|
"node_modules/qrcode/lib/core/format-info.js"(exports) {
|
|
var Utils = require_utils();
|
|
var G15 = 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0;
|
|
var G15_MASK = 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1;
|
|
var G15_BCH = Utils.getBCHDigit(G15);
|
|
exports.getEncodedBits = function getEncodedBits(errorCorrectionLevel, mask) {
|
|
const data = errorCorrectionLevel.bit << 3 | mask;
|
|
let d = data << 10;
|
|
while (Utils.getBCHDigit(d) - G15_BCH >= 0) {
|
|
d ^= G15 << Utils.getBCHDigit(d) - G15_BCH;
|
|
}
|
|
return (data << 10 | d) ^ G15_MASK;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/numeric-data.js
|
|
var require_numeric_data = __commonJS({
|
|
"node_modules/qrcode/lib/core/numeric-data.js"(exports, module) {
|
|
var Mode = require_mode();
|
|
function NumericData(data) {
|
|
this.mode = Mode.NUMERIC;
|
|
this.data = data.toString();
|
|
}
|
|
NumericData.getBitsLength = function getBitsLength(length) {
|
|
return 10 * Math.floor(length / 3) + (length % 3 ? length % 3 * 3 + 1 : 0);
|
|
};
|
|
NumericData.prototype.getLength = function getLength() {
|
|
return this.data.length;
|
|
};
|
|
NumericData.prototype.getBitsLength = function getBitsLength() {
|
|
return NumericData.getBitsLength(this.data.length);
|
|
};
|
|
NumericData.prototype.write = function write(bitBuffer) {
|
|
let i, group, value;
|
|
for (i = 0; i + 3 <= this.data.length; i += 3) {
|
|
group = this.data.substr(i, 3);
|
|
value = parseInt(group, 10);
|
|
bitBuffer.put(value, 10);
|
|
}
|
|
const remainingNum = this.data.length - i;
|
|
if (remainingNum > 0) {
|
|
group = this.data.substr(i);
|
|
value = parseInt(group, 10);
|
|
bitBuffer.put(value, remainingNum * 3 + 1);
|
|
}
|
|
};
|
|
module.exports = NumericData;
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/alphanumeric-data.js
|
|
var require_alphanumeric_data = __commonJS({
|
|
"node_modules/qrcode/lib/core/alphanumeric-data.js"(exports, module) {
|
|
var Mode = require_mode();
|
|
var ALPHA_NUM_CHARS = [
|
|
"0",
|
|
"1",
|
|
"2",
|
|
"3",
|
|
"4",
|
|
"5",
|
|
"6",
|
|
"7",
|
|
"8",
|
|
"9",
|
|
"A",
|
|
"B",
|
|
"C",
|
|
"D",
|
|
"E",
|
|
"F",
|
|
"G",
|
|
"H",
|
|
"I",
|
|
"J",
|
|
"K",
|
|
"L",
|
|
"M",
|
|
"N",
|
|
"O",
|
|
"P",
|
|
"Q",
|
|
"R",
|
|
"S",
|
|
"T",
|
|
"U",
|
|
"V",
|
|
"W",
|
|
"X",
|
|
"Y",
|
|
"Z",
|
|
" ",
|
|
"$",
|
|
"%",
|
|
"*",
|
|
"+",
|
|
"-",
|
|
".",
|
|
"/",
|
|
":"
|
|
];
|
|
function AlphanumericData(data) {
|
|
this.mode = Mode.ALPHANUMERIC;
|
|
this.data = data;
|
|
}
|
|
AlphanumericData.getBitsLength = function getBitsLength(length) {
|
|
return 11 * Math.floor(length / 2) + 6 * (length % 2);
|
|
};
|
|
AlphanumericData.prototype.getLength = function getLength() {
|
|
return this.data.length;
|
|
};
|
|
AlphanumericData.prototype.getBitsLength = function getBitsLength() {
|
|
return AlphanumericData.getBitsLength(this.data.length);
|
|
};
|
|
AlphanumericData.prototype.write = function write(bitBuffer) {
|
|
let i;
|
|
for (i = 0; i + 2 <= this.data.length; i += 2) {
|
|
let value = ALPHA_NUM_CHARS.indexOf(this.data[i]) * 45;
|
|
value += ALPHA_NUM_CHARS.indexOf(this.data[i + 1]);
|
|
bitBuffer.put(value, 11);
|
|
}
|
|
if (this.data.length % 2) {
|
|
bitBuffer.put(ALPHA_NUM_CHARS.indexOf(this.data[i]), 6);
|
|
}
|
|
};
|
|
module.exports = AlphanumericData;
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/byte-data.js
|
|
var require_byte_data = __commonJS({
|
|
"node_modules/qrcode/lib/core/byte-data.js"(exports, module) {
|
|
var Mode = require_mode();
|
|
function ByteData(data) {
|
|
this.mode = Mode.BYTE;
|
|
if (typeof data === "string") {
|
|
this.data = new TextEncoder().encode(data);
|
|
} else {
|
|
this.data = new Uint8Array(data);
|
|
}
|
|
}
|
|
ByteData.getBitsLength = function getBitsLength(length) {
|
|
return length * 8;
|
|
};
|
|
ByteData.prototype.getLength = function getLength() {
|
|
return this.data.length;
|
|
};
|
|
ByteData.prototype.getBitsLength = function getBitsLength() {
|
|
return ByteData.getBitsLength(this.data.length);
|
|
};
|
|
ByteData.prototype.write = function(bitBuffer) {
|
|
for (let i = 0, l = this.data.length; i < l; i++) {
|
|
bitBuffer.put(this.data[i], 8);
|
|
}
|
|
};
|
|
module.exports = ByteData;
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/kanji-data.js
|
|
var require_kanji_data = __commonJS({
|
|
"node_modules/qrcode/lib/core/kanji-data.js"(exports, module) {
|
|
var Mode = require_mode();
|
|
var Utils = require_utils();
|
|
function KanjiData(data) {
|
|
this.mode = Mode.KANJI;
|
|
this.data = data;
|
|
}
|
|
KanjiData.getBitsLength = function getBitsLength(length) {
|
|
return length * 13;
|
|
};
|
|
KanjiData.prototype.getLength = function getLength() {
|
|
return this.data.length;
|
|
};
|
|
KanjiData.prototype.getBitsLength = function getBitsLength() {
|
|
return KanjiData.getBitsLength(this.data.length);
|
|
};
|
|
KanjiData.prototype.write = function(bitBuffer) {
|
|
let i;
|
|
for (i = 0; i < this.data.length; i++) {
|
|
let value = Utils.toSJIS(this.data[i]);
|
|
if (value >= 33088 && value <= 40956) {
|
|
value -= 33088;
|
|
} else if (value >= 57408 && value <= 60351) {
|
|
value -= 49472;
|
|
} else {
|
|
throw new Error(
|
|
"Invalid SJIS character: " + this.data[i] + "\nMake sure your charset is UTF-8"
|
|
);
|
|
}
|
|
value = (value >>> 8 & 255) * 192 + (value & 255);
|
|
bitBuffer.put(value, 13);
|
|
}
|
|
};
|
|
module.exports = KanjiData;
|
|
}
|
|
});
|
|
|
|
// node_modules/dijkstrajs/dijkstra.js
|
|
var require_dijkstra = __commonJS({
|
|
"node_modules/dijkstrajs/dijkstra.js"(exports, module) {
|
|
"use strict";
|
|
var dijkstra = {
|
|
single_source_shortest_paths: function(graph, s, d) {
|
|
var predecessors = {};
|
|
var costs = {};
|
|
costs[s] = 0;
|
|
var open = dijkstra.PriorityQueue.make();
|
|
open.push(s, 0);
|
|
var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit;
|
|
while (!open.empty()) {
|
|
closest = open.pop();
|
|
u = closest.value;
|
|
cost_of_s_to_u = closest.cost;
|
|
adjacent_nodes = graph[u] || {};
|
|
for (v in adjacent_nodes) {
|
|
if (adjacent_nodes.hasOwnProperty(v)) {
|
|
cost_of_e = adjacent_nodes[v];
|
|
cost_of_s_to_u_plus_cost_of_e = cost_of_s_to_u + cost_of_e;
|
|
cost_of_s_to_v = costs[v];
|
|
first_visit = typeof costs[v] === "undefined";
|
|
if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
|
|
costs[v] = cost_of_s_to_u_plus_cost_of_e;
|
|
open.push(v, cost_of_s_to_u_plus_cost_of_e);
|
|
predecessors[v] = u;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (typeof d !== "undefined" && typeof costs[d] === "undefined") {
|
|
var msg = ["Could not find a path from ", s, " to ", d, "."].join("");
|
|
throw new Error(msg);
|
|
}
|
|
return predecessors;
|
|
},
|
|
extract_shortest_path_from_predecessor_list: function(predecessors, d) {
|
|
var nodes = [];
|
|
var u = d;
|
|
var predecessor;
|
|
while (u) {
|
|
nodes.push(u);
|
|
predecessor = predecessors[u];
|
|
u = predecessors[u];
|
|
}
|
|
nodes.reverse();
|
|
return nodes;
|
|
},
|
|
find_path: function(graph, s, d) {
|
|
var predecessors = dijkstra.single_source_shortest_paths(graph, s, d);
|
|
return dijkstra.extract_shortest_path_from_predecessor_list(
|
|
predecessors,
|
|
d
|
|
);
|
|
},
|
|
/**
|
|
* A very naive priority queue implementation.
|
|
*/
|
|
PriorityQueue: {
|
|
make: function(opts) {
|
|
var T = dijkstra.PriorityQueue, t = {}, key;
|
|
opts = opts || {};
|
|
for (key in T) {
|
|
if (T.hasOwnProperty(key)) {
|
|
t[key] = T[key];
|
|
}
|
|
}
|
|
t.queue = [];
|
|
t.sorter = opts.sorter || T.default_sorter;
|
|
return t;
|
|
},
|
|
default_sorter: function(a, b) {
|
|
return a.cost - b.cost;
|
|
},
|
|
/**
|
|
* Add a new item to the queue and ensure the highest priority element
|
|
* is at the front of the queue.
|
|
*/
|
|
push: function(value, cost) {
|
|
var item = { value, cost };
|
|
this.queue.push(item);
|
|
this.queue.sort(this.sorter);
|
|
},
|
|
/**
|
|
* Return the highest priority element in the queue.
|
|
*/
|
|
pop: function() {
|
|
return this.queue.shift();
|
|
},
|
|
empty: function() {
|
|
return this.queue.length === 0;
|
|
}
|
|
}
|
|
};
|
|
if (typeof module !== "undefined") {
|
|
module.exports = dijkstra;
|
|
}
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/segments.js
|
|
var require_segments = __commonJS({
|
|
"node_modules/qrcode/lib/core/segments.js"(exports) {
|
|
var Mode = require_mode();
|
|
var NumericData = require_numeric_data();
|
|
var AlphanumericData = require_alphanumeric_data();
|
|
var ByteData = require_byte_data();
|
|
var KanjiData = require_kanji_data();
|
|
var Regex = require_regex();
|
|
var Utils = require_utils();
|
|
var dijkstra = require_dijkstra();
|
|
function getStringByteLength(str) {
|
|
return unescape(encodeURIComponent(str)).length;
|
|
}
|
|
function getSegments(regex, mode, str) {
|
|
const segments = [];
|
|
let result;
|
|
while ((result = regex.exec(str)) !== null) {
|
|
segments.push({
|
|
data: result[0],
|
|
index: result.index,
|
|
mode,
|
|
length: result[0].length
|
|
});
|
|
}
|
|
return segments;
|
|
}
|
|
function getSegmentsFromString(dataStr) {
|
|
const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr);
|
|
const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr);
|
|
let byteSegs;
|
|
let kanjiSegs;
|
|
if (Utils.isKanjiModeEnabled()) {
|
|
byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr);
|
|
kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr);
|
|
} else {
|
|
byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr);
|
|
kanjiSegs = [];
|
|
}
|
|
const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs);
|
|
return segs.sort(function(s1, s2) {
|
|
return s1.index - s2.index;
|
|
}).map(function(obj) {
|
|
return {
|
|
data: obj.data,
|
|
mode: obj.mode,
|
|
length: obj.length
|
|
};
|
|
});
|
|
}
|
|
function getSegmentBitsLength(length, mode) {
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return NumericData.getBitsLength(length);
|
|
case Mode.ALPHANUMERIC:
|
|
return AlphanumericData.getBitsLength(length);
|
|
case Mode.KANJI:
|
|
return KanjiData.getBitsLength(length);
|
|
case Mode.BYTE:
|
|
return ByteData.getBitsLength(length);
|
|
}
|
|
}
|
|
function mergeSegments(segs) {
|
|
return segs.reduce(function(acc, curr) {
|
|
const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null;
|
|
if (prevSeg && prevSeg.mode === curr.mode) {
|
|
acc[acc.length - 1].data += curr.data;
|
|
return acc;
|
|
}
|
|
acc.push(curr);
|
|
return acc;
|
|
}, []);
|
|
}
|
|
function buildNodes(segs) {
|
|
const nodes = [];
|
|
for (let i = 0; i < segs.length; i++) {
|
|
const seg = segs[i];
|
|
switch (seg.mode) {
|
|
case Mode.NUMERIC:
|
|
nodes.push([
|
|
seg,
|
|
{ data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length },
|
|
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
|
|
]);
|
|
break;
|
|
case Mode.ALPHANUMERIC:
|
|
nodes.push([
|
|
seg,
|
|
{ data: seg.data, mode: Mode.BYTE, length: seg.length }
|
|
]);
|
|
break;
|
|
case Mode.KANJI:
|
|
nodes.push([
|
|
seg,
|
|
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
|
|
]);
|
|
break;
|
|
case Mode.BYTE:
|
|
nodes.push([
|
|
{ data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) }
|
|
]);
|
|
}
|
|
}
|
|
return nodes;
|
|
}
|
|
function buildGraph(nodes, version) {
|
|
const table = {};
|
|
const graph = { start: {} };
|
|
let prevNodeIds = ["start"];
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const nodeGroup = nodes[i];
|
|
const currentNodeIds = [];
|
|
for (let j = 0; j < nodeGroup.length; j++) {
|
|
const node = nodeGroup[j];
|
|
const key = "" + i + j;
|
|
currentNodeIds.push(key);
|
|
table[key] = { node, lastCount: 0 };
|
|
graph[key] = {};
|
|
for (let n = 0; n < prevNodeIds.length; n++) {
|
|
const prevNodeId = prevNodeIds[n];
|
|
if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) {
|
|
graph[prevNodeId][key] = getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) - getSegmentBitsLength(table[prevNodeId].lastCount, node.mode);
|
|
table[prevNodeId].lastCount += node.length;
|
|
} else {
|
|
if (table[prevNodeId]) table[prevNodeId].lastCount = node.length;
|
|
graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) + 4 + Mode.getCharCountIndicator(node.mode, version);
|
|
}
|
|
}
|
|
}
|
|
prevNodeIds = currentNodeIds;
|
|
}
|
|
for (let n = 0; n < prevNodeIds.length; n++) {
|
|
graph[prevNodeIds[n]].end = 0;
|
|
}
|
|
return { map: graph, table };
|
|
}
|
|
function buildSingleSegment(data, modesHint) {
|
|
let mode;
|
|
const bestMode = Mode.getBestModeForData(data);
|
|
mode = Mode.from(modesHint, bestMode);
|
|
if (mode !== Mode.BYTE && mode.bit < bestMode.bit) {
|
|
throw new Error('"' + data + '" cannot be encoded with mode ' + Mode.toString(mode) + ".\n Suggested mode is: " + Mode.toString(bestMode));
|
|
}
|
|
if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) {
|
|
mode = Mode.BYTE;
|
|
}
|
|
switch (mode) {
|
|
case Mode.NUMERIC:
|
|
return new NumericData(data);
|
|
case Mode.ALPHANUMERIC:
|
|
return new AlphanumericData(data);
|
|
case Mode.KANJI:
|
|
return new KanjiData(data);
|
|
case Mode.BYTE:
|
|
return new ByteData(data);
|
|
}
|
|
}
|
|
exports.fromArray = function fromArray(array) {
|
|
return array.reduce(function(acc, seg) {
|
|
if (typeof seg === "string") {
|
|
acc.push(buildSingleSegment(seg, null));
|
|
} else if (seg.data) {
|
|
acc.push(buildSingleSegment(seg.data, seg.mode));
|
|
}
|
|
return acc;
|
|
}, []);
|
|
};
|
|
exports.fromString = function fromString(data, version) {
|
|
const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled());
|
|
const nodes = buildNodes(segs);
|
|
const graph = buildGraph(nodes, version);
|
|
const path = dijkstra.find_path(graph.map, "start", "end");
|
|
const optimizedSegs = [];
|
|
for (let i = 1; i < path.length - 1; i++) {
|
|
optimizedSegs.push(graph.table[path[i]].node);
|
|
}
|
|
return exports.fromArray(mergeSegments(optimizedSegs));
|
|
};
|
|
exports.rawSplit = function rawSplit(data) {
|
|
return exports.fromArray(
|
|
getSegmentsFromString(data, Utils.isKanjiModeEnabled())
|
|
);
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/core/qrcode.js
|
|
var require_qrcode = __commonJS({
|
|
"node_modules/qrcode/lib/core/qrcode.js"(exports) {
|
|
var Utils = require_utils();
|
|
var ECLevel = require_error_correction_level();
|
|
var BitBuffer = require_bit_buffer();
|
|
var BitMatrix = require_bit_matrix();
|
|
var AlignmentPattern = require_alignment_pattern();
|
|
var FinderPattern = require_finder_pattern();
|
|
var MaskPattern = require_mask_pattern();
|
|
var ECCode = require_error_correction_code();
|
|
var ReedSolomonEncoder = require_reed_solomon_encoder();
|
|
var Version = require_version();
|
|
var FormatInfo = require_format_info();
|
|
var Mode = require_mode();
|
|
var Segments = require_segments();
|
|
function setupFinderPattern(matrix, version) {
|
|
const size = matrix.size;
|
|
const pos = FinderPattern.getPositions(version);
|
|
for (let i = 0; i < pos.length; i++) {
|
|
const row = pos[i][0];
|
|
const col = pos[i][1];
|
|
for (let r = -1; r <= 7; r++) {
|
|
if (row + r <= -1 || size <= row + r) continue;
|
|
for (let c = -1; c <= 7; c++) {
|
|
if (col + c <= -1 || size <= col + c) continue;
|
|
if (r >= 0 && r <= 6 && (c === 0 || c === 6) || c >= 0 && c <= 6 && (r === 0 || r === 6) || r >= 2 && r <= 4 && c >= 2 && c <= 4) {
|
|
matrix.set(row + r, col + c, true, true);
|
|
} else {
|
|
matrix.set(row + r, col + c, false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function setupTimingPattern(matrix) {
|
|
const size = matrix.size;
|
|
for (let r = 8; r < size - 8; r++) {
|
|
const value = r % 2 === 0;
|
|
matrix.set(r, 6, value, true);
|
|
matrix.set(6, r, value, true);
|
|
}
|
|
}
|
|
function setupAlignmentPattern(matrix, version) {
|
|
const pos = AlignmentPattern.getPositions(version);
|
|
for (let i = 0; i < pos.length; i++) {
|
|
const row = pos[i][0];
|
|
const col = pos[i][1];
|
|
for (let r = -2; r <= 2; r++) {
|
|
for (let c = -2; c <= 2; c++) {
|
|
if (r === -2 || r === 2 || c === -2 || c === 2 || r === 0 && c === 0) {
|
|
matrix.set(row + r, col + c, true, true);
|
|
} else {
|
|
matrix.set(row + r, col + c, false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function setupVersionInfo(matrix, version) {
|
|
const size = matrix.size;
|
|
const bits = Version.getEncodedBits(version);
|
|
let row, col, mod;
|
|
for (let i = 0; i < 18; i++) {
|
|
row = Math.floor(i / 3);
|
|
col = i % 3 + size - 8 - 3;
|
|
mod = (bits >> i & 1) === 1;
|
|
matrix.set(row, col, mod, true);
|
|
matrix.set(col, row, mod, true);
|
|
}
|
|
}
|
|
function setupFormatInfo(matrix, errorCorrectionLevel, maskPattern) {
|
|
const size = matrix.size;
|
|
const bits = FormatInfo.getEncodedBits(errorCorrectionLevel, maskPattern);
|
|
let i, mod;
|
|
for (i = 0; i < 15; i++) {
|
|
mod = (bits >> i & 1) === 1;
|
|
if (i < 6) {
|
|
matrix.set(i, 8, mod, true);
|
|
} else if (i < 8) {
|
|
matrix.set(i + 1, 8, mod, true);
|
|
} else {
|
|
matrix.set(size - 15 + i, 8, mod, true);
|
|
}
|
|
if (i < 8) {
|
|
matrix.set(8, size - i - 1, mod, true);
|
|
} else if (i < 9) {
|
|
matrix.set(8, 15 - i - 1 + 1, mod, true);
|
|
} else {
|
|
matrix.set(8, 15 - i - 1, mod, true);
|
|
}
|
|
}
|
|
matrix.set(size - 8, 8, 1, true);
|
|
}
|
|
function setupData(matrix, data) {
|
|
const size = matrix.size;
|
|
let inc = -1;
|
|
let row = size - 1;
|
|
let bitIndex = 7;
|
|
let byteIndex = 0;
|
|
for (let col = size - 1; col > 0; col -= 2) {
|
|
if (col === 6) col--;
|
|
while (true) {
|
|
for (let c = 0; c < 2; c++) {
|
|
if (!matrix.isReserved(row, col - c)) {
|
|
let dark = false;
|
|
if (byteIndex < data.length) {
|
|
dark = (data[byteIndex] >>> bitIndex & 1) === 1;
|
|
}
|
|
matrix.set(row, col - c, dark);
|
|
bitIndex--;
|
|
if (bitIndex === -1) {
|
|
byteIndex++;
|
|
bitIndex = 7;
|
|
}
|
|
}
|
|
}
|
|
row += inc;
|
|
if (row < 0 || size <= row) {
|
|
row -= inc;
|
|
inc = -inc;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function createData(version, errorCorrectionLevel, segments) {
|
|
const buffer = new BitBuffer();
|
|
segments.forEach(function(data) {
|
|
buffer.put(data.mode.bit, 4);
|
|
buffer.put(data.getLength(), Mode.getCharCountIndicator(data.mode, version));
|
|
data.write(buffer);
|
|
});
|
|
const totalCodewords = Utils.getSymbolTotalCodewords(version);
|
|
const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel);
|
|
const dataTotalCodewordsBits = (totalCodewords - ecTotalCodewords) * 8;
|
|
if (buffer.getLengthInBits() + 4 <= dataTotalCodewordsBits) {
|
|
buffer.put(0, 4);
|
|
}
|
|
while (buffer.getLengthInBits() % 8 !== 0) {
|
|
buffer.putBit(0);
|
|
}
|
|
const remainingByte = (dataTotalCodewordsBits - buffer.getLengthInBits()) / 8;
|
|
for (let i = 0; i < remainingByte; i++) {
|
|
buffer.put(i % 2 ? 17 : 236, 8);
|
|
}
|
|
return createCodewords(buffer, version, errorCorrectionLevel);
|
|
}
|
|
function createCodewords(bitBuffer, version, errorCorrectionLevel) {
|
|
const totalCodewords = Utils.getSymbolTotalCodewords(version);
|
|
const ecTotalCodewords = ECCode.getTotalCodewordsCount(version, errorCorrectionLevel);
|
|
const dataTotalCodewords = totalCodewords - ecTotalCodewords;
|
|
const ecTotalBlocks = ECCode.getBlocksCount(version, errorCorrectionLevel);
|
|
const blocksInGroup2 = totalCodewords % ecTotalBlocks;
|
|
const blocksInGroup1 = ecTotalBlocks - blocksInGroup2;
|
|
const totalCodewordsInGroup1 = Math.floor(totalCodewords / ecTotalBlocks);
|
|
const dataCodewordsInGroup1 = Math.floor(dataTotalCodewords / ecTotalBlocks);
|
|
const dataCodewordsInGroup2 = dataCodewordsInGroup1 + 1;
|
|
const ecCount = totalCodewordsInGroup1 - dataCodewordsInGroup1;
|
|
const rs = new ReedSolomonEncoder(ecCount);
|
|
let offset = 0;
|
|
const dcData = new Array(ecTotalBlocks);
|
|
const ecData = new Array(ecTotalBlocks);
|
|
let maxDataSize = 0;
|
|
const buffer = new Uint8Array(bitBuffer.buffer);
|
|
for (let b = 0; b < ecTotalBlocks; b++) {
|
|
const dataSize = b < blocksInGroup1 ? dataCodewordsInGroup1 : dataCodewordsInGroup2;
|
|
dcData[b] = buffer.slice(offset, offset + dataSize);
|
|
ecData[b] = rs.encode(dcData[b]);
|
|
offset += dataSize;
|
|
maxDataSize = Math.max(maxDataSize, dataSize);
|
|
}
|
|
const data = new Uint8Array(totalCodewords);
|
|
let index = 0;
|
|
let i, r;
|
|
for (i = 0; i < maxDataSize; i++) {
|
|
for (r = 0; r < ecTotalBlocks; r++) {
|
|
if (i < dcData[r].length) {
|
|
data[index++] = dcData[r][i];
|
|
}
|
|
}
|
|
}
|
|
for (i = 0; i < ecCount; i++) {
|
|
for (r = 0; r < ecTotalBlocks; r++) {
|
|
data[index++] = ecData[r][i];
|
|
}
|
|
}
|
|
return data;
|
|
}
|
|
function createSymbol(data, version, errorCorrectionLevel, maskPattern) {
|
|
let segments;
|
|
if (Array.isArray(data)) {
|
|
segments = Segments.fromArray(data);
|
|
} else if (typeof data === "string") {
|
|
let estimatedVersion = version;
|
|
if (!estimatedVersion) {
|
|
const rawSegments = Segments.rawSplit(data);
|
|
estimatedVersion = Version.getBestVersionForData(rawSegments, errorCorrectionLevel);
|
|
}
|
|
segments = Segments.fromString(data, estimatedVersion || 40);
|
|
} else {
|
|
throw new Error("Invalid data");
|
|
}
|
|
const bestVersion = Version.getBestVersionForData(segments, errorCorrectionLevel);
|
|
if (!bestVersion) {
|
|
throw new Error("The amount of data is too big to be stored in a QR Code");
|
|
}
|
|
if (!version) {
|
|
version = bestVersion;
|
|
} else if (version < bestVersion) {
|
|
throw new Error(
|
|
"\nThe chosen QR Code version cannot contain this amount of data.\nMinimum version required to store current data is: " + bestVersion + ".\n"
|
|
);
|
|
}
|
|
const dataBits = createData(version, errorCorrectionLevel, segments);
|
|
const moduleCount = Utils.getSymbolSize(version);
|
|
const modules = new BitMatrix(moduleCount);
|
|
setupFinderPattern(modules, version);
|
|
setupTimingPattern(modules);
|
|
setupAlignmentPattern(modules, version);
|
|
setupFormatInfo(modules, errorCorrectionLevel, 0);
|
|
if (version >= 7) {
|
|
setupVersionInfo(modules, version);
|
|
}
|
|
setupData(modules, dataBits);
|
|
if (isNaN(maskPattern)) {
|
|
maskPattern = MaskPattern.getBestMask(
|
|
modules,
|
|
setupFormatInfo.bind(null, modules, errorCorrectionLevel)
|
|
);
|
|
}
|
|
MaskPattern.applyMask(maskPattern, modules);
|
|
setupFormatInfo(modules, errorCorrectionLevel, maskPattern);
|
|
return {
|
|
modules,
|
|
version,
|
|
errorCorrectionLevel,
|
|
maskPattern,
|
|
segments
|
|
};
|
|
}
|
|
exports.create = function create(data, options) {
|
|
if (typeof data === "undefined" || data === "") {
|
|
throw new Error("No input text");
|
|
}
|
|
let errorCorrectionLevel = ECLevel.M;
|
|
let version;
|
|
let mask;
|
|
if (typeof options !== "undefined") {
|
|
errorCorrectionLevel = ECLevel.from(options.errorCorrectionLevel, ECLevel.M);
|
|
version = Version.from(options.version);
|
|
mask = MaskPattern.from(options.maskPattern);
|
|
if (options.toSJISFunc) {
|
|
Utils.setToSJISFunction(options.toSJISFunc);
|
|
}
|
|
}
|
|
return createSymbol(data, version, errorCorrectionLevel, mask);
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/renderer/utils.js
|
|
var require_utils2 = __commonJS({
|
|
"node_modules/qrcode/lib/renderer/utils.js"(exports) {
|
|
function hex2rgba(hex) {
|
|
if (typeof hex === "number") {
|
|
hex = hex.toString();
|
|
}
|
|
if (typeof hex !== "string") {
|
|
throw new Error("Color should be defined as hex string");
|
|
}
|
|
let hexCode = hex.slice().replace("#", "").split("");
|
|
if (hexCode.length < 3 || hexCode.length === 5 || hexCode.length > 8) {
|
|
throw new Error("Invalid hex color: " + hex);
|
|
}
|
|
if (hexCode.length === 3 || hexCode.length === 4) {
|
|
hexCode = Array.prototype.concat.apply([], hexCode.map(function(c) {
|
|
return [c, c];
|
|
}));
|
|
}
|
|
if (hexCode.length === 6) hexCode.push("F", "F");
|
|
const hexValue = parseInt(hexCode.join(""), 16);
|
|
return {
|
|
r: hexValue >> 24 & 255,
|
|
g: hexValue >> 16 & 255,
|
|
b: hexValue >> 8 & 255,
|
|
a: hexValue & 255,
|
|
hex: "#" + hexCode.slice(0, 6).join("")
|
|
};
|
|
}
|
|
exports.getOptions = function getOptions(options) {
|
|
if (!options) options = {};
|
|
if (!options.color) options.color = {};
|
|
const margin = typeof options.margin === "undefined" || options.margin === null || options.margin < 0 ? 4 : options.margin;
|
|
const width = options.width && options.width >= 21 ? options.width : void 0;
|
|
const scale = options.scale || 4;
|
|
return {
|
|
width,
|
|
scale: width ? 4 : scale,
|
|
margin,
|
|
color: {
|
|
dark: hex2rgba(options.color.dark || "#000000ff"),
|
|
light: hex2rgba(options.color.light || "#ffffffff")
|
|
},
|
|
type: options.type,
|
|
rendererOpts: options.rendererOpts || {}
|
|
};
|
|
};
|
|
exports.getScale = function getScale(qrSize, opts) {
|
|
return opts.width && opts.width >= qrSize + opts.margin * 2 ? opts.width / (qrSize + opts.margin * 2) : opts.scale;
|
|
};
|
|
exports.getImageWidth = function getImageWidth(qrSize, opts) {
|
|
const scale = exports.getScale(qrSize, opts);
|
|
return Math.floor((qrSize + opts.margin * 2) * scale);
|
|
};
|
|
exports.qrToImageData = function qrToImageData(imgData, qr, opts) {
|
|
const size = qr.modules.size;
|
|
const data = qr.modules.data;
|
|
const scale = exports.getScale(size, opts);
|
|
const symbolSize = Math.floor((size + opts.margin * 2) * scale);
|
|
const scaledMargin = opts.margin * scale;
|
|
const palette = [opts.color.light, opts.color.dark];
|
|
for (let i = 0; i < symbolSize; i++) {
|
|
for (let j = 0; j < symbolSize; j++) {
|
|
let posDst = (i * symbolSize + j) * 4;
|
|
let pxColor = opts.color.light;
|
|
if (i >= scaledMargin && j >= scaledMargin && i < symbolSize - scaledMargin && j < symbolSize - scaledMargin) {
|
|
const iSrc = Math.floor((i - scaledMargin) / scale);
|
|
const jSrc = Math.floor((j - scaledMargin) / scale);
|
|
pxColor = palette[data[iSrc * size + jSrc] ? 1 : 0];
|
|
}
|
|
imgData[posDst++] = pxColor.r;
|
|
imgData[posDst++] = pxColor.g;
|
|
imgData[posDst++] = pxColor.b;
|
|
imgData[posDst] = pxColor.a;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/renderer/canvas.js
|
|
var require_canvas = __commonJS({
|
|
"node_modules/qrcode/lib/renderer/canvas.js"(exports) {
|
|
var Utils = require_utils2();
|
|
function clearCanvas(ctx, canvas, size) {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
if (!canvas.style) canvas.style = {};
|
|
canvas.height = size;
|
|
canvas.width = size;
|
|
canvas.style.height = size + "px";
|
|
canvas.style.width = size + "px";
|
|
}
|
|
function getCanvasElement() {
|
|
try {
|
|
return document.createElement("canvas");
|
|
} catch (e) {
|
|
throw new Error("You need to specify a canvas element");
|
|
}
|
|
}
|
|
exports.render = function render(qrData, canvas, options) {
|
|
let opts = options;
|
|
let canvasEl = canvas;
|
|
if (typeof opts === "undefined" && (!canvas || !canvas.getContext)) {
|
|
opts = canvas;
|
|
canvas = void 0;
|
|
}
|
|
if (!canvas) {
|
|
canvasEl = getCanvasElement();
|
|
}
|
|
opts = Utils.getOptions(opts);
|
|
const size = Utils.getImageWidth(qrData.modules.size, opts);
|
|
const ctx = canvasEl.getContext("2d");
|
|
const image = ctx.createImageData(size, size);
|
|
Utils.qrToImageData(image.data, qrData, opts);
|
|
clearCanvas(ctx, canvasEl, size);
|
|
ctx.putImageData(image, 0, 0);
|
|
return canvasEl;
|
|
};
|
|
exports.renderToDataURL = function renderToDataURL(qrData, canvas, options) {
|
|
let opts = options;
|
|
if (typeof opts === "undefined" && (!canvas || !canvas.getContext)) {
|
|
opts = canvas;
|
|
canvas = void 0;
|
|
}
|
|
if (!opts) opts = {};
|
|
const canvasEl = exports.render(qrData, canvas, opts);
|
|
const type = opts.type || "image/png";
|
|
const rendererOpts = opts.rendererOpts || {};
|
|
return canvasEl.toDataURL(type, rendererOpts.quality);
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/renderer/svg-tag.js
|
|
var require_svg_tag = __commonJS({
|
|
"node_modules/qrcode/lib/renderer/svg-tag.js"(exports) {
|
|
var Utils = require_utils2();
|
|
function getColorAttrib(color, attrib) {
|
|
const alpha = color.a / 255;
|
|
const str = attrib + '="' + color.hex + '"';
|
|
return alpha < 1 ? str + " " + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"' : str;
|
|
}
|
|
function svgCmd(cmd, x, y) {
|
|
let str = cmd + x;
|
|
if (typeof y !== "undefined") str += " " + y;
|
|
return str;
|
|
}
|
|
function qrToPath(data, size, margin) {
|
|
let path = "";
|
|
let moveBy = 0;
|
|
let newRow = false;
|
|
let lineLength = 0;
|
|
for (let i = 0; i < data.length; i++) {
|
|
const col = Math.floor(i % size);
|
|
const row = Math.floor(i / size);
|
|
if (!col && !newRow) newRow = true;
|
|
if (data[i]) {
|
|
lineLength++;
|
|
if (!(i > 0 && col > 0 && data[i - 1])) {
|
|
path += newRow ? svgCmd("M", col + margin, 0.5 + row + margin) : svgCmd("m", moveBy, 0);
|
|
moveBy = 0;
|
|
newRow = false;
|
|
}
|
|
if (!(col + 1 < size && data[i + 1])) {
|
|
path += svgCmd("h", lineLength);
|
|
lineLength = 0;
|
|
}
|
|
} else {
|
|
moveBy++;
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
exports.render = function render(qrData, options, cb) {
|
|
const opts = Utils.getOptions(options);
|
|
const size = qrData.modules.size;
|
|
const data = qrData.modules.data;
|
|
const qrcodesize = size + opts.margin * 2;
|
|
const bg = !opts.color.light.a ? "" : "<path " + getColorAttrib(opts.color.light, "fill") + ' d="M0 0h' + qrcodesize + "v" + qrcodesize + 'H0z"/>';
|
|
const path = "<path " + getColorAttrib(opts.color.dark, "stroke") + ' d="' + qrToPath(data, size, opts.margin) + '"/>';
|
|
const viewBox = 'viewBox="0 0 ' + qrcodesize + " " + qrcodesize + '"';
|
|
const width = !opts.width ? "" : 'width="' + opts.width + '" height="' + opts.width + '" ';
|
|
const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg + path + "</svg>\n";
|
|
if (typeof cb === "function") {
|
|
cb(null, svgTag);
|
|
}
|
|
return svgTag;
|
|
};
|
|
}
|
|
});
|
|
|
|
// node_modules/qrcode/lib/browser.js
|
|
var require_browser = __commonJS({
|
|
"node_modules/qrcode/lib/browser.js"(exports) {
|
|
var canPromise = require_can_promise();
|
|
var QRCode2 = require_qrcode();
|
|
var CanvasRenderer = require_canvas();
|
|
var SvgRenderer = require_svg_tag();
|
|
function renderCanvas(renderFunc, canvas, text, opts, cb) {
|
|
const args = [].slice.call(arguments, 1);
|
|
const argsNum = args.length;
|
|
const isLastArgCb = typeof args[argsNum - 1] === "function";
|
|
if (!isLastArgCb && !canPromise()) {
|
|
throw new Error("Callback required as last argument");
|
|
}
|
|
if (isLastArgCb) {
|
|
if (argsNum < 2) {
|
|
throw new Error("Too few arguments provided");
|
|
}
|
|
if (argsNum === 2) {
|
|
cb = text;
|
|
text = canvas;
|
|
canvas = opts = void 0;
|
|
} else if (argsNum === 3) {
|
|
if (canvas.getContext && typeof cb === "undefined") {
|
|
cb = opts;
|
|
opts = void 0;
|
|
} else {
|
|
cb = opts;
|
|
opts = text;
|
|
text = canvas;
|
|
canvas = void 0;
|
|
}
|
|
}
|
|
} else {
|
|
if (argsNum < 1) {
|
|
throw new Error("Too few arguments provided");
|
|
}
|
|
if (argsNum === 1) {
|
|
text = canvas;
|
|
canvas = opts = void 0;
|
|
} else if (argsNum === 2 && !canvas.getContext) {
|
|
opts = text;
|
|
text = canvas;
|
|
canvas = void 0;
|
|
}
|
|
return new Promise(function(resolve, reject) {
|
|
try {
|
|
const data = QRCode2.create(text, opts);
|
|
resolve(renderFunc(data, canvas, opts));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
try {
|
|
const data = QRCode2.create(text, opts);
|
|
cb(null, renderFunc(data, canvas, opts));
|
|
} catch (e) {
|
|
cb(e);
|
|
}
|
|
}
|
|
exports.create = QRCode2.create;
|
|
exports.toCanvas = renderCanvas.bind(null, CanvasRenderer.render);
|
|
exports.toDataURL = renderCanvas.bind(null, CanvasRenderer.renderToDataURL);
|
|
exports.toString = renderCanvas.bind(null, function(data, _, opts) {
|
|
return SvgRenderer.render(data, opts);
|
|
});
|
|
}
|
|
});
|
|
|
|
// app/web/admin/shared/constants.js
|
|
var LS_TOKEN = "admin_access_token";
|
|
var ADMIN_AUTH_REDIRECT_REASON_KEY = "admin_auth_redirect_reason";
|
|
var PAGE_SIZE = 50;
|
|
var DEFAULT_FORM_FIELD_TYPES = ["string", "text", "number", "boolean", "date"];
|
|
var ALL_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "~"];
|
|
var OPERATOR_LABELS = {
|
|
"=": "=",
|
|
"!=": "!=",
|
|
">": ">",
|
|
"<": "<",
|
|
">=": ">=",
|
|
"<=": "<=",
|
|
"~": "~"
|
|
};
|
|
var ROLE_LABELS = {
|
|
ADMIN: "\u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440",
|
|
LAWYER: "\u042E\u0440\u0438\u0441\u0442",
|
|
CURATOR: "\u041A\u0443\u0440\u0430\u0442\u043E\u0440"
|
|
};
|
|
var STATUS_LABELS = {
|
|
NEW: "\u041D\u043E\u0432\u0430\u044F",
|
|
IN_PROGRESS: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435",
|
|
WAITING_CLIENT: "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043A\u043B\u0438\u0435\u043D\u0442\u0430",
|
|
WAITING_COURT: "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u0441\u0443\u0434\u0430",
|
|
RESOLVED: "\u0420\u0435\u0448\u0435\u043D\u0430",
|
|
CLOSED: "\u0417\u0430\u043A\u0440\u044B\u0442\u0430",
|
|
REJECTED: "\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0430"
|
|
};
|
|
var INVOICE_STATUS_LABELS = {
|
|
WAITING_PAYMENT: "\u041E\u0436\u0438\u0434\u0430\u0435\u0442 \u043E\u043F\u043B\u0430\u0442\u0443",
|
|
PAID: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D",
|
|
CANCELED: "\u041E\u0442\u043C\u0435\u043D\u0435\u043D"
|
|
};
|
|
var STATUS_KIND_LABELS = {
|
|
DEFAULT: "\u041E\u0431\u044B\u0447\u043D\u044B\u0439",
|
|
INVOICE: "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u0441\u0447\u0435\u0442\u0430",
|
|
PAID: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E"
|
|
};
|
|
var REQUEST_UPDATE_EVENT_LABELS = {
|
|
MESSAGE: "\u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435",
|
|
ATTACHMENT: "\u0444\u0430\u0439\u043B",
|
|
REQUEST_DATA: "\u0434\u0430\u043D\u043D\u044B\u0435",
|
|
ASSIGNMENT: "\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
|
|
REASSIGNMENT: "\u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
|
|
STATUS: "\u0441\u0442\u0430\u0442\u0443\u0441"
|
|
};
|
|
var SERVICE_REQUEST_TYPE_LABELS = {
|
|
CURATOR_CONTACT: "\u0417\u0430\u043F\u0440\u043E\u0441 \u043A \u043A\u0443\u0440\u0430\u0442\u043E\u0440\u0443",
|
|
LAWYER_CHANGE_REQUEST: "\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430"
|
|
};
|
|
var SERVICE_REQUEST_STATUS_LABELS = {
|
|
NEW: "\u041D\u043E\u0432\u044B\u0439",
|
|
IN_PROGRESS: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435",
|
|
RESOLVED: "\u0420\u0435\u0448\u0435\u043D",
|
|
REJECTED: "\u041E\u0442\u043A\u043B\u043E\u043D\u0435\u043D"
|
|
};
|
|
var KANBAN_GROUPS = [
|
|
{ key: "NEW", label: "\u041D\u043E\u0432\u044B\u0435" },
|
|
{ key: "IN_PROGRESS", label: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435" },
|
|
{ key: "WAITING", label: "\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435" },
|
|
{ key: "DONE", label: "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u044B" }
|
|
];
|
|
var TABLE_SERVER_CONFIG = {
|
|
requests: {
|
|
table: "requests",
|
|
// Requests use a specialized endpoint because it supports virtual/server-side filters
|
|
// (e.g. deadline alerts and unread notifications) that are not plain table columns.
|
|
endpoint: "/api/admin/requests/query",
|
|
sort: [{ field: "created_at", dir: "desc" }]
|
|
},
|
|
serviceRequests: {
|
|
table: "request_service_requests",
|
|
endpoint: "/api/admin/crud/request_service_requests/query",
|
|
sort: [{ field: "created_at", dir: "desc" }]
|
|
},
|
|
invoices: {
|
|
table: "invoices",
|
|
endpoint: "/api/admin/invoices/query",
|
|
sort: [{ field: "issued_at", dir: "desc" }]
|
|
},
|
|
quotes: {
|
|
table: "quotes",
|
|
endpoint: "/api/admin/crud/quotes/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
topics: {
|
|
table: "topics",
|
|
endpoint: "/api/admin/crud/topics/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
statuses: {
|
|
table: "statuses",
|
|
endpoint: "/api/admin/crud/statuses/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
formFields: {
|
|
table: "form_fields",
|
|
endpoint: "/api/admin/crud/form_fields/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
topicRequiredFields: {
|
|
table: "topic_required_fields",
|
|
endpoint: "/api/admin/crud/topic_required_fields/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
topicDataTemplates: {
|
|
table: "topic_data_templates",
|
|
endpoint: "/api/admin/crud/topic_data_templates/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
statusTransitions: {
|
|
table: "topic_status_transitions",
|
|
endpoint: "/api/admin/crud/topic_status_transitions/query",
|
|
sort: [{ field: "sort_order", dir: "asc" }]
|
|
},
|
|
users: {
|
|
table: "admin_users",
|
|
endpoint: "/api/admin/crud/admin_users/query",
|
|
sort: [{ field: "created_at", dir: "desc" }]
|
|
},
|
|
userTopics: {
|
|
table: "admin_user_topics",
|
|
endpoint: "/api/admin/crud/admin_user_topics/query",
|
|
sort: [{ field: "created_at", dir: "desc" }]
|
|
}
|
|
};
|
|
var TABLE_MUTATION_CONFIG = Object.fromEntries(
|
|
Object.entries(TABLE_SERVER_CONFIG).map(([tableKey, config]) => [
|
|
tableKey,
|
|
{
|
|
create: "/api/admin/crud/" + config.table,
|
|
update: (id) => "/api/admin/crud/" + config.table + "/" + id,
|
|
delete: (id) => "/api/admin/crud/" + config.table + "/" + id
|
|
}
|
|
])
|
|
);
|
|
TABLE_MUTATION_CONFIG.invoices = {
|
|
create: "/api/admin/invoices",
|
|
update: (id) => "/api/admin/invoices/" + id,
|
|
delete: (id) => "/api/admin/invoices/" + id
|
|
};
|
|
var TABLE_KEY_ALIASES = {
|
|
request_service_requests: "serviceRequests",
|
|
form_fields: "formFields",
|
|
status_groups: "statusGroups",
|
|
topic_required_fields: "topicRequiredFields",
|
|
topic_data_templates: "topicDataTemplates",
|
|
topic_status_transitions: "statusTransitions",
|
|
admin_users: "users",
|
|
admin_user_topics: "userTopics"
|
|
};
|
|
var TABLE_UNALIASES = Object.fromEntries(Object.entries(TABLE_KEY_ALIASES).map(([table, alias]) => [alias, table]));
|
|
var KNOWN_CONFIG_TABLE_KEYS = /* @__PURE__ */ new Set([
|
|
"quotes",
|
|
"topics",
|
|
"statuses",
|
|
"formFields",
|
|
"topicRequiredFields",
|
|
"topicDataTemplates",
|
|
"statusTransitions",
|
|
"users",
|
|
"userTopics"
|
|
]);
|
|
|
|
// app/web/admin/shared/state.js
|
|
function createTableState() {
|
|
return {
|
|
filters: [],
|
|
sort: null,
|
|
offset: 0,
|
|
total: 0,
|
|
showAll: false,
|
|
rows: []
|
|
};
|
|
}
|
|
function createRequestModalState() {
|
|
return {
|
|
loading: false,
|
|
requestId: null,
|
|
trackNumber: "",
|
|
requestData: null,
|
|
financeSummary: null,
|
|
invoices: [],
|
|
statusRouteNodes: [],
|
|
statusHistory: [],
|
|
availableStatuses: [],
|
|
currentImportantDateAt: "",
|
|
pendingStatusChangePreset: null,
|
|
messages: [],
|
|
messagesHasMore: false,
|
|
messagesLoadingMore: false,
|
|
messagesLoadedCount: 0,
|
|
messagesTotal: 0,
|
|
attachments: [],
|
|
messageDraft: "",
|
|
selectedFiles: [],
|
|
fileUploading: false
|
|
};
|
|
}
|
|
|
|
// app/web/admin/shared/DropdownField.jsx
|
|
var { useEffect, useMemo, useRef, useState } = React;
|
|
function DropdownField({
|
|
id,
|
|
value,
|
|
onChange,
|
|
options,
|
|
placeholder = "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
|
|
disabled = false,
|
|
className = "",
|
|
ariaLabel = ""
|
|
}) {
|
|
const rootRef = useRef(null);
|
|
const [open, setOpen] = useState(false);
|
|
const normalizedOptions = useMemo(
|
|
() => (Array.isArray(options) ? options : []).map((option) => ({
|
|
value: String(option?.value ?? ""),
|
|
label: String(option?.label ?? option?.value ?? ""),
|
|
disabled: Boolean(option?.disabled)
|
|
})),
|
|
[options]
|
|
);
|
|
const currentValue = String(value ?? "");
|
|
const currentOption = normalizedOptions.find((option) => option.value === currentValue) || null;
|
|
useEffect(() => {
|
|
if (!open) return void 0;
|
|
const handlePointerDown = (event) => {
|
|
if (rootRef.current && !rootRef.current.contains(event.target)) setOpen(false);
|
|
};
|
|
const handleKeyDown = (event) => {
|
|
if (event.key === "Escape") setOpen(false);
|
|
};
|
|
document.addEventListener("mousedown", handlePointerDown);
|
|
document.addEventListener("keydown", handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener("mousedown", handlePointerDown);
|
|
document.removeEventListener("keydown", handleKeyDown);
|
|
};
|
|
}, [open]);
|
|
useEffect(() => {
|
|
if (disabled && open) setOpen(false);
|
|
}, [disabled, open]);
|
|
return /* @__PURE__ */ React.createElement("div", { className: "dropdown-field" + (open ? " open" : "") + (disabled ? " disabled" : "") + (className ? " " + className : ""), ref: rootRef }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
id,
|
|
type: "button",
|
|
className: "dropdown-field-trigger",
|
|
"aria-label": ariaLabel || placeholder,
|
|
"aria-haspopup": "listbox",
|
|
"aria-expanded": open ? "true" : "false",
|
|
disabled,
|
|
onClick: () => setOpen((prev) => !prev)
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "dropdown-field-label" + (currentOption ? "" : " placeholder") }, currentOption ? currentOption.label : placeholder),
|
|
/* @__PURE__ */ React.createElement("svg", { className: "dropdown-field-caret", viewBox: "0 0 14 14", width: "14", height: "14", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M3.2 4.8a.75.75 0 0 1 1.06 0L7 7.54 9.74 4.8a.75.75 0 1 1 1.06 1.06L7.53 9.13a.75.75 0 0 1-1.06 0L3.2 5.86a.75.75 0 0 1 0-1.06Z", fill: "currentColor" }))
|
|
), open ? /* @__PURE__ */ React.createElement("div", { className: "dropdown-field-menu", role: "listbox", "aria-labelledby": id }, normalizedOptions.length ? normalizedOptions.map((option) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
key: option.value,
|
|
type: "button",
|
|
role: "option",
|
|
className: "dropdown-field-option" + (option.value === currentValue ? " selected" : ""),
|
|
"aria-selected": option.value === currentValue ? "true" : "false",
|
|
disabled: option.disabled,
|
|
onClick: () => {
|
|
if (option.disabled) return;
|
|
setOpen(false);
|
|
if (typeof onChange === "function") onChange(option.value);
|
|
}
|
|
},
|
|
option.label
|
|
)) : /* @__PURE__ */ React.createElement("div", { className: "dropdown-field-empty" }, "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439")) : null);
|
|
}
|
|
|
|
// app/web/admin/shared/icons.jsx
|
|
function RefreshIcon() {
|
|
return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }), /* @__PURE__ */ React.createElement("polyline", { points: "21 3 21 9 15 9" }));
|
|
}
|
|
function FilterIcon() {
|
|
return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M3 5h18l-7 8v5l-4 2v-7z" }));
|
|
}
|
|
function AddIcon() {
|
|
return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M12 5v14" }), /* @__PURE__ */ React.createElement("path", { d: "M5 12h14" }));
|
|
}
|
|
function PrevIcon() {
|
|
return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M15 18l-6-6 6-6" }));
|
|
}
|
|
function NextIcon() {
|
|
return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M9 18l6-6-6-6" }));
|
|
}
|
|
function DownloadIcon() {
|
|
return /* @__PURE__ */ React.createElement("svg", { className: "ui-glyph", viewBox: "0 0 24 24", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement("path", { d: "M12 4v11" }), /* @__PURE__ */ React.createElement("path", { d: "M8 11l4 4 4-4" }), /* @__PURE__ */ React.createElement("path", { d: "M5 20h14" }));
|
|
}
|
|
|
|
// app/web/admin/shared/utils.js
|
|
function resolveAdminRoute(search) {
|
|
const params = new URLSearchParams(String(search || ""));
|
|
const section = String(params.get("section") || "").trim();
|
|
const view = String(params.get("view") || "").trim();
|
|
const requestId = String(params.get("requestId") || "").trim();
|
|
return { section, view, requestId };
|
|
}
|
|
function humanizeKey(value) {
|
|
const text = String(value || "").replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
if (!text) return "-";
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
}
|
|
function metaKindToFilterType(kind) {
|
|
if (kind === "boolean") return "boolean";
|
|
if (kind === "number") return "number";
|
|
if (kind === "date" || kind === "datetime") return "date";
|
|
return "text";
|
|
}
|
|
function metaKindToRecordType(kind) {
|
|
if (kind === "boolean") return "boolean";
|
|
if (kind === "number") return "number";
|
|
if (kind === "json") return "json";
|
|
return "text";
|
|
}
|
|
function decodeJwtPayload(token) {
|
|
try {
|
|
const payload = token.split(".")[1] || "";
|
|
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
const json = decodeURIComponent(
|
|
atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
|
|
);
|
|
return JSON.parse(json);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
function sortByName(items) {
|
|
return [...items].sort((a, b) => String(a.name || a.code || "").localeCompare(String(b.name || b.code || ""), "ru"));
|
|
}
|
|
function roleLabel(role) {
|
|
return ROLE_LABELS[role] || role || "-";
|
|
}
|
|
function statusLabel(code) {
|
|
return STATUS_LABELS[code] || code || "-";
|
|
}
|
|
function invoiceStatusLabel(code) {
|
|
return INVOICE_STATUS_LABELS[code] || code || "-";
|
|
}
|
|
function statusKindLabel(code) {
|
|
return STATUS_KIND_LABELS[code] || code || "-";
|
|
}
|
|
function fallbackStatusGroup(statusCode) {
|
|
const code = String(statusCode || "").toUpperCase();
|
|
if (!code) return "NEW";
|
|
if (code.startsWith("NEW")) return "NEW";
|
|
if (code.includes("WAIT") || code.includes("PEND") || code.includes("HOLD")) return "WAITING";
|
|
if (code.includes("CLOSE") || code.includes("RESOLV") || code.includes("REJECT") || code.includes("DONE") || code.includes("PAID")) return "DONE";
|
|
return "IN_PROGRESS";
|
|
}
|
|
function boolLabel(value) {
|
|
return value ? "\u0414\u0430" : "\u041D\u0435\u0442";
|
|
}
|
|
function boolFilterLabel(value) {
|
|
return value ? "True" : "False";
|
|
}
|
|
function fmtDate(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const year = String(date.getFullYear()).slice(-2);
|
|
const hours = String(date.getHours()).padStart(2, "0");
|
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
|
}
|
|
function fmtDateOnly(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const year = String(date.getFullYear()).slice(-2);
|
|
return `${day}.${month}.${year}`;
|
|
}
|
|
function fmtTimeOnly(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
function fmtKanbanDate(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const year = String(date.getFullYear()).slice(-2);
|
|
const hours = String(date.getHours()).padStart(2, "0");
|
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
|
}
|
|
function fmtShortDateTime(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const year = String(date.getFullYear()).slice(-2);
|
|
const hours = String(date.getHours()).padStart(2, "0");
|
|
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
return `${day}.${month}.${year} ${hours}:${minutes}`;
|
|
}
|
|
function resolveDeadlineTone(value) {
|
|
if (!value) return "ok";
|
|
const time = new Date(value).getTime();
|
|
if (!Number.isFinite(time)) return "ok";
|
|
const delta = time - Date.now();
|
|
const fourDaysMs = 4 * 24 * 60 * 60 * 1e3;
|
|
const oneDayMs = 24 * 60 * 60 * 1e3;
|
|
if (delta > fourDaysMs) return "ok";
|
|
if (delta > oneDayMs) return "warn";
|
|
return "danger";
|
|
}
|
|
function fmtAmount(value) {
|
|
if (value == null || value === "") return "-";
|
|
const number = Number(value);
|
|
if (Number.isNaN(number)) return String(value);
|
|
return number.toLocaleString("ru-RU");
|
|
}
|
|
function fmtBytes(value) {
|
|
const size = Number(value || 0);
|
|
if (!Number.isFinite(size) || size <= 0) return "0 \u0411";
|
|
const units = ["\u0411", "\u041A\u0411", "\u041C\u0411", "\u0413\u0411"];
|
|
let normalized = size;
|
|
let index = 0;
|
|
while (normalized >= 1024 && index < units.length - 1) {
|
|
normalized /= 1024;
|
|
index += 1;
|
|
}
|
|
return normalized.toLocaleString("ru-RU", { maximumFractionDigits: index === 0 ? 0 : 1 }) + " " + units[index];
|
|
}
|
|
function normalizeStringList(value) {
|
|
if (!Array.isArray(value)) return [];
|
|
const out = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
value.forEach((item) => {
|
|
const text = String(item || "").trim();
|
|
if (!text) return;
|
|
const key = text.toLowerCase();
|
|
if (seen.has(key)) return;
|
|
seen.add(key);
|
|
out.push(text);
|
|
});
|
|
return out;
|
|
}
|
|
function listPreview(value, emptyLabel) {
|
|
const items = normalizeStringList(value);
|
|
return items.length ? items.join(", ") : emptyLabel;
|
|
}
|
|
function normalizeReferenceMeta(raw) {
|
|
if (!raw || typeof raw !== "object") return null;
|
|
const table = String(raw.table || "").trim();
|
|
const valueField = String(raw.value_field || "id").trim() || "id";
|
|
const labelField = String(raw.label_field || valueField).trim() || valueField;
|
|
if (!table) return null;
|
|
return { table, value_field: valueField, label_field: labelField };
|
|
}
|
|
function userInitials(name, email) {
|
|
const source = String(name || "").trim();
|
|
if (source) {
|
|
const parts = source.split(/\s+/).filter(Boolean);
|
|
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
return source.slice(0, 2).toUpperCase();
|
|
}
|
|
const mail = String(email || "").trim();
|
|
return (mail.slice(0, 2) || "U").toUpperCase();
|
|
}
|
|
function avatarColor(seed) {
|
|
const palette = ["#6f8fa9", "#568f7d", "#a07a5c", "#7d6ea9", "#8f6f8f", "#7f8c5a"];
|
|
const text = String(seed || "");
|
|
let hash = 0;
|
|
for (let i = 0; i < text.length; i += 1) hash = hash * 31 + text.charCodeAt(i) >>> 0;
|
|
return palette[hash % palette.length];
|
|
}
|
|
function resolveAvatarSrc(avatarUrl, accessToken, size) {
|
|
const raw = String(avatarUrl || "").trim();
|
|
if (!raw) return "";
|
|
if (raw.startsWith("s3://")) {
|
|
const key = raw.slice("s3://".length);
|
|
if (!key || !accessToken) return "";
|
|
const useThumb = Number(size || 0) > 0 && Number(size || 0) <= 160;
|
|
return "/api/admin/uploads/object/" + encodeURIComponent(key) + "?token=" + encodeURIComponent(accessToken) + (useThumb ? "&variant=thumb" : "");
|
|
}
|
|
return raw;
|
|
}
|
|
function resolveAdminObjectSrc(s3Key, accessToken) {
|
|
const key = String(s3Key || "").trim();
|
|
if (!key || !accessToken) return "";
|
|
return "/api/admin/uploads/object/" + encodeURIComponent(key) + "?token=" + encodeURIComponent(accessToken);
|
|
}
|
|
function detectAttachmentPreviewKind(fileName, mimeType) {
|
|
const name = String(fileName || "").toLowerCase();
|
|
const mime = String(mimeType || "").toLowerCase();
|
|
if (/\.(txt|md|csv|json|log|xml|ya?ml|ini|cfg)$/i.test(name)) return "text";
|
|
if (mime.startsWith("text/") || mime === "application/json" || mime === "application/xml" || mime === "text/xml") {
|
|
return "text";
|
|
}
|
|
if (mime.startsWith("image/") || /\.(png|jpe?g|gif|webp|bmp|svg)$/.test(name)) return "image";
|
|
if (mime.startsWith("video/") || /\.(mp4|webm|ogg|mov|m4v)$/.test(name)) return "video";
|
|
if (mime === "application/pdf" || /\.pdf$/.test(name)) return "pdf";
|
|
return "none";
|
|
}
|
|
function buildUniversalQuery(filters, sort, limit, offset) {
|
|
return {
|
|
filters: filters || [],
|
|
sort: sort || [],
|
|
page: { limit: limit ?? 50, offset: offset ?? 0 }
|
|
};
|
|
}
|
|
function canAccessSection(role, section) {
|
|
const roleCode = String(role || "").toUpperCase();
|
|
const allowed = /* @__PURE__ */ new Set([
|
|
"dashboard",
|
|
"kanban",
|
|
"requests",
|
|
"serviceRequests",
|
|
"requestWorkspace",
|
|
"invoices",
|
|
"meta",
|
|
"quotes",
|
|
"config",
|
|
"availableTables"
|
|
]);
|
|
if (!allowed.has(section)) return false;
|
|
if (section === "requests") return roleCode === "ADMIN" || roleCode === "LAWYER";
|
|
if (section === "serviceRequests") return roleCode === "ADMIN" || roleCode === "CURATOR";
|
|
if (section === "quotes" || section === "config" || section === "availableTables") return roleCode === "ADMIN";
|
|
return true;
|
|
}
|
|
function translateApiError(message) {
|
|
const direct = {
|
|
"Missing auth token": "\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
|
|
"Missing bearer token": "\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438",
|
|
"Invalid token": "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 \u0442\u043E\u043A\u0435\u043D",
|
|
Forbidden: "\u041D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043E\u0447\u043D\u043E \u043F\u0440\u0430\u0432",
|
|
"Invalid credentials": "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0439 \u043B\u043E\u0433\u0438\u043D \u0438\u043B\u0438 \u043F\u0430\u0440\u043E\u043B\u044C",
|
|
"Request not found": "\u0417\u0430\u044F\u0432\u043A\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430",
|
|
"Quote not found": "\u0426\u0438\u0442\u0430\u0442\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430",
|
|
not_found: "\u0417\u0430\u043F\u0438\u0441\u044C \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430"
|
|
};
|
|
if (direct[message]) return direct[message];
|
|
if (String(message).startsWith("HTTP ")) return "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 (" + message + ")";
|
|
return message;
|
|
}
|
|
function getOperatorsForType(type) {
|
|
if (type === "number" || type === "date" || type === "datetime") return ["=", "!=", ">", "<", ">=", "<="];
|
|
if (type === "boolean" || type === "reference" || type === "enum") return ["=", "!="];
|
|
return [...ALL_OPERATORS];
|
|
}
|
|
function localizeMeta(data) {
|
|
const fieldTypeMap = {
|
|
string: "\u0441\u0442\u0440\u043E\u043A\u0430",
|
|
text: "\u0442\u0435\u043A\u0441\u0442",
|
|
boolean: "\u0431\u0443\u043B\u0435\u0432\u043E",
|
|
number: "\u0447\u0438\u0441\u043B\u043E",
|
|
date: "\u0434\u0430\u0442\u0430"
|
|
};
|
|
return {
|
|
\u0421\u0443\u0449\u043D\u043E\u0441\u0442\u044C: data.entity,
|
|
\u041F\u043E\u043B\u044F: (data.fields || []).map((field) => ({
|
|
"\u041A\u043E\u0434 \u043F\u043E\u043B\u044F": field.field_name,
|
|
\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435: field.label,
|
|
\u0422\u0438\u043F: fieldTypeMap[field.type] || field.type,
|
|
\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435: boolLabel(field.required),
|
|
"\u0422\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u0435\u043D\u0438\u0435": boolLabel(field.read_only),
|
|
"\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0435\u043C\u044B\u0435 \u0440\u043E\u043B\u0438": (field.editable_roles || []).map(roleLabel)
|
|
}))
|
|
};
|
|
}
|
|
|
|
// app/web/admin/features/kanban/KanbanBoard.jsx
|
|
function KanbanBoard({
|
|
loading,
|
|
columns,
|
|
rows,
|
|
role,
|
|
actorId,
|
|
filters,
|
|
onRefresh,
|
|
onOpenFilter,
|
|
onRemoveFilter,
|
|
onEditFilter,
|
|
getFilterChipLabel,
|
|
onOpenSort,
|
|
sortActive,
|
|
onOpenRequest,
|
|
onClaimRequest,
|
|
onMoveRequest,
|
|
status,
|
|
FilterToolbarComponent,
|
|
StatusLineComponent
|
|
}) {
|
|
const { useMemo: useMemo2, useState: useState4 } = React;
|
|
const [draggingId, setDraggingId] = useState4("");
|
|
const [dragOverGroup, setDragOverGroup] = useState4("");
|
|
const safeColumns = Array.isArray(columns) && columns.length ? columns : KANBAN_GROUPS;
|
|
const grouped = useMemo2(() => {
|
|
const map = {};
|
|
safeColumns.forEach((column) => {
|
|
map[String(column.key)] = [];
|
|
});
|
|
(rows || []).forEach((row) => {
|
|
const group = String(row?.status_group || fallbackStatusGroup(row?.status_code));
|
|
if (!map[group]) map[group] = [];
|
|
map[group].push(row);
|
|
});
|
|
return map;
|
|
}, [rows, safeColumns]);
|
|
const rowMap = useMemo2(() => {
|
|
const map = /* @__PURE__ */ new Map();
|
|
(rows || []).forEach((row) => {
|
|
if (!row?.id) return;
|
|
map.set(String(row.id), row);
|
|
});
|
|
return map;
|
|
}, [rows]);
|
|
const onDropToGroup = (event, groupKey) => {
|
|
event.preventDefault();
|
|
const requestId = String(event.dataTransfer.getData("text/plain") || draggingId || "");
|
|
setDragOverGroup("");
|
|
setDraggingId("");
|
|
if (!requestId) return;
|
|
const row = rowMap.get(requestId);
|
|
if (!row) return;
|
|
onMoveRequest(row, String(groupKey || ""));
|
|
};
|
|
const FilterToolbar = FilterToolbarComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
return /* @__PURE__ */ React.createElement("div", { className: "kanban-wrap" }, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u041A\u0430\u043D\u0431\u0430\u043D \u0437\u0430\u044F\u0432\u043E\u043A"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0413\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E \u0433\u0440\u0443\u043F\u043F\u0430\u043C \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u0430\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u044F \u043A\u0430\u0440\u0442\u043E\u0447\u0435\u043A.")), /* @__PURE__ */ React.createElement("div", { className: "section-head-actions" }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary" + (sortActive ? " active-success" : ""), type: "button", onClick: onOpenSort }, "\u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onRefresh, disabled: loading, title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(RefreshIcon, null)), /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)))), FilterToolbar ? /* @__PURE__ */ React.createElement(
|
|
FilterToolbar,
|
|
{
|
|
filters: filters || [],
|
|
onOpen: onOpenFilter,
|
|
onRemove: onRemoveFilter,
|
|
onEdit: onEditFilter,
|
|
hideAction: true,
|
|
getChipLabel: getFilterChipLabel
|
|
}
|
|
) : null, /* @__PURE__ */ React.createElement("div", { className: "kanban-board", id: "kanban-board" }, safeColumns.map((column) => {
|
|
const key = String(column.key || "");
|
|
const cards = grouped[key] || [];
|
|
const isOver = dragOverGroup === key;
|
|
return /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
key,
|
|
className: "kanban-column" + (isOver ? " drag-over" : ""),
|
|
onDragOver: (event) => {
|
|
event.preventDefault();
|
|
setDragOverGroup(key);
|
|
},
|
|
onDragLeave: (event) => {
|
|
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
setDragOverGroup((prev) => prev === key ? "" : prev);
|
|
},
|
|
onDrop: (event) => onDropToGroup(event, key)
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "kanban-column-head" }, /* @__PURE__ */ React.createElement("b", null, column.label || key), /* @__PURE__ */ React.createElement("span", null, Number(column.total ?? cards.length))),
|
|
/* @__PURE__ */ React.createElement("div", { className: "kanban-column-body" }, cards.length ? cards.map((row) => {
|
|
const requestId = String(row.id || "");
|
|
const isUnassigned = !String(row.assigned_lawyer_id || "").trim();
|
|
const canClaim = role === "LAWYER" && isUnassigned;
|
|
const canMove = role === "ADMIN" || !isUnassigned && String(row.assigned_lawyer_id || "").trim() === String(actorId || "").trim();
|
|
const transitionOptions = Array.isArray(row.available_transitions) ? row.available_transitions : [];
|
|
const deadline = row.sla_deadline_at || row.case_deadline_at || "";
|
|
const deadlineTone = resolveDeadlineTone(deadline);
|
|
const unreadTypes = /* @__PURE__ */ new Set();
|
|
if (role === "LAWYER") {
|
|
if (row.lawyer_has_unread_updates && row.lawyer_unread_event_type) unreadTypes.add(String(row.lawyer_unread_event_type).toUpperCase());
|
|
} else {
|
|
if (row.client_has_unread_updates && row.client_unread_event_type) unreadTypes.add(String(row.client_unread_event_type).toUpperCase());
|
|
if (row.lawyer_has_unread_updates && row.lawyer_unread_event_type) unreadTypes.add(String(row.lawyer_unread_event_type).toUpperCase());
|
|
}
|
|
const hasUnreadMessage = unreadTypes.has("MESSAGE");
|
|
const hasUnreadAttachment = unreadTypes.has("ATTACHMENT");
|
|
return /* @__PURE__ */ React.createElement(
|
|
"article",
|
|
{
|
|
key: requestId,
|
|
className: "kanban-card" + (canMove ? " draggable" : ""),
|
|
draggable: canMove,
|
|
role: "button",
|
|
tabIndex: 0,
|
|
onClick: (event) => onOpenRequest(requestId, event),
|
|
onKeyDown: (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
onOpenRequest(requestId, event);
|
|
}
|
|
},
|
|
onDragStart: (event) => {
|
|
if (!canMove) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
setDraggingId(requestId);
|
|
event.dataTransfer.effectAllowed = "move";
|
|
event.dataTransfer.setData("text/plain", requestId);
|
|
},
|
|
onDragEnd: () => {
|
|
setDraggingId("");
|
|
setDragOverGroup("");
|
|
}
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "kanban-card-head" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "request-track-link",
|
|
onClick: (event) => {
|
|
event.stopPropagation();
|
|
onOpenRequest(requestId, event);
|
|
},
|
|
title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443"
|
|
},
|
|
/* @__PURE__ */ React.createElement("code", null, row.track_number || "-")
|
|
), /* @__PURE__ */ React.createElement("span", { className: "kanban-status-badge group-" + String(row.status_group || "").toLowerCase() }, row.status_name || statusLabel(row.status_code))),
|
|
/* @__PURE__ */ React.createElement("p", { className: "kanban-card-desc" }, String(row.description || "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")),
|
|
/* @__PURE__ */ React.createElement("div", { className: "kanban-card-meta" }, /* @__PURE__ */ React.createElement("span", null, row.client_name || "-"), /* @__PURE__ */ React.createElement("span", null, fmtKanbanDate(row.created_at))),
|
|
/* @__PURE__ */ React.createElement("div", { className: "kanban-card-meta" }, /* @__PURE__ */ React.createElement("span", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("span", null, row.assigned_lawyer_name || (isUnassigned ? "\u041D\u0435 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E" : row.assigned_lawyer_id || "-"))),
|
|
/* @__PURE__ */ React.createElement("div", { className: "kanban-card-meta" }, /* @__PURE__ */ React.createElement("div", { className: "kanban-update-icons" }, /* @__PURE__ */ React.createElement("span", { className: "kanban-update-icon" + (hasUnreadMessage ? " is-unread" : ""), title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F" }, "\u{1F4AC}"), /* @__PURE__ */ React.createElement("span", { className: "kanban-update-icon" + (hasUnreadAttachment ? " is-unread" : ""), title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B" }, "\u{1F4CE}")), /* @__PURE__ */ React.createElement("span", { className: "kanban-deadline-chip tone-" + deadlineTone }, deadline ? fmtKanbanDate(deadline) : "\u2014")),
|
|
/* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "kanban-card-actions",
|
|
onClick: (event) => event.stopPropagation(),
|
|
onMouseDown: (event) => event.stopPropagation()
|
|
},
|
|
canClaim ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary btn-sm", type: "button", onClick: () => onClaimRequest(requestId) }, "\u0412\u0437\u044F\u0442\u044C \u0432 \u0440\u0430\u0431\u043E\u0442\u0443") : null,
|
|
canMove && transitionOptions.length ? /* @__PURE__ */ React.createElement("div", { onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
className: "kanban-transition-select",
|
|
value: "",
|
|
placeholder: "\u041F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438\u2026",
|
|
onChange: (nextValue) => {
|
|
const targetStatus = String(nextValue || "");
|
|
if (!targetStatus) return;
|
|
onMoveRequest(row, "", targetStatus);
|
|
},
|
|
options: transitionOptions.map((transition) => ({
|
|
value: String(transition.to_status),
|
|
label: String(transition.to_status_name || transition.to_status)
|
|
}))
|
|
}
|
|
)) : null
|
|
)
|
|
);
|
|
}) : /* @__PURE__ */ React.createElement("p", { className: "muted kanban-empty" }, "\u041F\u0443\u0441\u0442\u043E"))
|
|
);
|
|
})), StatusLine ? /* @__PURE__ */ React.createElement(StatusLine, { status }) : null);
|
|
}
|
|
|
|
// app/web/admin/features/config/ConfigSection.jsx
|
|
function fmtBalance(value) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return "-";
|
|
return number.toLocaleString("ru-RU", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " \u20BD";
|
|
}
|
|
function smsBalanceSummary(health) {
|
|
if (!health || typeof health !== "object") return "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero: \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430...";
|
|
const provider = String(health.provider || "").toLowerCase();
|
|
if (provider !== "smsaero") {
|
|
return "SMS \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440: " + String(health.provider || "-") + " (\u0431\u0430\u043B\u0430\u043D\u0441 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D)";
|
|
}
|
|
if (health.balance_available) {
|
|
return "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero: " + fmtBalance(health.balance_amount);
|
|
}
|
|
const issues = Array.isArray(health.issues) ? health.issues.filter(Boolean) : [];
|
|
return "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D" + (issues.length ? " \u2022 " + String(issues[0]) : "");
|
|
}
|
|
function ConfigSection(props) {
|
|
const {
|
|
token,
|
|
tables,
|
|
dictionaries,
|
|
configActiveKey,
|
|
activeConfigTableState,
|
|
activeConfigMeta,
|
|
genericConfigHeaders,
|
|
canCreateInConfig,
|
|
canUpdateInConfig,
|
|
canDeleteInConfig,
|
|
statusDesignerTopicCode,
|
|
statusDesignerCards,
|
|
getTableLabel,
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
resolveReferenceLabel,
|
|
resolveTableConfig,
|
|
getStatus,
|
|
loadCurrentConfigTable,
|
|
onRefreshSmsProviderHealth,
|
|
smsProviderHealth,
|
|
openCreateRecordModal,
|
|
openFilterModal,
|
|
removeFilterChip,
|
|
openFilterEditModal,
|
|
toggleTableSort,
|
|
openEditRecordModal,
|
|
deleteRecord,
|
|
loadStatusDesignerTopic,
|
|
openCreateStatusTransitionForTopic,
|
|
loadPrevPage,
|
|
loadNextPage,
|
|
loadAllRows,
|
|
FilterToolbarComponent,
|
|
DataTableComponent,
|
|
StatusLineComponent,
|
|
IconButtonComponent,
|
|
UserAvatarComponent
|
|
} = props;
|
|
const FilterToolbar = FilterToolbarComponent;
|
|
const DataTable = DataTableComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const IconButton = IconButtonComponent;
|
|
const UserAvatar = UserAvatarComponent;
|
|
const statusRouteLabel = (code) => resolveReferenceLabel({ table: "statuses", value_field: "code", label_field: "name" }, code);
|
|
const canRefresh = Boolean(configActiveKey);
|
|
const canCreateRecord = Boolean(canCreateInConfig && configActiveKey);
|
|
const canLoadAllRows = Boolean(
|
|
configActiveKey && activeConfigTableState.total > 0 && !activeConfigTableState.showAll && activeConfigTableState.rows.length < activeConfigTableState.total
|
|
);
|
|
const canLoadPrev = Boolean(configActiveKey && !activeConfigTableState.showAll && activeConfigTableState.offset > 0);
|
|
const canLoadNext = Boolean(
|
|
configActiveKey && !activeConfigTableState.showAll && activeConfigTableState.offset + PAGE_SIZE < activeConfigTableState.total
|
|
);
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "breadcrumbs" }, configActiveKey ? getTableLabel(configActiveKey) : "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D"), configActiveKey === "otp_sessions" ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, smsBalanceSummary(smsProviderHealth), smsProviderHealth?.loaded_at ? " \u2022 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E " + fmtDate(smsProviderHealth.loaded_at) : "") : null), /* @__PURE__ */ React.createElement("div", { className: "config-head-actions" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn",
|
|
type: "button",
|
|
onClick: () => openCreateRecordModal(configActiveKey),
|
|
disabled: !canCreateRecord,
|
|
title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C",
|
|
"aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C"
|
|
},
|
|
/* @__PURE__ */ React.createElement(AddIcon, null)
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn",
|
|
type: "button",
|
|
onClick: () => openFilterModal(configActiveKey),
|
|
disabled: !configActiveKey,
|
|
title: "\u0424\u0438\u043B\u044C\u0442\u0440",
|
|
"aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440"
|
|
},
|
|
/* @__PURE__ */ React.createElement(FilterIcon, null)
|
|
), configActiveKey === "otp_sessions" ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onRefreshSmsProviderHealth }, "\u0411\u0430\u043B\u0430\u043D\u0441") : null)), /* @__PURE__ */ React.createElement("div", { className: "config-layout" }, /* @__PURE__ */ React.createElement("div", { className: "config-panel config-panel-flat" }, /* @__PURE__ */ React.createElement("div", { className: "config-content" }, /* @__PURE__ */ React.createElement(
|
|
FilterToolbar,
|
|
{
|
|
filters: activeConfigTableState.filters,
|
|
onOpen: () => openFilterModal(configActiveKey),
|
|
onRemove: (index) => removeFilterChip(configActiveKey, index),
|
|
onEdit: (index) => openFilterEditModal(configActiveKey, index),
|
|
hideAction: true,
|
|
getChipLabel: (clause) => {
|
|
const fieldDef = getFieldDef(configActiveKey, clause.field);
|
|
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview(configActiveKey, clause);
|
|
}
|
|
}
|
|
), configActiveKey === "topics" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", sortable: true, field: "name" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", sortable: true, field: "enabled" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.topics.rows,
|
|
emptyColspan: 4,
|
|
onSort: (field) => toggleTableSort("topics", field),
|
|
sortClause: tables.topics.sort && tables.topics.sort[0] || TABLE_SERVER_CONFIG.topics.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.name || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0435\u043C\u0443", onClick: () => openEditRecordModal("topics", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u0435\u043C\u0443", onClick: () => deleteRecord("topics", row.id), tone: "danger" }))))
|
|
}
|
|
) : null, configActiveKey === "quotes" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "author", label: "\u0410\u0432\u0442\u043E\u0440", sortable: true, field: "author" },
|
|
{ key: "text", label: "\u0422\u0435\u043A\u0441\u0442", sortable: true, field: "text" },
|
|
{ key: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", sortable: true, field: "source" },
|
|
{ key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", sortable: true, field: "is_active" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.quotes.rows,
|
|
emptyColspan: 7,
|
|
onSort: (field) => toggleTableSort("quotes", field),
|
|
sortClause: tables.quotes.sort && tables.quotes.sort[0] || TABLE_SERVER_CONFIG.quotes.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.author || "-"), /* @__PURE__ */ React.createElement("td", null, row.text || "-"), /* @__PURE__ */ React.createElement("td", null, row.source || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_active)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => openEditRecordModal("quotes", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => deleteRecord("quotes", row.id), tone: "danger" }))))
|
|
}
|
|
) : null, configActiveKey === "statuses" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", sortable: true, field: "name" },
|
|
{ key: "status_group_id", label: "\u0413\u0440\u0443\u043F\u043F\u0430", sortable: true, field: "status_group_id" },
|
|
{ key: "kind", label: "\u0422\u0438\u043F", sortable: true, field: "kind" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", sortable: true, field: "enabled" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "is_terminal", label: "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439", sortable: true, field: "is_terminal" },
|
|
{ key: "invoice_template", label: "\u0428\u0430\u0431\u043B\u043E\u043D \u0441\u0447\u0435\u0442\u0430" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.statuses.rows,
|
|
emptyColspan: 8,
|
|
onSort: (field) => toggleTableSort("statuses", field),
|
|
sortClause: tables.statuses.sort && tables.statuses.sort[0] || TABLE_SERVER_CONFIG.statuses.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.name || "-"), /* @__PURE__ */ React.createElement("td", null, resolveReferenceLabel({ table: "status_groups", value_field: "id", label_field: "name" }, row.status_group_id)), /* @__PURE__ */ React.createElement("td", null, statusKindLabel(row.kind)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_terminal)), /* @__PURE__ */ React.createElement("td", null, row.invoice_template || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441", onClick: () => openEditRecordModal("statuses", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441", onClick: () => deleteRecord("statuses", row.id), tone: "danger" }))))
|
|
}
|
|
) : null, configActiveKey === "formFields" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "key", label: "\u041A\u043B\u044E\u0447", sortable: true, field: "key" },
|
|
{ key: "label", label: "\u041C\u0435\u0442\u043A\u0430", sortable: true, field: "label" },
|
|
{ key: "type", label: "\u0422\u0438\u043F", sortable: true, field: "type" },
|
|
{ key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", sortable: true, field: "required" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", sortable: true, field: "enabled" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.formFields.rows,
|
|
emptyColspan: 7,
|
|
onSort: (field) => toggleTableSort("formFields", field),
|
|
sortClause: tables.formFields.sort && tables.formFields.sort[0] || TABLE_SERVER_CONFIG.formFields.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.key || "-")), /* @__PURE__ */ React.createElement("td", null, row.label || "-"), /* @__PURE__ */ React.createElement("td", null, row.type || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.required)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", onClick: () => openEditRecordModal("formFields", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", onClick: () => deleteRecord("formFields", row.id), tone: "danger" }))))
|
|
}
|
|
) : null, configActiveKey === "topicRequiredFields" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" },
|
|
{ key: "field_key", label: "\u041F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", sortable: true, field: "field_key" },
|
|
{ key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", sortable: true, field: "required" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", sortable: true, field: "enabled" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u043E", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.topicRequiredFields.rows,
|
|
emptyColspan: 7,
|
|
onSort: (field) => toggleTableSort("topicRequiredFields", field),
|
|
sortClause: tables.topicRequiredFields.sort && tables.topicRequiredFields.sort[0] || TABLE_SERVER_CONFIG.topicRequiredFields.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.field_key || "-")), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.required)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: "\u270E",
|
|
tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
onClick: () => openEditRecordModal("topicRequiredFields", row)
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: "\u{1F5D1}",
|
|
tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
onClick: () => deleteRecord("topicRequiredFields", row.id),
|
|
tone: "danger"
|
|
}
|
|
))))
|
|
}
|
|
) : null, configActiveKey === "topicDataTemplates" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" },
|
|
{ key: "key", label: "\u041A\u043B\u044E\u0447", sortable: true, field: "key" },
|
|
{ key: "label", label: "\u041C\u0435\u0442\u043A\u0430", sortable: true, field: "label" },
|
|
{ key: "description", label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", sortable: true, field: "description" },
|
|
{ key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", sortable: true, field: "required" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", sortable: true, field: "enabled" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u043E", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.topicDataTemplates.rows,
|
|
emptyColspan: 9,
|
|
onSort: (field) => toggleTableSort("topicDataTemplates", field),
|
|
sortClause: tables.topicDataTemplates.sort && tables.topicDataTemplates.sort[0] || TABLE_SERVER_CONFIG.topicDataTemplates.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.key || "-")), /* @__PURE__ */ React.createElement("td", null, row.label || "-"), /* @__PURE__ */ React.createElement("td", null, row.description || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.required)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D", onClick: () => openEditRecordModal("topicDataTemplates", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D", onClick: () => deleteRecord("topicDataTemplates", row.id), tone: "danger" }))))
|
|
}
|
|
) : null, configActiveKey === "statusTransitions" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "status-designer" }, /* @__PURE__ */ React.createElement("div", { className: "status-designer-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h4", null, "\u041A\u043E\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u043E\u0440 \u043C\u0430\u0440\u0448\u0440\u0443\u0442\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0412\u0435\u0442\u0432\u043B\u0435\u043D\u0438\u044F, \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u044B, SLA \u0438 \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u043D\u0438\u044F \u043A \u0434\u0430\u043D\u043D\u044B\u043C/\u0444\u0430\u0439\u043B\u0430\u043C \u043D\u0430 \u043A\u0430\u0436\u0434\u043E\u043C \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0435.")), /* @__PURE__ */ React.createElement("div", { className: "status-designer-controls" }, /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "status-designer-topic",
|
|
value: statusDesignerTopicCode,
|
|
onChange: (nextValue) => loadStatusDesignerTopic(nextValue),
|
|
options: [
|
|
{ value: "", label: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443" },
|
|
...(dictionaries.topics || []).map((topic) => ({
|
|
value: topic.code,
|
|
label: (topic.name || topic.code) + " (" + topic.code + ")"
|
|
}))
|
|
],
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443"
|
|
}
|
|
), /* @__PURE__ */ React.createElement("button", { className: "btn secondary btn-sm", type: "button", onClick: () => loadStatusDesignerTopic(statusDesignerTopicCode) }, "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0442\u0435\u043C\u0443"), /* @__PURE__ */ React.createElement("button", { className: "btn btn-sm", type: "button", onClick: openCreateStatusTransitionForTopic }, "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0445\u043E\u0434"))), statusDesignerCards.length ? /* @__PURE__ */ React.createElement("div", { className: "status-designer-grid", id: "status-designer-cards" }, statusDesignerCards.map((card) => /* @__PURE__ */ React.createElement("div", { className: "status-node-card", key: card.code }, /* @__PURE__ */ React.createElement("div", { className: "status-node-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("b", null, card.name), /* @__PURE__ */ React.createElement("code", null, card.code)), card.isTerminal ? /* @__PURE__ */ React.createElement("span", { className: "status-node-terminal" }, "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439") : null), card.outgoing.length ? /* @__PURE__ */ React.createElement("ul", { className: "simple-list status-node-links" }, card.outgoing.map((link) => /* @__PURE__ */ React.createElement("li", { key: String(link.id) }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "status-link-chip",
|
|
type: "button",
|
|
onClick: () => openEditRecordModal("statusTransitions", link)
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", null, statusRouteLabel(link.to_status)),
|
|
/* @__PURE__ */ React.createElement("small", null, "SLA: " + (link.sla_hours == null ? "-" : String(link.sla_hours) + " \u0447") + " \u2022 \u0414\u0430\u043D\u043D\u044B\u0435: " + listPreview(link.required_data_keys, "-") + " \u2022 \u0424\u0430\u0439\u043B\u044B: " + listPreview(link.required_mime_types, "-"))
|
|
)))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041D\u0435\u0442 \u0438\u0441\u0445\u043E\u0434\u044F\u0449\u0438\u0445 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u043E\u0432")))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0414\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0439 \u0442\u0435\u043C\u044B \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u044B \u043F\u043E\u043A\u0430 \u043D\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D\u044B.")), /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" },
|
|
{ key: "from_status", label: "\u0418\u0437 \u0441\u0442\u0430\u0442\u0443\u0441\u0430", sortable: true, field: "from_status" },
|
|
{ key: "to_status", label: "\u0412 \u0441\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "to_status" },
|
|
{ key: "sla_hours", label: "SLA (\u0447\u0430\u0441\u044B)", sortable: true, field: "sla_hours" },
|
|
{ key: "required_data_keys", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435" },
|
|
{ key: "required_mime_types", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", sortable: true, field: "enabled" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.statusTransitions.rows,
|
|
emptyColspan: 9,
|
|
onSort: (field) => toggleTableSort("statusTransitions", field),
|
|
sortClause: tables.statusTransitions.sort && tables.statusTransitions.sort[0] || TABLE_SERVER_CONFIG.statusTransitions.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, statusRouteLabel(row.from_status)), /* @__PURE__ */ React.createElement("td", null, statusRouteLabel(row.to_status)), /* @__PURE__ */ React.createElement("td", null, row.sla_hours == null ? "-" : String(row.sla_hours)), /* @__PURE__ */ React.createElement("td", null, listPreview(row.required_data_keys, "-")), /* @__PURE__ */ React.createElement("td", null, listPreview(row.required_mime_types, "-")), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.enabled)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: "\u270E",
|
|
tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0435\u0440\u0435\u0445\u043E\u0434",
|
|
onClick: () => openEditRecordModal("statusTransitions", row)
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: "\u{1F5D1}",
|
|
tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u0445\u043E\u0434",
|
|
onClick: () => deleteRecord("statusTransitions", row.id),
|
|
tone: "danger"
|
|
}
|
|
))))
|
|
}
|
|
)) : null, configActiveKey === "users" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "name", label: "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C", sortable: true, field: "name" },
|
|
{ key: "email", label: "Email", sortable: true, field: "email" },
|
|
{ key: "role", label: "\u0420\u043E\u043B\u044C", sortable: true, field: "role" },
|
|
{ key: "primary_topic_code", label: "\u041F\u0440\u043E\u0444\u0438\u043B\u044C (\u0442\u0435\u043C\u0430)", sortable: true, field: "primary_topic_code" },
|
|
{ key: "default_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430", sortable: true, field: "default_rate" },
|
|
{ key: "salary_percent", label: "\u041F\u0440\u043E\u0446\u0435\u043D\u0442", sortable: true, field: "salary_percent" },
|
|
{ key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", sortable: true, field: "is_active" },
|
|
{ key: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", sortable: true, field: "responsible" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.users.rows,
|
|
emptyColspan: 10,
|
|
onSort: (field) => toggleTableSort("users", field),
|
|
sortClause: tables.users.sort && tables.users.sort[0] || TABLE_SERVER_CONFIG.users.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "user-identity" }, /* @__PURE__ */ React.createElement(UserAvatar, { name: row.name, email: row.email, avatarUrl: row.avatar_url, accessToken: token, size: 32 }), /* @__PURE__ */ React.createElement("div", { className: "user-identity-text" }, /* @__PURE__ */ React.createElement("button", { className: "user-identity-link", type: "button", onClick: () => openEditRecordModal("users", row) }, row.name || "-")))), /* @__PURE__ */ React.createElement("td", null, row.email || "-"), /* @__PURE__ */ React.createElement("td", null, roleLabel(row.role)), /* @__PURE__ */ React.createElement("td", null, resolveReferenceLabel({ table: "topics", value_field: "code", label_field: "name" }, row.primary_topic_code)), /* @__PURE__ */ React.createElement("td", null, row.default_rate == null ? "-" : String(row.default_rate)), /* @__PURE__ */ React.createElement("td", null, row.salary_percent == null ? "-" : String(row.salary_percent)), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_active)), /* @__PURE__ */ React.createElement("td", null, row.responsible || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F", onClick: () => deleteRecord("users", row.id), tone: "danger" }))))
|
|
}
|
|
) : null, configActiveKey === "userTopics" ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "admin_user_id", label: "\u042E\u0440\u0438\u0441\u0442", sortable: true, field: "admin_user_id" },
|
|
{ key: "topic_code", label: "\u0414\u043E\u043F. \u0442\u0435\u043C\u0430", sortable: true, field: "topic_code" },
|
|
{ key: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", sortable: true, field: "responsible" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u043E", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tables.userTopics.rows,
|
|
emptyColspan: 5,
|
|
onSort: (field) => toggleTableSort("userTopics", field),
|
|
sortClause: tables.userTopics.sort && tables.userTopics.sort[0] || TABLE_SERVER_CONFIG.userTopics.sort[0],
|
|
renderRow: (row) => {
|
|
const lawyer = (dictionaries.users || []).find((item) => String(item.id) === String(row.admin_user_id));
|
|
const lawyerLabel = lawyer ? lawyer.name || lawyer.email || row.admin_user_id : row.admin_user_id || "-";
|
|
return /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, lawyerLabel), /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, row.responsible || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0432\u044F\u0437\u044C", onClick: () => openEditRecordModal("userTopics", row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u044C", onClick: () => deleteRecord("userTopics", row.id), tone: "danger" }))));
|
|
}
|
|
}
|
|
) : null, configActiveKey && !KNOWN_CONFIG_TABLE_KEYS.has(configActiveKey) ? /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: genericConfigHeaders,
|
|
rows: activeConfigTableState.rows,
|
|
emptyColspan: Math.max(1, genericConfigHeaders.length),
|
|
onSort: (field) => toggleTableSort(configActiveKey, field),
|
|
sortClause: activeConfigTableState.sort && activeConfigTableState.sort[0] || (resolveTableConfig(configActiveKey)?.sort || [])[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id || JSON.stringify(row) }, (activeConfigMeta?.columns || []).filter((column) => String(column?.name || "") !== "id").map((column) => {
|
|
const key = String(column.name || "");
|
|
const value = row[key];
|
|
if (column.kind === "boolean") return /* @__PURE__ */ React.createElement("td", { key }, boolLabel(Boolean(value)));
|
|
if (column.kind === "date" || column.kind === "datetime") return /* @__PURE__ */ React.createElement("td", { key }, fmtDate(value));
|
|
if (column.kind === "json") return /* @__PURE__ */ React.createElement("td", { key }, value == null ? "-" : JSON.stringify(value));
|
|
const reference = normalizeReferenceMeta(column.reference);
|
|
if (reference) return /* @__PURE__ */ React.createElement("td", { key }, resolveReferenceLabel(reference, value));
|
|
return /* @__PURE__ */ React.createElement("td", { key }, value == null || value === "" ? "-" : String(value));
|
|
}), canUpdateInConfig || canDeleteInConfig ? /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, canUpdateInConfig ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u043F\u0438\u0441\u044C", onClick: () => openEditRecordModal(configActiveKey, row) }) : null, canDeleteInConfig ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u0438\u0441\u044C", onClick: () => deleteRecord(configActiveKey, row.id), tone: "danger" }) : null)) : null)
|
|
}
|
|
) : null, /* @__PURE__ */ React.createElement("div", { className: "pager table-footer-bar config-controls-bar" }, /* @__PURE__ */ React.createElement("div", { className: "config-controls-summary" }, activeConfigTableState.showAll ? "\u0412\u0441\u0435\u0433\u043E: " + activeConfigTableState.total + " \u2022 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0432\u0441\u0435 \u0437\u0430\u043F\u0438\u0441\u0438" : "\u0412\u0441\u0435\u0433\u043E: " + activeConfigTableState.total + " \u2022 \u0441\u043C\u0435\u0449\u0435\u043D\u0438\u0435: " + activeConfigTableState.offset), /* @__PURE__ */ React.createElement("div", { className: "config-controls-actions" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn table-control-loadall",
|
|
type: "button",
|
|
onClick: () => loadAllRows(configActiveKey),
|
|
disabled: !canLoadAllRows,
|
|
title: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + activeConfigTableState.total,
|
|
"aria-label": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + activeConfigTableState.total
|
|
},
|
|
/* @__PURE__ */ React.createElement(DownloadIcon, null),
|
|
/* @__PURE__ */ React.createElement("span", null, activeConfigTableState.total)
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn",
|
|
type: "button",
|
|
onClick: () => loadCurrentConfigTable(true),
|
|
disabled: !canRefresh,
|
|
title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C",
|
|
"aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C"
|
|
},
|
|
/* @__PURE__ */ React.createElement(RefreshIcon, null)
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn",
|
|
type: "button",
|
|
onClick: () => loadPrevPage(configActiveKey),
|
|
disabled: !canLoadPrev,
|
|
title: "\u041D\u0430\u0437\u0430\u0434",
|
|
"aria-label": "\u041D\u0430\u0437\u0430\u0434"
|
|
},
|
|
/* @__PURE__ */ React.createElement(PrevIcon, null)
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn",
|
|
type: "button",
|
|
onClick: () => loadNextPage(configActiveKey),
|
|
disabled: !canLoadNext,
|
|
title: "\u0412\u043F\u0435\u0440\u0435\u0434",
|
|
"aria-label": "\u0412\u043F\u0435\u0440\u0435\u0434"
|
|
},
|
|
/* @__PURE__ */ React.createElement(NextIcon, null)
|
|
))), /* @__PURE__ */ React.createElement(StatusLine, { status: getStatus(configActiveKey) })))));
|
|
}
|
|
|
|
// app/web/admin/features/dashboard/DashboardSection.jsx
|
|
function DashboardSection({
|
|
dashboardData,
|
|
token,
|
|
status,
|
|
apiCall,
|
|
onOpenRequest,
|
|
DataTableComponent,
|
|
StatusLineComponent,
|
|
UserAvatarComponent
|
|
}) {
|
|
const { useMemo: useMemo2, useState: useState4 } = React;
|
|
const DataTable = DataTableComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const UserAvatar = UserAvatarComponent;
|
|
const [lawyerModal, setLawyerModal] = useState4({
|
|
open: false,
|
|
loading: false,
|
|
error: "",
|
|
lawyer: null,
|
|
rows: [],
|
|
totals: { amount: 0, salary: 0 }
|
|
});
|
|
const statusCards = useMemo2(() => {
|
|
return Object.entries(dashboardData?.byStatus || {}).map(([label, value]) => ({ label, value })).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru"));
|
|
}, [dashboardData?.byStatus]);
|
|
const fmtThousandsCompact = (value) => {
|
|
const amount = Number(value || 0);
|
|
if (!Number.isFinite(amount)) return "0";
|
|
return new Intl.NumberFormat("ru-RU", {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 1
|
|
}).format(amount / 1e3);
|
|
};
|
|
const openLawyerModal = async (lawyerRow) => {
|
|
if (!lawyerRow?.lawyer_id || typeof apiCall !== "function") return;
|
|
setLawyerModal({
|
|
open: true,
|
|
loading: true,
|
|
error: "",
|
|
lawyer: lawyerRow,
|
|
rows: [],
|
|
totals: { amount: 0, salary: 0 }
|
|
});
|
|
try {
|
|
const data = await apiCall("/api/admin/metrics/lawyers/" + encodeURIComponent(String(lawyerRow.lawyer_id)) + "/active-requests");
|
|
setLawyerModal((prev) => ({
|
|
...prev,
|
|
loading: false,
|
|
error: "",
|
|
rows: Array.isArray(data?.rows) ? data.rows : [],
|
|
totals: {
|
|
amount: Number(data?.totals?.amount || 0),
|
|
salary: Number(data?.totals?.salary || 0)
|
|
}
|
|
}));
|
|
} catch (error) {
|
|
setLawyerModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438" }));
|
|
}
|
|
};
|
|
const closeLawyerModal = () => {
|
|
setLawyerModal({ open: false, loading: false, error: "", lawyer: null, rows: [], totals: { amount: 0, salary: 0 } });
|
|
};
|
|
const isLawyerScope = dashboardData?.scope === "LAWYER";
|
|
const lawyerCards = Array.isArray(dashboardData?.lawyerLoads) ? dashboardData.lawyerLoads : [];
|
|
const currentLawyer = lawyerCards[0] || null;
|
|
const lawyerMetrics = currentLawyer ? [
|
|
{ label: "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435", value: String(currentLawyer.active_load ?? 0) },
|
|
{ label: "\u041D\u043E\u0432\u044B\u0435", value: String(currentLawyer.monthly_assigned_count ?? 0) },
|
|
{ label: "\u0417\u0430\u043A\u0440\u044B\u0442\u043E", value: String(currentLawyer.monthly_completed_count ?? 0) },
|
|
{ label: "\u0417\u041F, \u0442\u044B\u0441.", value: fmtThousandsCompact(currentLawyer.monthly_salary) }
|
|
] : [];
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u041E\u0431\u0437\u043E\u0440 \u043C\u0435\u0442\u0440\u0438\u043A"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, isLawyerScope ? "\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043E\u043A \u0438 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u043B\u044C\u043D\u0430\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430." : "\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043E\u043A, \u0444\u0438\u043D\u0430\u043D\u0441\u044B \u043C\u0435\u0441\u044F\u0446\u0430 \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u044E\u0440\u0438\u0441\u0442\u043E\u0432."))), /* @__PURE__ */ React.createElement("div", { className: "cards" }, (dashboardData?.cards || []).map((card) => /* @__PURE__ */ React.createElement("div", { className: "card", key: card.label }, /* @__PURE__ */ React.createElement("p", null, card.label), /* @__PURE__ */ React.createElement("b", null, card.value)))), statusCards.length ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.8rem" } }, /* @__PURE__ */ React.createElement("div", { className: "section-head", style: { marginBottom: "0.5rem" } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", { style: { margin: 0 } }, "\u0421\u0442\u0430\u0442\u0443\u0441\u044B \u0437\u0430\u044F\u0432\u043E\u043A"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.2rem" } }, "\u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0440\u0430\u0441\u043A\u043B\u0430\u0434\u043A\u0430 \u043F\u043E \u0432\u0441\u0435\u043C \u0441\u0442\u0430\u0442\u0443\u0441\u0430\u043C."))), /* @__PURE__ */ React.createElement("div", { className: "cards" }, statusCards.map((card) => /* @__PURE__ */ React.createElement("div", { className: "card", key: "status-" + card.label }, /* @__PURE__ */ React.createElement("p", null, card.label), /* @__PURE__ */ React.createElement("b", null, String(card.value ?? 0)))))) : null, isLawyerScope ? /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.9rem" } }, /* @__PURE__ */ React.createElement("h3", { style: { margin: "0 0 0.55rem" } }, "\u041C\u043E\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430"), /* @__PURE__ */ React.createElement("div", { className: "cards" }, lawyerMetrics.length ? lawyerMetrics.map((metric) => /* @__PURE__ */ React.createElement("div", { className: "card", key: "lawyer-metric-" + metric.label }, /* @__PURE__ */ React.createElement("p", null, metric.label), /* @__PURE__ */ React.createElement("b", null, metric.value))) : /* @__PURE__ */ React.createElement("div", { className: "card" }, /* @__PURE__ */ React.createElement("p", null, "\u041C\u043E\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430"), /* @__PURE__ */ React.createElement("b", null, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445")))) : /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.9rem" } }, /* @__PURE__ */ React.createElement("h3", { style: { margin: "0 0 0.55rem" } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u044E\u0440\u0438\u0441\u0442\u043E\u0432"), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-grid" }, lawyerCards.length ? lawyerCards.map((row) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
key: row.lawyer_id,
|
|
type: "button",
|
|
className: "lawyer-dashboard-card",
|
|
onClick: () => openLawyerModal(row),
|
|
title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0434\u0435\u0442\u0430\u043B\u0438 \u044E\u0440\u0438\u0441\u0442\u0430"
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-left" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-avatar" }, /* @__PURE__ */ React.createElement(UserAvatar, { name: row.name, email: row.email, avatarUrl: row.avatar_url, accessToken: token, size: 72 })), /* @__PURE__ */ React.createElement("b", { className: "lawyer-dashboard-name" }, row.name || row.email || "-"), /* @__PURE__ */ React.createElement("span", { className: "lawyer-dashboard-topic" }, row.primary_topic_code || "\u0422\u0435\u043C\u0430 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430")),
|
|
/* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-right" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435"), /* @__PURE__ */ React.createElement("b", null, String(row.active_load ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u041D\u043E\u0432\u044B\u0435"), /* @__PURE__ */ React.createElement("b", null, String(row.monthly_assigned_count ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u0430\u043A\u0440\u044B\u0442\u043E"), /* @__PURE__ */ React.createElement("b", null, String(row.monthly_completed_count ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0421\u0443\u043C\u043C\u0430, \u0442\u044B\u0441."), /* @__PURE__ */ React.createElement("b", null, fmtThousandsCompact(row.monthly_paid_gross))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u041F, \u0442\u044B\u0441."), /* @__PURE__ */ React.createElement("b", null, fmtThousandsCompact(row.monthly_salary))))
|
|
)) : /* @__PURE__ */ React.createElement("div", { className: "card" }, /* @__PURE__ */ React.createElement("p", null, "\u042E\u0440\u0438\u0441\u0442\u044B"), /* @__PURE__ */ React.createElement("b", null, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445")))), /* @__PURE__ */ React.createElement(StatusLine, { status }), /* @__PURE__ */ React.createElement("div", { className: "overlay" + (lawyerModal.open ? " open" : ""), onClick: closeLawyerModal }, /* @__PURE__ */ React.createElement("div", { className: "modal lawyer-dashboard-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, lawyerModal.lawyer ? "\u042E\u0440\u0438\u0441\u0442: " + (lawyerModal.lawyer.name || lawyerModal.lawyer.email || "-") : "\u042E\u0440\u0438\u0441\u0442"), lawyerModal.lawyer ? /* @__PURE__ */ React.createElement("p", { className: "muted", style: { margin: "0.2rem 0 0" } }, (lawyerModal.lawyer.primary_topic_code || "\u0422\u0435\u043C\u0430 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430") + " \u2022 " + (lawyerModal.lawyer.email || "")) : null), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeLawyerModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), lawyerModal.lawyer ? /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-summary" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-avatar" }, /* @__PURE__ */ React.createElement(
|
|
UserAvatar,
|
|
{
|
|
name: lawyerModal.lawyer.name,
|
|
email: lawyerModal.lawyer.email,
|
|
avatarUrl: lawyerModal.lawyer.avatar_url,
|
|
accessToken: token,
|
|
size: 84
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-metrics" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0412 \u0440\u0430\u0431\u043E\u0442\u0435"), /* @__PURE__ */ React.createElement("b", null, String(lawyerModal.lawyer.active_load ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u041D\u043E\u0432\u044B\u0435"), /* @__PURE__ */ React.createElement("b", null, String(lawyerModal.lawyer.monthly_assigned_count ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0435"), /* @__PURE__ */ React.createElement("b", null, String(lawyerModal.lawyer.monthly_completed_count ?? 0))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0421\u0443\u043C\u043C\u0430"), /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.lawyer.monthly_paid_gross))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-metric-pair" }, /* @__PURE__ */ React.createElement("span", null, "\u0417\u0430\u0440\u043F\u043B\u0430\u0442\u0430"), /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.lawyer.monthly_salary))))) : null, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-scroll" }, lawyerModal.loading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u0437\u0430\u044F\u0432\u043E\u043A...") : null, lawyerModal.error ? /* @__PURE__ */ React.createElement("p", { className: "status error" }, lawyerModal.error) : null, !lawyerModal.loading ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-table-area" }, /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "track_number", label: "\u041D\u043E\u043C\u0435\u0440" },
|
|
{ key: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441" },
|
|
{ key: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430" },
|
|
{ key: "invoice_amount", label: "\u0421\u0443\u043C\u043C\u0430 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435" },
|
|
{ key: "month_paid_amount", label: "\u041E\u043F\u043B\u0430\u0442\u044B" },
|
|
{ key: "month_salary_amount", label: "\u0417\u0430\u0440\u043F\u043B\u0430\u0442\u0430" }
|
|
],
|
|
rows: lawyerModal.rows || [],
|
|
emptyColspan: 7,
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "request-track-link",
|
|
onClick: (event) => {
|
|
if (typeof onOpenRequest === "function") onOpenRequest(row.id, event);
|
|
closeLawyerModal();
|
|
},
|
|
title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443"
|
|
},
|
|
/* @__PURE__ */ React.createElement("code", null, row.track_number || "-")
|
|
)), /* @__PURE__ */ React.createElement("td", null, statusLabel(row.status_code)), /* @__PURE__ */ React.createElement("td", null, row.client_name || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, fmtAmount(row.invoice_amount)), /* @__PURE__ */ React.createElement("td", null, fmtAmount(row.month_paid_amount)), /* @__PURE__ */ React.createElement("td", null, fmtAmount(row.month_salary_amount)))
|
|
}
|
|
))) : null), !lawyerModal.loading ? /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-modal-footer" }, /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-total-chip" }, "\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0445: ", /* @__PURE__ */ React.createElement("b", null, String((lawyerModal.rows || []).length))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-total-chip" }, "\u041E\u043F\u043B\u0430\u0442\u044B: ", /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.totals.amount))), /* @__PURE__ */ React.createElement("div", { className: "lawyer-dashboard-total-chip" }, "\u0417\u0430\u0440\u043F\u043B\u0430\u0442\u0430: ", /* @__PURE__ */ React.createElement("b", null, fmtAmount(lawyerModal.totals.salary)))) : null)));
|
|
}
|
|
|
|
// app/web/admin/features/invoices/InvoicesSection.jsx
|
|
function InvoicesSection({
|
|
role,
|
|
tables,
|
|
status,
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
onRefresh,
|
|
onCreate,
|
|
onOpenFilter,
|
|
onRemoveFilter,
|
|
onEditFilter,
|
|
onSort,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onOpenRequest,
|
|
onDownloadPdf,
|
|
onEditRecord,
|
|
onDeleteRecord,
|
|
FilterToolbarComponent,
|
|
DataTableComponent,
|
|
TablePagerComponent,
|
|
StatusLineComponent,
|
|
IconButtonComponent
|
|
}) {
|
|
const tableState = tables?.invoices || { rows: [], filters: [], sort: [] };
|
|
const FilterToolbar = FilterToolbarComponent;
|
|
const DataTable = DataTableComponent;
|
|
const TablePager = TablePagerComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const IconButton = IconButtonComponent;
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0421\u0447\u0435\u0442\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0441\u0447\u0435\u0442\u0430 \u043A\u043B\u0438\u0435\u043D\u0442\u0430\u043C, \u0441\u0442\u0430\u0442\u0443\u0441\u044B \u043E\u043F\u043B\u0430\u0442\u044B \u0438 \u0432\u044B\u0433\u0440\u0443\u0437\u043A\u0430 PDF.")), /* @__PURE__ */ React.createElement("div", { className: "section-head-actions" }, onCreate ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onCreate, title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C", "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(AddIcon, null)) : null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)))), /* @__PURE__ */ React.createElement(
|
|
FilterToolbar,
|
|
{
|
|
filters: tableState.filters,
|
|
onOpen: onOpenFilter,
|
|
onRemove: onRemoveFilter,
|
|
onEdit: onEditFilter,
|
|
hideAction: true,
|
|
getChipLabel: (clause) => {
|
|
const fieldDef = getFieldDef("invoices", clause.field);
|
|
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("invoices", clause);
|
|
}
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "invoice_number", label: "\u041D\u043E\u043C\u0435\u0440", sortable: true, field: "invoice_number" },
|
|
{ key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "status" },
|
|
{ key: "amount", label: "\u0421\u0443\u043C\u043C\u0430", sortable: true, field: "amount" },
|
|
{ key: "payer_display_name", label: "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A", sortable: true, field: "payer_display_name" },
|
|
{ key: "request_track_number", label: "\u0417\u0430\u044F\u0432\u043A\u0430" },
|
|
{ key: "issued_by_name", label: "\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u043B", sortable: true, field: "issued_by_admin_user_id" },
|
|
{ key: "issued_at", label: "\u0421\u0444\u043E\u0440\u043C\u0438\u0440\u043E\u0432\u0430\u043D", sortable: true, field: "issued_at" },
|
|
{ key: "paid_at", label: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D", sortable: true, field: "paid_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tableState.rows,
|
|
emptyColspan: 9,
|
|
onSort,
|
|
sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.invoices.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.invoice_number || "-")), /* @__PURE__ */ React.createElement("td", null, row.status_label || invoiceStatusLabel(row.status)), /* @__PURE__ */ React.createElement("td", null, row.amount == null ? "-" : String(row.amount) + " " + String(row.currency || "RUB")), /* @__PURE__ */ React.createElement("td", null, row.payer_display_name || "-"), /* @__PURE__ */ React.createElement("td", null, row.request_id ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "request-track-link",
|
|
onClick: (event) => onOpenRequest(row, event),
|
|
title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443"
|
|
},
|
|
/* @__PURE__ */ React.createElement("code", null, row.request_track_number || row.request_id || "-")
|
|
) : /* @__PURE__ */ React.createElement("code", null, row.request_track_number || row.request_id || "-")), /* @__PURE__ */ React.createElement("td", null, row.issued_by_name || "-"), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.issued_at)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.paid_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u2B07", tooltip: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C PDF", onClick: () => onDownloadPdf(row) }), role === "ADMIN" ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0447\u0435\u0442", onClick: () => onEditRecord(row) }) : null, role === "ADMIN" ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0447\u0435\u0442", onClick: () => onDeleteRecord(row.id), tone: "danger" }) : null)))
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
TablePager,
|
|
{
|
|
tableState,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onRefresh
|
|
}
|
|
), /* @__PURE__ */ React.createElement(StatusLine, { status }));
|
|
}
|
|
|
|
// app/web/admin/features/requests/RequestsSection.jsx
|
|
function renderRequestUpdatesCell(row, role) {
|
|
const hasServiceRequestUnread = Boolean(row?.has_service_requests_unread);
|
|
const serviceRequestCount = Number(row?.service_requests_unread_count || 0);
|
|
const viewerUnreadTotal = Number(row?.viewer_unread_total || 0);
|
|
const viewerUnreadByEvent = row?.viewer_unread_by_event && typeof row.viewer_unread_by_event === "object" ? row.viewer_unread_by_event : {};
|
|
const viewerUnreadLabel = viewerUnreadTotal > 0 ? Object.entries(viewerUnreadByEvent).map(([eventType, count]) => {
|
|
const code = String(eventType || "").toUpperCase();
|
|
const label = REQUEST_UPDATE_EVENT_LABELS[code] || code.toLowerCase();
|
|
return label + ": " + String(count || 0);
|
|
}).join(", ") : "";
|
|
if (role === "LAWYER") {
|
|
const has = Boolean(row.lawyer_has_unread_updates);
|
|
const eventType = String(row.lawyer_unread_event_type || "").toUpperCase();
|
|
if (!has && !hasServiceRequestUnread && !viewerUnreadTotal) return /* @__PURE__ */ React.createElement("span", { className: "request-update-empty" }, "\u043D\u0435\u0442");
|
|
return /* @__PURE__ */ React.createElement("span", { className: "request-updates-stack" }, viewerUnreadTotal > 0 ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435: " + (viewerUnreadLabel || String(viewerUnreadTotal)) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u041C\u043D\u0435: " + String(viewerUnreadTotal)) : null, has ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u0415\u0441\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u043E\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435: " + (REQUEST_UPDATE_EVENT_LABELS[eventType] || eventType.toLowerCase()) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), REQUEST_UPDATE_EVENT_LABELS[eventType] || "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435") : null, hasServiceRequestUnread ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430: " + String(serviceRequestCount) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u0417\u0430\u043F\u0440\u043E\u0441\u044B: " + String(serviceRequestCount || 1)) : null);
|
|
}
|
|
const clientHas = Boolean(row.client_has_unread_updates);
|
|
const clientType = String(row.client_unread_event_type || "").toUpperCase();
|
|
const lawyerHas = Boolean(row.lawyer_has_unread_updates);
|
|
const lawyerType = String(row.lawyer_unread_event_type || "").toUpperCase();
|
|
if (!clientHas && !lawyerHas && !hasServiceRequestUnread && !viewerUnreadTotal) return /* @__PURE__ */ React.createElement("span", { className: "request-update-empty" }, "\u043D\u0435\u0442");
|
|
return /* @__PURE__ */ React.createElement("span", { className: "request-updates-stack" }, viewerUnreadTotal > 0 ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435: " + (viewerUnreadLabel || String(viewerUnreadTotal)) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u041C\u043D\u0435: " + String(viewerUnreadTotal)) : null, clientHas ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041A\u043B\u0438\u0435\u043D\u0442\u0443: " + (REQUEST_UPDATE_EVENT_LABELS[clientType] || clientType.toLowerCase()) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u041A\u043B\u0438\u0435\u043D\u0442: " + (REQUEST_UPDATE_EVENT_LABELS[clientType] || "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435")) : null, lawyerHas ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u042E\u0440\u0438\u0441\u0442\u0443: " + (REQUEST_UPDATE_EVENT_LABELS[lawyerType] || lawyerType.toLowerCase()) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u042E\u0440\u0438\u0441\u0442: " + (REQUEST_UPDATE_EVENT_LABELS[lawyerType] || "\u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435")) : null, hasServiceRequestUnread ? /* @__PURE__ */ React.createElement("span", { className: "request-update-chip", title: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430: " + String(serviceRequestCount) }, /* @__PURE__ */ React.createElement("span", { className: "request-update-dot" }), "\u0417\u0430\u043F\u0440\u043E\u0441\u044B: " + String(serviceRequestCount || 1)) : null);
|
|
}
|
|
function RequestsSection({
|
|
role,
|
|
tables,
|
|
status,
|
|
getStatus,
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
resolveReferenceLabel,
|
|
onRefresh,
|
|
onCreate,
|
|
onOpenFilter,
|
|
onRemoveFilter,
|
|
onEditFilter,
|
|
onSort,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onClaimRequest,
|
|
onOpenReassign,
|
|
onOpenRequest,
|
|
onEditRecord,
|
|
onDeleteRecord,
|
|
FilterToolbarComponent,
|
|
DataTableComponent,
|
|
TablePagerComponent,
|
|
StatusLineComponent,
|
|
IconButtonComponent
|
|
}) {
|
|
const tableState = tables?.requests || { rows: [], filters: [], sort: [] };
|
|
const FilterToolbar = FilterToolbarComponent;
|
|
const DataTable = DataTableComponent;
|
|
const TablePager = TablePagerComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const IconButton = IconButtonComponent;
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0417\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0421\u0435\u0440\u0432\u0435\u0440\u043D\u0430\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u044F \u0438 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0445 \u0437\u0430\u044F\u0432\u043E\u043A.")), /* @__PURE__ */ React.createElement("div", { className: "section-head-actions" }, onCreate ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onCreate, title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C", "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(AddIcon, null)) : null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)))), /* @__PURE__ */ React.createElement(
|
|
FilterToolbar,
|
|
{
|
|
filters: tableState.filters,
|
|
onOpen: onOpenFilter,
|
|
onRemove: onRemoveFilter,
|
|
onEdit: onEditFilter,
|
|
hideAction: true,
|
|
getChipLabel: (clause) => {
|
|
const fieldDef = getFieldDef("requests", clause.field);
|
|
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("requests", clause);
|
|
}
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "track_number", label: "\u041D\u043E\u043C\u0435\u0440", sortable: true, field: "track_number" },
|
|
{ key: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442", sortable: true, field: "client_name" },
|
|
{ key: "client_phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", sortable: true, field: "client_phone" },
|
|
{ key: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "status_code" },
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", sortable: true, field: "topic_code" },
|
|
{ key: "assigned_lawyer_id", label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D", sortable: true, field: "assigned_lawyer_id" },
|
|
{ key: "invoice_amount", label: "\u0421\u0447\u0435\u0442", sortable: true, field: "invoice_amount" },
|
|
{ key: "paid_at", label: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E", sortable: true, field: "paid_at" },
|
|
{ key: "updates", label: "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u044F" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tableState.rows,
|
|
emptyColspan: 11,
|
|
onSort,
|
|
sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.requests.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "request-track-link",
|
|
onClick: (event) => onOpenRequest(row.id, event),
|
|
title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443"
|
|
},
|
|
/* @__PURE__ */ React.createElement("code", null, row.track_number || "-")
|
|
)), /* @__PURE__ */ React.createElement("td", null, row.client_name || "-"), /* @__PURE__ */ React.createElement("td", null, row.client_phone || "-"), /* @__PURE__ */ React.createElement("td", null, statusLabel(row.status_code)), /* @__PURE__ */ React.createElement("td", null, row.topic_code || "-"), /* @__PURE__ */ React.createElement("td", null, resolveReferenceLabel({ table: "admin_users", value_field: "id", label_field: "name" }, row.assigned_lawyer_id)), /* @__PURE__ */ React.createElement("td", null, row.invoice_amount == null ? "-" : String(row.invoice_amount)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.paid_at)), /* @__PURE__ */ React.createElement("td", null, renderRequestUpdatesCell(row, role)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, role === "LAWYER" ? /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: "\u{1F4E5}",
|
|
tooltip: row.assigned_lawyer_id ? "\u0417\u0430\u044F\u0432\u043A\u0430 \u0443\u0436\u0435 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0430" : "\u0412\u0437\u044F\u0442\u044C \u0432 \u0440\u0430\u0431\u043E\u0442\u0443",
|
|
onClick: () => onClaimRequest(row.id),
|
|
disabled: Boolean(row.assigned_lawyer_id)
|
|
}
|
|
) : null, role === "ADMIN" && row.assigned_lawyer_id ? /* @__PURE__ */ React.createElement(IconButton, { icon: "\u21C4", tooltip: "\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0438\u0442\u044C", onClick: () => onOpenReassign(row) }) : null, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443", onClick: () => onEditRecord(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443", onClick: () => onDeleteRecord(row.id), tone: "danger" }))))
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
TablePager,
|
|
{
|
|
tableState,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onRefresh
|
|
}
|
|
), /* @__PURE__ */ React.createElement(StatusLine, { status: status || (typeof getStatus === "function" ? getStatus("requests") : null) }));
|
|
}
|
|
|
|
// app/web/admin/features/quotes/QuotesSection.jsx
|
|
function QuotesSection({
|
|
tables,
|
|
status,
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
onRefresh,
|
|
onCreate,
|
|
onOpenFilter,
|
|
onRemoveFilter,
|
|
onEditFilter,
|
|
onSort,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onEditRecord,
|
|
onDeleteRecord,
|
|
FilterToolbarComponent,
|
|
DataTableComponent,
|
|
TablePagerComponent,
|
|
StatusLineComponent,
|
|
IconButtonComponent
|
|
}) {
|
|
const tableState = tables?.quotes || { rows: [], filters: [], sort: [] };
|
|
const FilterToolbar = FilterToolbarComponent;
|
|
const DataTable = DataTableComponent;
|
|
const TablePager = TablePagerComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const IconButton = IconButtonComponent;
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0426\u0438\u0442\u0430\u0442\u044B"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0423\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043F\u0443\u0431\u043B\u0438\u0447\u043D\u043E\u0439 \u043B\u0435\u043D\u0442\u043E\u0439 \u0446\u0438\u0442\u0430\u0442 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u043C\u0438 \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u043C\u0438."))), /* @__PURE__ */ React.createElement(
|
|
FilterToolbar,
|
|
{
|
|
filters: tableState.filters,
|
|
onOpen: onOpenFilter,
|
|
onRemove: onRemoveFilter,
|
|
onEdit: onEditFilter,
|
|
hideAction: true,
|
|
getChipLabel: (clause) => {
|
|
const fieldDef = getFieldDef("quotes", clause.field);
|
|
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("quotes", clause);
|
|
}
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "author", label: "\u0410\u0432\u0442\u043E\u0440", sortable: true, field: "author" },
|
|
{ key: "text", label: "\u0422\u0435\u043A\u0441\u0442", sortable: true, field: "text" },
|
|
{ key: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", sortable: true, field: "source" },
|
|
{ key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", sortable: true, field: "is_active" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", sortable: true, field: "sort_order" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D\u0430", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tableState.rows,
|
|
emptyColspan: 7,
|
|
onSort,
|
|
sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.quotes.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, row.author || "-"), /* @__PURE__ */ React.createElement("td", null, row.text || "-"), /* @__PURE__ */ React.createElement("td", null, row.source || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(row.is_active)), /* @__PURE__ */ React.createElement("td", null, String(row.sort_order ?? 0)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => onEditRecord(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0446\u0438\u0442\u0430\u0442\u0443", onClick: () => onDeleteRecord(row.id), tone: "danger" }))))
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
TablePager,
|
|
{
|
|
tableState,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onRefresh,
|
|
onCreate,
|
|
onOpenFilter
|
|
}
|
|
), /* @__PURE__ */ React.createElement(StatusLine, { status }));
|
|
}
|
|
|
|
// app/web/admin/features/service-requests/ServiceRequestsSection.jsx
|
|
function serviceRequestTypeLabel(value) {
|
|
const code = String(value || "").toUpperCase();
|
|
return SERVICE_REQUEST_TYPE_LABELS[code] || code || "-";
|
|
}
|
|
function serviceRequestStatusLabel(value) {
|
|
const code = String(value || "").toUpperCase();
|
|
return SERVICE_REQUEST_STATUS_LABELS[code] || code || "-";
|
|
}
|
|
function unreadLabel(row, role) {
|
|
if (String(role || "").toUpperCase() === "LAWYER") {
|
|
return row?.lawyer_unread ? "\u0414\u0430" : "\u041D\u0435\u0442";
|
|
}
|
|
return row?.admin_unread ? "\u0414\u0430" : "\u041D\u0435\u0442";
|
|
}
|
|
function ServiceRequestsSection({
|
|
role,
|
|
tables,
|
|
status,
|
|
getStatus,
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
resolveReferenceLabel,
|
|
onRefresh,
|
|
onCreate,
|
|
onOpenFilter,
|
|
onRemoveFilter,
|
|
onEditFilter,
|
|
onSort,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onOpenRequest,
|
|
onMarkRead,
|
|
onEditRecord,
|
|
onDeleteRecord,
|
|
FilterToolbarComponent,
|
|
DataTableComponent,
|
|
TablePagerComponent,
|
|
StatusLineComponent,
|
|
IconButtonComponent
|
|
}) {
|
|
const tableState = tables?.serviceRequests || { rows: [], filters: [], sort: [] };
|
|
const FilterToolbar = FilterToolbarComponent;
|
|
const DataTable = DataTableComponent;
|
|
const TablePager = TablePagerComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const IconButton = IconButtonComponent;
|
|
const roleCode = String(role || "").toUpperCase();
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0417\u0430\u043F\u0440\u043E\u0441\u044B"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430 \u043A \u043A\u0443\u0440\u0430\u0442\u043E\u0440\u0443 \u0438 \u043E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u044F \u043D\u0430 \u0441\u043C\u0435\u043D\u0443 \u044E\u0440\u0438\u0441\u0442\u0430.")), /* @__PURE__ */ React.createElement("div", { className: "section-head-actions" }, onCreate && roleCode === "ADMIN" ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onCreate, title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C", "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(AddIcon, null)) : null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)))), /* @__PURE__ */ React.createElement(
|
|
FilterToolbar,
|
|
{
|
|
filters: tableState.filters,
|
|
onOpen: onOpenFilter,
|
|
onRemove: onRemoveFilter,
|
|
onEdit: onEditFilter,
|
|
hideAction: true,
|
|
getChipLabel: (clause) => {
|
|
const fieldDef = getFieldDef("serviceRequests", clause.field);
|
|
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("serviceRequests", clause);
|
|
}
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "type", label: "\u0422\u0438\u043F", sortable: true, field: "type" },
|
|
{ key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", sortable: true, field: "status" },
|
|
{ key: "body", label: "\u041E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u0435", sortable: false },
|
|
{ key: "request_id", label: "\u0417\u0430\u044F\u0432\u043A\u0430", sortable: true, field: "request_id" },
|
|
{ key: "unread", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E", sortable: true, field: roleCode === "LAWYER" ? "lawyer_unread" : "admin_unread" },
|
|
{ key: "created_at", label: "\u0421\u043E\u0437\u0434\u0430\u043D", sortable: true, field: "created_at" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tableState.rows,
|
|
emptyColspan: 7,
|
|
onSort,
|
|
sortClause: tableState.sort && tableState.sort[0] || TABLE_SERVER_CONFIG.serviceRequests.sort[0],
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: row.id }, /* @__PURE__ */ React.createElement("td", null, serviceRequestTypeLabel(row.type)), /* @__PURE__ */ React.createElement("td", null, serviceRequestStatusLabel(row.status)), /* @__PURE__ */ React.createElement("td", null, row.body || "-"), /* @__PURE__ */ React.createElement("td", null, (() => {
|
|
const requestTrackNumber = String(row?.request_track_number || "").trim() || String(
|
|
typeof resolveReferenceLabel === "function" ? resolveReferenceLabel({ table: "requests", value_field: "id", label_field: "track_number" }, row?.request_id) : ""
|
|
).trim();
|
|
const requestLabel = requestTrackNumber || String(row?.request_id || "").trim() || "-";
|
|
if (!row.request_id) return "-";
|
|
return /* @__PURE__ */ React.createElement("button", { type: "button", className: "request-track-link", onClick: (event) => onOpenRequest(row.request_id, event), title: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443" }, /* @__PURE__ */ React.createElement("code", null, requestLabel));
|
|
})()), /* @__PURE__ */ React.createElement("td", null, unreadLabel(row, roleCode)), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.created_at)), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u2713", tooltip: "\u041E\u0442\u043C\u0435\u0442\u0438\u0442\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u043C", onClick: () => onMarkRead(row.id) }), roleCode === "ADMIN" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(IconButton, { icon: "\u270E", tooltip: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441", onClick: () => onEditRecord(row) }), /* @__PURE__ */ React.createElement(IconButton, { icon: "\u{1F5D1}", tooltip: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441", onClick: () => onDeleteRecord(row.id), tone: "danger" })) : null)))
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
TablePager,
|
|
{
|
|
tableState,
|
|
onPrev,
|
|
onNext,
|
|
onLoadAll,
|
|
onRefresh
|
|
}
|
|
), /* @__PURE__ */ React.createElement(StatusLine, { status: status || (typeof getStatus === "function" ? getStatus("serviceRequests") : null) }));
|
|
}
|
|
|
|
// app/web/admin/features/requests/RequestWorkspace.jsx
|
|
function RequestWorkspace({
|
|
viewerRole,
|
|
viewerUserId,
|
|
viewerUserEmail,
|
|
viewerUserName,
|
|
loading,
|
|
trackNumber,
|
|
requestData,
|
|
financeSummary,
|
|
invoices,
|
|
statusRouteNodes,
|
|
statusHistory,
|
|
availableStatuses,
|
|
currentImportantDateAt,
|
|
pendingStatusChangePreset,
|
|
messages,
|
|
messagesHasMore,
|
|
messagesLoadingMore,
|
|
attachments,
|
|
messageDraft,
|
|
selectedFiles,
|
|
fileUploading,
|
|
status,
|
|
onMessageChange,
|
|
onSendMessage,
|
|
onLoadOlderMessages,
|
|
onFilesSelect,
|
|
onRemoveSelectedFile,
|
|
onClearSelectedFiles,
|
|
onLoadRequestDataTemplates,
|
|
onLoadRequestDataBatch,
|
|
onLoadRequestDataTemplateDetails,
|
|
onSaveRequestDataTemplate,
|
|
onSaveRequestDataBatch,
|
|
onIssueInvoice,
|
|
onDownloadInvoicePdf,
|
|
onSaveRequestDataValues,
|
|
onUploadRequestAttachment,
|
|
onChangeStatus,
|
|
onConsumePendingStatusChangePreset,
|
|
onLiveProbe,
|
|
onTypingSignal,
|
|
domIds,
|
|
AttachmentPreviewModalComponent,
|
|
StatusLineComponent
|
|
}) {
|
|
const { useEffect: useEffect4, useMemo: useMemo2, useRef: useRef4, useState: useState4 } = React;
|
|
const [preview, setPreview] = useState4({ open: false, url: "", fileName: "", mimeType: "" });
|
|
const [chatTab, setChatTab] = useState4("chat");
|
|
const [dropActive, setDropActive] = useState4(false);
|
|
const [financeOpen, setFinanceOpen] = useState4(false);
|
|
const [financeIssueForm, setFinanceIssueForm] = useState4({
|
|
open: false,
|
|
saving: false,
|
|
amount: "",
|
|
serviceDescription: "",
|
|
payerDisplayName: "",
|
|
error: ""
|
|
});
|
|
const [requestDataListOpen, setRequestDataListOpen] = useState4(false);
|
|
const [descriptionOpen, setDescriptionOpen] = useState4(false);
|
|
const [requestTemplateSuggestOpen, setRequestTemplateSuggestOpen] = useState4(false);
|
|
const [catalogFieldSuggestOpen, setCatalogFieldSuggestOpen] = useState4(false);
|
|
const [statusChangeModal, setStatusChangeModal] = useState4({
|
|
open: false,
|
|
saving: false,
|
|
statusCode: "",
|
|
allowedStatusCodes: null,
|
|
importantDateAt: "",
|
|
comment: "",
|
|
files: [],
|
|
error: ""
|
|
});
|
|
const [draggedRequestRowId, setDraggedRequestRowId] = useState4("");
|
|
const [dragOverRequestRowId, setDragOverRequestRowId] = useState4("");
|
|
const [dataRequestModal, setDataRequestModal] = useState4({
|
|
open: false,
|
|
loading: false,
|
|
saving: false,
|
|
savingTemplate: false,
|
|
messageId: "",
|
|
documentName: "",
|
|
availableDocuments: [],
|
|
templateList: [],
|
|
requestTemplateQuery: "",
|
|
templateName: "",
|
|
selectedRequestTemplateId: "",
|
|
templates: [],
|
|
catalogFieldQuery: "",
|
|
selectedCatalogTemplateId: "",
|
|
rows: [],
|
|
customLabel: "",
|
|
customType: "string",
|
|
templateStatus: "",
|
|
error: ""
|
|
});
|
|
const [clientDataModal, setClientDataModal] = useState4({
|
|
open: false,
|
|
loading: false,
|
|
saving: false,
|
|
messageId: "",
|
|
items: [],
|
|
status: "",
|
|
error: ""
|
|
});
|
|
const [composerFocused, setComposerFocused] = useState4(false);
|
|
const [typingPeers, setTypingPeers] = useState4([]);
|
|
const [liveMode, setLiveMode] = useState4("online");
|
|
const fileInputRef = useRef4(null);
|
|
const statusChangeFileInputRef = useRef4(null);
|
|
const chatListRef = useRef4(null);
|
|
const liveCursorRef = useRef4("");
|
|
const liveTimerRef = useRef4(null);
|
|
const liveInFlightRef = useRef4(false);
|
|
const liveFailCountRef = useRef4(0);
|
|
const typingHeartbeatRef = useRef4(null);
|
|
const typingActiveRef = useRef4(false);
|
|
const lastAutoScrollCursorRef = useRef4("");
|
|
const idMap = useMemo2(
|
|
() => ({
|
|
messagesList: "request-modal-messages",
|
|
filesList: "request-modal-files",
|
|
messageBody: "request-modal-message-body",
|
|
sendButton: "request-modal-message-send",
|
|
fileInput: "request-modal-file-input",
|
|
fileUploadButton: "",
|
|
dataRequestOverlay: "data-request-overlay",
|
|
dataRequestItems: "data-request-items",
|
|
dataRequestStatus: "data-request-status",
|
|
dataRequestSave: "data-request-save",
|
|
...domIds || {}
|
|
}),
|
|
[domIds]
|
|
);
|
|
const requestDataTypeOptions = useMemo2(
|
|
() => [
|
|
{ value: "string", label: "\u0421\u0442\u0440\u043E\u043A\u0430" },
|
|
{ value: "date", label: "\u0414\u0430\u0442\u0430" },
|
|
{ value: "number", label: "\u0427\u0438\u0441\u043B\u043E" },
|
|
{ value: "file", label: "\u0424\u0430\u0439\u043B" },
|
|
{ value: "text", label: "\u0422\u0435\u043A\u0441\u0442" }
|
|
],
|
|
[]
|
|
);
|
|
const openPreview = (item) => {
|
|
if (!item?.download_url) return;
|
|
setPreview({
|
|
open: true,
|
|
url: String(item.download_url),
|
|
fileName: String(item.file_name || ""),
|
|
mimeType: String(item.mime_type || "")
|
|
});
|
|
};
|
|
const closePreview = () => setPreview({ open: false, url: "", fileName: "", mimeType: "" });
|
|
const pendingFiles = Array.isArray(selectedFiles) ? selectedFiles : [];
|
|
const hasPendingFiles = pendingFiles.length > 0;
|
|
const canSubmit = Boolean(String(messageDraft || "").trim() || hasPendingFiles);
|
|
const onInputFiles = (event) => {
|
|
const files = Array.from(event.target && event.target.files || []);
|
|
if (files.length && typeof onFilesSelect === "function") onFilesSelect(files);
|
|
event.target.value = "";
|
|
};
|
|
const onDropFiles = (event) => {
|
|
event.preventDefault();
|
|
setDropActive(false);
|
|
const files = Array.from(event.dataTransfer && event.dataTransfer.files || []);
|
|
if (files.length && typeof onFilesSelect === "function") onFilesSelect(files);
|
|
};
|
|
const row = requestData && typeof requestData === "object" ? requestData : null;
|
|
const finance = financeSummary && typeof financeSummary === "object" ? financeSummary : null;
|
|
const viewerRoleCode = String(viewerRole || "").toUpperCase();
|
|
const canRequestData = viewerRoleCode === "LAWYER" || viewerRoleCode === "ADMIN";
|
|
const canFillRequestData = viewerRoleCode === "CLIENT";
|
|
const canSeeRate = viewerRoleCode !== "CLIENT";
|
|
const canSeeCreatedUpdatedInCard = viewerRoleCode !== "CLIENT";
|
|
const showTopicStatusInCard = viewerRoleCode !== "CLIENT";
|
|
const showContactsInCard = viewerRoleCode !== "CLIENT";
|
|
const safeMessages = Array.isArray(messages) ? messages : [];
|
|
const safeAttachments = Array.isArray(attachments) ? attachments : [];
|
|
const safeInvoices = Array.isArray(invoices) ? invoices : [];
|
|
const safeStatusHistory = Array.isArray(statusHistory) ? statusHistory : [];
|
|
const safeAvailableStatuses = Array.isArray(availableStatuses) ? availableStatuses : [];
|
|
const totalFilesBytes = safeAttachments.reduce((acc, item) => acc + Number(item?.size_bytes || 0), 0);
|
|
const clientLabel = row?.client_name || "-";
|
|
const clientPhone = String(row?.client_phone || "").trim();
|
|
const lawyerLabel = row?.assigned_lawyer_name || row?.assigned_lawyer_id || "\u041D\u0435 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D";
|
|
const lawyerPhone = String(row?.assigned_lawyer_phone || "").trim();
|
|
const clientHasPhone = Boolean(clientPhone);
|
|
const lawyerHasPhone = Boolean(lawyerPhone);
|
|
const messagePlaceholder = canFillRequestData ? "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u044E\u0440\u0438\u0441\u0442\u0430" : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0434\u043B\u044F \u043A\u043B\u0438\u0435\u043D\u0442\u0430";
|
|
const selectedRequestTemplateCandidate = useMemo2(
|
|
() => (dataRequestModal.templateList || []).find((item) => {
|
|
const query = String(dataRequestModal.requestTemplateQuery || "").trim().toLowerCase();
|
|
if (!query) return false;
|
|
return query === String(item?.name || "").trim().toLowerCase() || query === String(item?.id || "").trim().toLowerCase();
|
|
}) || null,
|
|
[dataRequestModal.requestTemplateQuery, dataRequestModal.templateList]
|
|
);
|
|
const selectedCatalogFieldCandidate = useMemo2(
|
|
() => (dataRequestModal.templates || []).find((item) => {
|
|
const query = String(dataRequestModal.catalogFieldQuery || "").trim().toLowerCase();
|
|
if (!query) return false;
|
|
return query === String(item?.label || "").trim().toLowerCase() || query === String(item?.key || "").trim().toLowerCase() || query === String(item?.id || "").trim().toLowerCase();
|
|
}) || null,
|
|
[dataRequestModal.catalogFieldQuery, dataRequestModal.templates]
|
|
);
|
|
const filteredRequestTemplates = useMemo2(() => {
|
|
const query = String(dataRequestModal.requestTemplateQuery || "").trim().toLowerCase();
|
|
const rows = Array.isArray(dataRequestModal.templateList) ? dataRequestModal.templateList : [];
|
|
if (!query) return rows.slice(0, 8);
|
|
return rows.filter((item) => String(item?.name || "").toLowerCase().includes(query)).slice(0, 8);
|
|
}, [dataRequestModal.requestTemplateQuery, dataRequestModal.templateList]);
|
|
const filteredCatalogFields = useMemo2(() => {
|
|
const query = String(dataRequestModal.catalogFieldQuery || "").trim().toLowerCase();
|
|
const rows = Array.isArray(dataRequestModal.templates) ? dataRequestModal.templates : [];
|
|
if (!query) return rows.slice(0, 10);
|
|
return rows.filter((item) => {
|
|
const label = String(item?.label || "").toLowerCase();
|
|
const key = String(item?.key || "").toLowerCase();
|
|
return label.includes(query) || key.includes(query);
|
|
}).slice(0, 10);
|
|
}, [dataRequestModal.catalogFieldQuery, dataRequestModal.templates]);
|
|
const requestTemplateActionMode = selectedRequestTemplateCandidate ? "save" : String(dataRequestModal.requestTemplateQuery || "").trim() ? "create" : "";
|
|
const catalogFieldActionMode = selectedCatalogFieldCandidate ? "add" : String(dataRequestModal.catalogFieldQuery || "").trim() ? "create" : "";
|
|
const requestTemplateBadge = useMemo2(() => {
|
|
const query = String(dataRequestModal.requestTemplateQuery || "").trim();
|
|
if (!query) return null;
|
|
const matched = selectedRequestTemplateCandidate;
|
|
if (!matched) return { kind: "create", label: "\u041D\u043E\u0432\u044B\u0439 \u0448\u0430\u0431\u043B\u043E\u043D" };
|
|
const roleCode = String(viewerRole || "").toUpperCase();
|
|
const actorId = String(viewerUserId || "").trim();
|
|
const ownerId = String(matched.created_by_admin_id || "").trim();
|
|
if (roleCode === "LAWYER" && ownerId && actorId && ownerId !== actorId) {
|
|
return { kind: "readonly", label: "\u0427\u0443\u0436\u043E\u0439 \u0448\u0430\u0431\u043B\u043E\u043D" };
|
|
}
|
|
return { kind: "existing", label: "\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0439 \u0448\u0430\u0431\u043B\u043E\u043D" };
|
|
}, [dataRequestModal.requestTemplateQuery, selectedRequestTemplateCandidate, viewerRole, viewerUserId]);
|
|
const canSaveSelectedRequestTemplate = useMemo2(() => {
|
|
if (!String(dataRequestModal.requestTemplateQuery || "").trim()) return false;
|
|
if (!requestTemplateBadge) return true;
|
|
return requestTemplateBadge.kind !== "readonly";
|
|
}, [dataRequestModal.requestTemplateQuery, requestTemplateBadge]);
|
|
const attachmentById = useMemo2(() => {
|
|
const map = /* @__PURE__ */ new Map();
|
|
safeAttachments.forEach((item) => {
|
|
const id = String(item?.id || "").trim();
|
|
if (id) map.set(id, item);
|
|
});
|
|
return map;
|
|
}, [safeAttachments]);
|
|
const statusOptions = useMemo2(
|
|
() => safeAvailableStatuses.filter((item) => item && item.code).map((item) => ({
|
|
code: String(item.code),
|
|
name: String(item.name || "").trim() || humanizeKey(item.code),
|
|
groupName: item.status_group_name ? String(item.status_group_name) : "",
|
|
isTerminal: Boolean(item.is_terminal)
|
|
})),
|
|
[safeAvailableStatuses]
|
|
);
|
|
const statusByCode = useMemo2(() => new Map(statusOptions.map((item) => [item.code, item])), [statusOptions]);
|
|
const toDateTimeLocalValue = (value) => {
|
|
if (!value) return "";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "";
|
|
const pad = (n) => String(n).padStart(2, "0");
|
|
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + "T" + pad(date.getHours()) + ":" + pad(date.getMinutes());
|
|
};
|
|
const defaultImportantDateLocal = useMemo2(() => {
|
|
const source = String(currentImportantDateAt || row?.important_date_at || "").trim();
|
|
if (source) {
|
|
const local = toDateTimeLocalValue(source);
|
|
if (local) return local;
|
|
}
|
|
const next = new Date(Date.now() + 3 * 24 * 60 * 60 * 1e3);
|
|
return toDateTimeLocalValue(next.toISOString());
|
|
}, [currentImportantDateAt, row?.important_date_at]);
|
|
const formatDuration = (seconds) => {
|
|
const total = Number(seconds);
|
|
if (!Number.isFinite(total) || total < 0) return "\u2014";
|
|
const days = Math.floor(total / 86400);
|
|
const hours = Math.floor(total % 86400 / 3600);
|
|
const minutes = Math.floor(total % 3600 / 60);
|
|
if (days > 0) return days + " \u0434 " + hours + " \u0447";
|
|
if (hours > 0) return hours + " \u0447 " + minutes + " \u043C\u0438\u043D";
|
|
return Math.max(0, minutes) + " \u043C\u0438\u043D";
|
|
};
|
|
const formatMoneyInput = (value) => {
|
|
const amount = Number(value);
|
|
if (!Number.isFinite(amount) || amount <= 0) return "";
|
|
return String(Math.round((amount + Number.EPSILON) * 100) / 100);
|
|
};
|
|
const openFinanceIssueForm = () => {
|
|
const defaultAmount = finance?.request_cost ?? row?.request_cost ?? row?.invoice_amount ?? finance?.effective_rate ?? row?.effective_rate ?? "";
|
|
setFinanceIssueForm({
|
|
open: true,
|
|
saving: false,
|
|
amount: formatMoneyInput(defaultAmount),
|
|
serviceDescription: String(row?.topic_name || row?.topic_code || "\u042E\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0443\u0441\u043B\u0443\u0433\u0438"),
|
|
payerDisplayName: String(row?.client_name || "").trim() || "\u041A\u043B\u0438\u0435\u043D\u0442",
|
|
error: ""
|
|
});
|
|
};
|
|
const closeFinanceIssueForm = () => {
|
|
setFinanceIssueForm((prev) => ({ ...prev, open: false, saving: false, error: "" }));
|
|
};
|
|
const closeFinanceModal = () => {
|
|
setFinanceOpen(false);
|
|
closeFinanceIssueForm();
|
|
};
|
|
const submitFinanceIssueForm = async (event) => {
|
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
if (!row?.id || typeof onIssueInvoice !== "function") return;
|
|
const normalizedAmount = Number(String(financeIssueForm.amount || "").replace(",", "."));
|
|
if (!Number.isFinite(normalizedAmount) || normalizedAmount <= 0) {
|
|
setFinanceIssueForm((prev) => ({ ...prev, error: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u0443\u044E \u0441\u0443\u043C\u043C\u0443 \u0441\u0447\u0435\u0442\u0430" }));
|
|
return;
|
|
}
|
|
setFinanceIssueForm((prev) => ({ ...prev, saving: true, error: "" }));
|
|
try {
|
|
await onIssueInvoice({
|
|
requestId: String(row.id),
|
|
amount: normalizedAmount,
|
|
serviceDescription: String(financeIssueForm.serviceDescription || ""),
|
|
payerDisplayName: String(financeIssueForm.payerDisplayName || "")
|
|
});
|
|
setFinanceIssueForm((prev) => ({ ...prev, open: false, saving: false, error: "" }));
|
|
} catch (error) {
|
|
setFinanceIssueForm((prev) => ({ ...prev, saving: false, error: error?.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0447\u0435\u0442" }));
|
|
}
|
|
};
|
|
const openStatusChangeModal = (preset) => {
|
|
const suggested = Array.isArray(preset?.suggestedStatuses) ? preset.suggestedStatuses.filter(Boolean) : [];
|
|
const currentCode = String(row?.status_code || "").trim();
|
|
const firstSuggested = suggested.find((code) => code && code !== currentCode) || "";
|
|
setStatusChangeModal({
|
|
open: true,
|
|
saving: false,
|
|
statusCode: firstSuggested,
|
|
allowedStatusCodes: suggested.length ? suggested : null,
|
|
importantDateAt: defaultImportantDateLocal,
|
|
comment: "",
|
|
files: [],
|
|
error: ""
|
|
});
|
|
};
|
|
const closeStatusChangeModal = () => {
|
|
setStatusChangeModal((prev) => ({ ...prev, open: false, saving: false, error: "", files: [] }));
|
|
};
|
|
useEffect4(() => {
|
|
if (!pendingStatusChangePreset) return;
|
|
openStatusChangeModal(pendingStatusChangePreset);
|
|
if (typeof onConsumePendingStatusChangePreset === "function") onConsumePendingStatusChangePreset();
|
|
}, [pendingStatusChangePreset]);
|
|
const requestDataListItems = useMemo2(() => {
|
|
const byKey = /* @__PURE__ */ new Map();
|
|
const messagesChrono = [...safeMessages].sort((a, b) => {
|
|
const at = new Date(a?.created_at || 0).getTime();
|
|
const bt = new Date(b?.created_at || 0).getTime();
|
|
if (at !== bt) return at - bt;
|
|
return String(a?.id || "").localeCompare(String(b?.id || ""), "ru");
|
|
});
|
|
messagesChrono.forEach((msg) => {
|
|
if (String(msg?.message_kind || "") !== "REQUEST_DATA") return;
|
|
const items = Array.isArray(msg?.request_data_items) ? msg.request_data_items : [];
|
|
items.forEach((item, idx) => {
|
|
const key = String(item?.key || item?.id || "item-" + idx);
|
|
if (!key) return;
|
|
byKey.set(key, {
|
|
id: String(item?.id || ""),
|
|
key,
|
|
label: String(item?.label || item?.label_short || key),
|
|
field_type: String(item?.field_type || "string").toLowerCase(),
|
|
value_text: item?.value_text == null ? "" : String(item.value_text),
|
|
is_filled: Boolean(item?.is_filled),
|
|
source_message_id: String(msg?.id || ""),
|
|
source_message_created_at: msg?.created_at || null,
|
|
value_file: item?.value_file || null
|
|
});
|
|
});
|
|
});
|
|
return Array.from(byKey.values()).sort((a, b) => {
|
|
const aFilled = a.is_filled ? 1 : 0;
|
|
const bFilled = b.is_filled ? 1 : 0;
|
|
if (aFilled !== bFilled) return aFilled - bFilled;
|
|
return String(a.label || a.key).localeCompare(String(b.label || b.key), "ru");
|
|
});
|
|
}, [safeMessages]);
|
|
const attachmentsByMessageId = useMemo2(() => {
|
|
const map = /* @__PURE__ */ new Map();
|
|
safeAttachments.forEach((item) => {
|
|
const messageId = String(item?.message_id || "").trim();
|
|
if (!messageId) return;
|
|
if (!map.has(messageId)) map.set(messageId, []);
|
|
map.get(messageId).push(item);
|
|
});
|
|
return map;
|
|
}, [safeAttachments]);
|
|
const localActivityCursor = useMemo2(() => {
|
|
let latestTs = 0;
|
|
const pickLatest = (value) => {
|
|
if (!value) return;
|
|
const ts = new Date(value).getTime();
|
|
if (Number.isFinite(ts) && ts > latestTs) latestTs = ts;
|
|
};
|
|
safeMessages.forEach((item) => {
|
|
pickLatest(item?.updated_at);
|
|
pickLatest(item?.created_at);
|
|
});
|
|
safeAttachments.forEach((item) => {
|
|
pickLatest(item?.updated_at);
|
|
pickLatest(item?.created_at);
|
|
});
|
|
return latestTs > 0 ? new Date(latestTs).toISOString() : "";
|
|
}, [safeAttachments, safeMessages]);
|
|
const typingHintText = useMemo2(() => {
|
|
const rows = Array.isArray(typingPeers) ? typingPeers : [];
|
|
if (!rows.length) return "";
|
|
const labels = rows.map((item) => String(item?.actor_label || item?.label || "").trim()).filter(Boolean);
|
|
if (!labels.length) return "\u0421\u043E\u0431\u0435\u0441\u0435\u0434\u043D\u0438\u043A \u043F\u0435\u0447\u0430\u0442\u0430\u0435\u0442...";
|
|
const unique = [];
|
|
labels.forEach((label) => {
|
|
if (!unique.includes(label)) unique.push(label);
|
|
});
|
|
if (unique.length === 1) return unique[0] + " \u043F\u0435\u0447\u0430\u0442\u0430\u0435\u0442...";
|
|
if (unique.length === 2) return unique[0] + " \u0438 " + unique[1] + " \u043F\u0435\u0447\u0430\u0442\u0430\u044E\u0442...";
|
|
return unique[0] + ", " + unique[1] + " \u0438 \u0435\u0449\u0435 " + String(unique.length - 2) + " \u043F\u0435\u0447\u0430\u0442\u0430\u044E\u0442...";
|
|
}, [typingPeers]);
|
|
const openAttachmentFromMessage = (item) => {
|
|
if (!item?.download_url) return;
|
|
const kind = detectAttachmentPreviewKind(item.file_name, item.mime_type);
|
|
if (kind === "none") {
|
|
window.open(String(item.download_url), "_blank", "noopener,noreferrer");
|
|
return;
|
|
}
|
|
openPreview(item);
|
|
};
|
|
const downloadAttachment = (item) => {
|
|
const url = String(item?.download_url || "").trim();
|
|
if (!url) return;
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.target = "_blank";
|
|
link.rel = "noreferrer";
|
|
const fileName = String(item?.file_name || "").trim();
|
|
if (fileName) link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
};
|
|
useEffect4(() => {
|
|
liveCursorRef.current = localActivityCursor || "";
|
|
}, [localActivityCursor, row?.id]);
|
|
useEffect4(() => {
|
|
if (!row || typeof onLiveProbe !== "function") {
|
|
setTypingPeers([]);
|
|
setLiveMode("online");
|
|
if (liveTimerRef.current) {
|
|
clearTimeout(liveTimerRef.current);
|
|
liveTimerRef.current = null;
|
|
}
|
|
liveInFlightRef.current = false;
|
|
liveFailCountRef.current = 0;
|
|
return void 0;
|
|
}
|
|
let cancelled = false;
|
|
const scheduleNext = (ms) => {
|
|
if (cancelled) return;
|
|
if (liveTimerRef.current) clearTimeout(liveTimerRef.current);
|
|
liveTimerRef.current = setTimeout(runProbe, ms);
|
|
};
|
|
const runProbe = async () => {
|
|
if (cancelled || liveInFlightRef.current) return;
|
|
liveInFlightRef.current = true;
|
|
try {
|
|
const payload = await onLiveProbe({ cursor: liveCursorRef.current });
|
|
const cursor = String(payload?.cursor || "").trim();
|
|
if (cursor) liveCursorRef.current = cursor;
|
|
setTypingPeers(Array.isArray(payload?.typing) ? payload.typing : []);
|
|
liveFailCountRef.current = 0;
|
|
setLiveMode("online");
|
|
} catch (_) {
|
|
liveFailCountRef.current += 1;
|
|
setLiveMode(liveFailCountRef.current >= 3 ? "degraded" : "online");
|
|
} finally {
|
|
liveInFlightRef.current = false;
|
|
const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
const baseInterval = hidden ? 8e3 : 2500;
|
|
const failStep = Math.min(5, Math.max(0, liveFailCountRef.current));
|
|
const backoffInterval = failStep > 0 ? Math.min(3e4, baseInterval * Math.pow(2, failStep - 1)) : baseInterval;
|
|
scheduleNext(backoffInterval);
|
|
}
|
|
};
|
|
runProbe();
|
|
return () => {
|
|
cancelled = true;
|
|
if (liveTimerRef.current) {
|
|
clearTimeout(liveTimerRef.current);
|
|
liveTimerRef.current = null;
|
|
}
|
|
liveInFlightRef.current = false;
|
|
liveFailCountRef.current = 0;
|
|
setTypingPeers([]);
|
|
setLiveMode("online");
|
|
};
|
|
}, [onLiveProbe, row, trackNumber]);
|
|
const typingEnabled = Boolean(
|
|
row && typeof onTypingSignal === "function" && !loading && !fileUploading && composerFocused && String(messageDraft || "").trim()
|
|
);
|
|
useEffect4(() => {
|
|
if (typeof onTypingSignal !== "function" || !row) {
|
|
if (typingHeartbeatRef.current) {
|
|
clearInterval(typingHeartbeatRef.current);
|
|
typingHeartbeatRef.current = null;
|
|
}
|
|
typingActiveRef.current = false;
|
|
return;
|
|
}
|
|
if (typingEnabled) {
|
|
if (!typingActiveRef.current) {
|
|
typingActiveRef.current = true;
|
|
void onTypingSignal({ typing: true }).catch(() => null);
|
|
}
|
|
if (!typingHeartbeatRef.current) {
|
|
typingHeartbeatRef.current = setInterval(() => {
|
|
void onTypingSignal({ typing: true }).catch(() => null);
|
|
}, 2500);
|
|
}
|
|
return;
|
|
}
|
|
if (typingHeartbeatRef.current) {
|
|
clearInterval(typingHeartbeatRef.current);
|
|
typingHeartbeatRef.current = null;
|
|
}
|
|
if (typingActiveRef.current) {
|
|
typingActiveRef.current = false;
|
|
void onTypingSignal({ typing: false }).catch(() => null);
|
|
}
|
|
}, [onTypingSignal, row, typingEnabled]);
|
|
useEffect4(
|
|
() => () => {
|
|
if (typingHeartbeatRef.current) {
|
|
clearInterval(typingHeartbeatRef.current);
|
|
typingHeartbeatRef.current = null;
|
|
}
|
|
if (typingActiveRef.current && typeof onTypingSignal === "function") {
|
|
typingActiveRef.current = false;
|
|
void onTypingSignal({ typing: false }).catch(() => null);
|
|
}
|
|
},
|
|
[onTypingSignal]
|
|
);
|
|
const newDataRequestRow = (source) => {
|
|
const item = source || {};
|
|
const label = String(item.label || "").trim();
|
|
const key = String(item.key || "").trim();
|
|
const fieldTypeRaw = String(item.field_type || item.value_type || "string").trim().toLowerCase();
|
|
const fieldType = ["string", "text", "date", "number", "file"].includes(fieldTypeRaw) ? fieldTypeRaw : "string";
|
|
return {
|
|
localId: "row-" + Math.random().toString(36).slice(2),
|
|
id: item.id ? String(item.id) : "",
|
|
topic_template_id: item.topic_template_id ? String(item.topic_template_id) : item.id ? String(item.id) : "",
|
|
key,
|
|
label: label || "\u041F\u043E\u043B\u0435",
|
|
field_type: fieldType,
|
|
document_name: String(item.document_name || "").trim(),
|
|
value_text: item.value_text == null ? "" : String(item.value_text),
|
|
value_file: item.value_file || null,
|
|
is_filled: Boolean(item.is_filled)
|
|
};
|
|
};
|
|
const getRequestDataRowIdentity = (item) => {
|
|
const rowItem = item || {};
|
|
const key = String(rowItem.key || "").trim().toLowerCase();
|
|
if (key) return "key:" + key;
|
|
const tplId = String(rowItem.topic_template_id || rowItem.id || "").trim();
|
|
if (tplId) return "tpl:" + tplId;
|
|
return "label:" + String(rowItem.label || "").trim().toLowerCase();
|
|
};
|
|
const mergeRequestDataRows = (baseRows, incomingRows) => {
|
|
const rows = Array.isArray(baseRows) ? [...baseRows] : [];
|
|
const nextItems = Array.isArray(incomingRows) ? incomingRows : [];
|
|
const seen = new Set(rows.map((rowItem) => getRequestDataRowIdentity(rowItem)));
|
|
nextItems.forEach((rowItem) => {
|
|
const identity = getRequestDataRowIdentity(rowItem);
|
|
if (!identity || seen.has(identity)) return;
|
|
seen.add(identity);
|
|
rows.push(rowItem);
|
|
});
|
|
return rows;
|
|
};
|
|
const openCreateDataRequestModal = async () => {
|
|
if (!canRequestData || typeof onLoadRequestDataTemplates !== "function") return;
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
open: true,
|
|
loading: true,
|
|
saving: false,
|
|
savingTemplate: false,
|
|
messageId: "",
|
|
rows: [],
|
|
error: "",
|
|
templateStatus: "",
|
|
requestTemplateQuery: "",
|
|
catalogFieldQuery: "",
|
|
selectedCatalogTemplateId: "",
|
|
selectedRequestTemplateId: "",
|
|
templateName: "",
|
|
documentName: "",
|
|
customLabel: "",
|
|
customType: "string"
|
|
}));
|
|
try {
|
|
const data = await onLoadRequestDataTemplates();
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
open: true,
|
|
loading: false,
|
|
templates: Array.isArray(data?.rows) ? data.rows : [],
|
|
templateList: Array.isArray(data?.templates) ? data.templates : [],
|
|
availableDocuments: Array.isArray(data?.documents) ? data.documents : [],
|
|
documentName: "",
|
|
requestTemplateQuery: "",
|
|
catalogFieldQuery: ""
|
|
}));
|
|
} catch (error) {
|
|
setDataRequestModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D\u044B" }));
|
|
}
|
|
};
|
|
const openEditDataRequestModal = async (messageId) => {
|
|
if (!canRequestData || !messageId) return;
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
open: true,
|
|
loading: true,
|
|
saving: false,
|
|
savingTemplate: false,
|
|
messageId: String(messageId),
|
|
rows: [],
|
|
error: "",
|
|
templateStatus: "",
|
|
requestTemplateQuery: "",
|
|
catalogFieldQuery: "",
|
|
selectedCatalogTemplateId: "",
|
|
selectedRequestTemplateId: "",
|
|
templateName: ""
|
|
}));
|
|
try {
|
|
const [batch, templates] = await Promise.all([
|
|
typeof onLoadRequestDataBatch === "function" ? onLoadRequestDataBatch(messageId) : Promise.resolve({ items: [] }),
|
|
typeof onLoadRequestDataTemplates === "function" ? onLoadRequestDataTemplates() : Promise.resolve({ rows: [], documents: [], templates: [] })
|
|
]);
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
open: true,
|
|
loading: false,
|
|
messageId: String(messageId),
|
|
rows: Array.isArray(batch?.items) ? batch.items.map(newDataRequestRow) : [],
|
|
documentName: String(batch?.document_name || ""),
|
|
templates: Array.isArray(templates?.rows) ? templates.rows : [],
|
|
templateList: Array.isArray(templates?.templates) ? templates.templates : [],
|
|
availableDocuments: Array.isArray(templates?.documents) ? templates.documents : [],
|
|
requestTemplateQuery: "",
|
|
catalogFieldQuery: ""
|
|
}));
|
|
} catch (error) {
|
|
setDataRequestModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441" }));
|
|
}
|
|
};
|
|
const closeDataRequestModal = () => {
|
|
setDataRequestModal((prev) => ({ ...prev, open: false, error: "", saving: false, savingTemplate: false, templateStatus: "" }));
|
|
};
|
|
const findRequestTemplateByQuery = (queryValue) => {
|
|
const query = String(queryValue || "").trim().toLowerCase();
|
|
if (!query) return null;
|
|
return (dataRequestModal.templateList || []).find((item) => {
|
|
const id = String(item?.id || "").toLowerCase();
|
|
const name = String(item?.name || "").toLowerCase();
|
|
return query === id || query === name;
|
|
}) || null;
|
|
};
|
|
const findCatalogFieldByQuery = (queryValue) => {
|
|
const query = String(queryValue || "").trim().toLowerCase();
|
|
if (!query) return null;
|
|
return (dataRequestModal.templates || []).find((item) => {
|
|
const id = String(item?.id || "").toLowerCase();
|
|
const key = String(item?.key || "").toLowerCase();
|
|
const label = String(item?.label || "").toLowerCase();
|
|
return query === id || query === key || query === label;
|
|
}) || null;
|
|
};
|
|
const applyRequestTemplateById = async (rawTemplateId, templateNameHint) => {
|
|
if (typeof onLoadRequestDataTemplateDetails !== "function") return;
|
|
const templateId = String(rawTemplateId || "").trim();
|
|
if (!templateId) return;
|
|
setDataRequestModal((prev) => ({ ...prev, loading: true, error: "" }));
|
|
try {
|
|
const data = await onLoadRequestDataTemplateDetails(templateId);
|
|
const incomingRows = (Array.isArray(data?.items) ? data.items : []).map(
|
|
(item) => newDataRequestRow({
|
|
...item,
|
|
topic_template_id: item.topic_data_template_id || item.topic_template_id || "",
|
|
field_type: item.value_type || item.field_type
|
|
})
|
|
);
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
loading: false,
|
|
rows: mergeRequestDataRows(prev.rows, incomingRows),
|
|
selectedRequestTemplateId: String(data?.template?.id || prev.selectedRequestTemplateId || ""),
|
|
requestTemplateQuery: String(data?.template?.name || templateNameHint || prev.requestTemplateQuery || ""),
|
|
templateStatus: ""
|
|
}));
|
|
} catch (error) {
|
|
setDataRequestModal((prev) => ({ ...prev, loading: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" }));
|
|
}
|
|
};
|
|
const applySelectedRequestTemplate = async () => {
|
|
const selectedByQuery = findRequestTemplateByQuery(dataRequestModal.requestTemplateQuery);
|
|
const templateId = String(selectedByQuery?.id || dataRequestModal.selectedRequestTemplateId || "").trim();
|
|
return applyRequestTemplateById(templateId, selectedByQuery?.name || "");
|
|
};
|
|
const refreshDataRequestCatalog = async () => {
|
|
if (typeof onLoadRequestDataTemplates !== "function") return null;
|
|
const data = await onLoadRequestDataTemplates();
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
templates: Array.isArray(data?.rows) ? data.rows : [],
|
|
templateList: Array.isArray(data?.templates) ? data.templates : [],
|
|
availableDocuments: Array.isArray(data?.documents) ? data.documents : [],
|
|
selectedRequestTemplateId: prev.selectedRequestTemplateId && (Array.isArray(data?.templates) ? data.templates : []).some((item) => String(item?.id) === String(prev.selectedRequestTemplateId)) ? prev.selectedRequestTemplateId : ""
|
|
}));
|
|
return data;
|
|
};
|
|
const saveCurrentDataRequestTemplate = async () => {
|
|
if (typeof onSaveRequestDataTemplate !== "function") return;
|
|
const selectedFromQuery = findRequestTemplateByQuery(dataRequestModal.requestTemplateQuery);
|
|
const templateName = String(dataRequestModal.requestTemplateQuery || "").trim();
|
|
const rows = (dataRequestModal.rows || []).filter((row2) => String(row2.label || "").trim());
|
|
if (!templateName) {
|
|
setDataRequestModal((prev) => ({ ...prev, error: "\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0448\u0430\u0431\u043B\u043E\u043D\u0430" }));
|
|
return;
|
|
}
|
|
if (!rows.length) {
|
|
setDataRequestModal((prev) => ({ ...prev, error: "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u043D\u043E \u043F\u043E\u043B\u0435 \u0434\u043B\u044F \u0448\u0430\u0431\u043B\u043E\u043D\u0430" }));
|
|
return;
|
|
}
|
|
setDataRequestModal((prev) => ({ ...prev, savingTemplate: true, error: "", templateStatus: "" }));
|
|
try {
|
|
const result = await onSaveRequestDataTemplate({
|
|
template_id: String(selectedFromQuery?.id || dataRequestModal.selectedRequestTemplateId || "").trim() || void 0,
|
|
name: templateName,
|
|
items: rows.map((row2) => ({
|
|
topic_data_template_id: row2.topic_template_id || void 0,
|
|
key: row2.key || void 0,
|
|
label: row2.label,
|
|
value_type: row2.field_type || "string"
|
|
}))
|
|
});
|
|
const savedRows = (Array.isArray(result?.items) ? result.items : []).map(
|
|
(item) => newDataRequestRow({
|
|
...item,
|
|
topic_template_id: item.topic_data_template_id || item.topic_template_id || "",
|
|
field_type: item.value_type || item.field_type
|
|
})
|
|
);
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
savingTemplate: false,
|
|
rows: savedRows.length ? savedRows : prev.rows,
|
|
selectedRequestTemplateId: String(result?.template?.id || prev.selectedRequestTemplateId || ""),
|
|
requestTemplateQuery: String(result?.template?.name || templateName),
|
|
templateStatus: "\u0428\u0430\u0431\u043B\u043E\u043D \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D"
|
|
}));
|
|
await refreshDataRequestCatalog();
|
|
} catch (error) {
|
|
setDataRequestModal((prev) => ({ ...prev, savingTemplate: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" }));
|
|
}
|
|
};
|
|
const addSelectedTemplateRow = () => {
|
|
const selectedByQuery = findCatalogFieldByQuery(dataRequestModal.catalogFieldQuery);
|
|
const templateId = String(selectedByQuery?.id || dataRequestModal.selectedCatalogTemplateId || "").trim();
|
|
const template = (dataRequestModal.templates || []).find((item) => String(item.id) === templateId);
|
|
if (!template) {
|
|
const manualLabel = String(dataRequestModal.catalogFieldQuery || "").trim();
|
|
if (!manualLabel) return;
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
catalogFieldQuery: "",
|
|
templateStatus: "",
|
|
rows: [...prev.rows || [], newDataRequestRow({ label: manualLabel, field_type: "string" })]
|
|
}));
|
|
return;
|
|
}
|
|
setDataRequestModal((prev) => {
|
|
const exists = (prev.rows || []).some((row2) => String(row2.key || "") === String(template.key || ""));
|
|
if (exists) return { ...prev, selectedCatalogTemplateId: "", catalogFieldQuery: "" };
|
|
return {
|
|
...prev,
|
|
selectedCatalogTemplateId: "",
|
|
catalogFieldQuery: "",
|
|
templateStatus: "",
|
|
rows: [...prev.rows || [], newDataRequestRow({ ...template, topic_template_id: template.id, field_type: template.value_type })]
|
|
};
|
|
});
|
|
};
|
|
const updateDataRequestRow = (localId, patch) => {
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
templateStatus: "",
|
|
rows: (prev.rows || []).map((row2) => row2.localId === localId ? { ...row2, ...patch || {} } : row2)
|
|
}));
|
|
};
|
|
const removeDataRequestRow = (localId) => {
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
templateStatus: "",
|
|
rows: (prev.rows || []).filter((row2) => row2.localId !== localId)
|
|
}));
|
|
};
|
|
const moveDataRequestRow = (localId, delta) => {
|
|
const shift = Number(delta) || 0;
|
|
if (!shift) return;
|
|
setDataRequestModal((prev) => {
|
|
const rows = Array.isArray(prev.rows) ? [...prev.rows] : [];
|
|
const index = rows.findIndex((row2) => row2.localId === localId);
|
|
if (index < 0) return prev;
|
|
const nextIndex = index + shift;
|
|
if (nextIndex < 0 || nextIndex >= rows.length) return prev;
|
|
const [item] = rows.splice(index, 1);
|
|
rows.splice(nextIndex, 0, item);
|
|
return { ...prev, templateStatus: "", rows };
|
|
});
|
|
};
|
|
const moveDataRequestRowToIndex = (localId, targetIndexRaw) => {
|
|
const targetIndex = Number(targetIndexRaw);
|
|
if (!Number.isInteger(targetIndex)) return;
|
|
setDataRequestModal((prev) => {
|
|
const rows = Array.isArray(prev.rows) ? [...prev.rows] : [];
|
|
const fromIndex = rows.findIndex((rowItem) => rowItem.localId === localId);
|
|
if (fromIndex < 0) return prev;
|
|
const boundedIndex = Math.max(0, Math.min(rows.length - 1, targetIndex));
|
|
if (fromIndex === boundedIndex) return prev;
|
|
const [item] = rows.splice(fromIndex, 1);
|
|
rows.splice(boundedIndex, 0, item);
|
|
return { ...prev, templateStatus: "", rows };
|
|
});
|
|
};
|
|
const submitDataRequestModal = async () => {
|
|
if (typeof onSaveRequestDataBatch !== "function") return;
|
|
const rows = (dataRequestModal.rows || []).filter((row2) => String(row2.label || "").trim());
|
|
if (!rows.length) {
|
|
setDataRequestModal((prev) => ({ ...prev, error: "\u0414\u043E\u0431\u0430\u0432\u044C\u0442\u0435 \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u043D\u043E \u043F\u043E\u043B\u0435" }));
|
|
return;
|
|
}
|
|
setDataRequestModal((prev) => ({ ...prev, saving: true, error: "" }));
|
|
try {
|
|
await onSaveRequestDataBatch({
|
|
message_id: dataRequestModal.messageId || void 0,
|
|
items: rows.map((row2) => ({
|
|
id: row2.id || void 0,
|
|
topic_template_id: row2.topic_template_id || void 0,
|
|
key: row2.key || void 0,
|
|
label: row2.label,
|
|
field_type: row2.field_type || "string",
|
|
document_name: row2.document_name || void 0
|
|
}))
|
|
});
|
|
closeDataRequestModal();
|
|
} catch (error) {
|
|
setDataRequestModal((prev) => ({ ...prev, saving: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441" }));
|
|
}
|
|
};
|
|
const closeClientDataModal = () => {
|
|
setClientDataModal({
|
|
open: false,
|
|
loading: false,
|
|
saving: false,
|
|
messageId: "",
|
|
items: [],
|
|
status: "",
|
|
error: ""
|
|
});
|
|
};
|
|
const openClientDataRequestModal = async (messageId) => {
|
|
if (!canFillRequestData || typeof onLoadRequestDataBatch !== "function" || !messageId) return;
|
|
setClientDataModal({
|
|
open: true,
|
|
loading: true,
|
|
saving: false,
|
|
messageId: String(messageId),
|
|
items: [],
|
|
status: "",
|
|
error: ""
|
|
});
|
|
try {
|
|
const data = await onLoadRequestDataBatch(String(messageId));
|
|
const items = Array.isArray(data?.items) ? data.items.slice().sort((a, b) => Number(a?.sort_order || 0) - Number(b?.sort_order || 0)).map((item, index) => ({
|
|
localId: "client-data-" + String(item?.id || item?.key || index),
|
|
id: String(item?.id || ""),
|
|
key: String(item?.key || ""),
|
|
label: String(item?.label || item?.key || "\u041F\u043E\u043B\u0435"),
|
|
field_type: String(item?.field_type || "string").toLowerCase(),
|
|
value_text: item?.value_text == null ? "" : String(item.value_text),
|
|
value_file: item?.value_file || null,
|
|
pendingFile: null
|
|
})) : [];
|
|
setClientDataModal((prev) => ({ ...prev, loading: false, items }));
|
|
} catch (error) {
|
|
setClientDataModal((prev) => ({ ...prev, loading: false, error: error?.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0437\u0430\u043F\u0440\u043E\u0441 \u0434\u0430\u043D\u043D\u044B\u0445" }));
|
|
}
|
|
};
|
|
const updateClientDataItem = (localId, patch) => {
|
|
setClientDataModal((prev) => ({
|
|
...prev,
|
|
status: "",
|
|
error: "",
|
|
items: (prev.items || []).map((item) => item.localId === localId ? { ...item, ...patch || {} } : item)
|
|
}));
|
|
};
|
|
const submitClientDataModal = async (event) => {
|
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
if (!canFillRequestData || typeof onSaveRequestDataValues !== "function") return;
|
|
const currentMessageId = String(clientDataModal.messageId || "").trim();
|
|
if (!currentMessageId) return;
|
|
setClientDataModal((prev) => ({ ...prev, saving: true, status: "", error: "" }));
|
|
try {
|
|
const payloadItems = [];
|
|
for (const item of clientDataModal.items || []) {
|
|
const fieldType = String(item?.field_type || "string").toLowerCase();
|
|
if (fieldType === "file") {
|
|
let attachmentId = String(item?.value_text || "").trim();
|
|
if (item?.pendingFile) {
|
|
if (typeof onUploadRequestAttachment !== "function") {
|
|
throw new Error("\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0444\u0430\u0439\u043B\u0430 \u0434\u043B\u044F \u043F\u043E\u043B\u044F \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430");
|
|
}
|
|
const uploadResult = await onUploadRequestAttachment(item.pendingFile, {
|
|
source: "data_request",
|
|
message_id: currentMessageId,
|
|
key: String(item?.key || "")
|
|
});
|
|
attachmentId = String(
|
|
uploadResult && (uploadResult.attachment_id || uploadResult.id || uploadResult.value || uploadResult) || ""
|
|
).trim();
|
|
if (!attachmentId) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u043E\u043B\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430");
|
|
}
|
|
payloadItems.push({
|
|
id: String(item?.id || ""),
|
|
key: String(item?.key || ""),
|
|
attachment_id: attachmentId || "",
|
|
value_text: attachmentId || ""
|
|
});
|
|
continue;
|
|
}
|
|
payloadItems.push({
|
|
id: String(item?.id || ""),
|
|
key: String(item?.key || ""),
|
|
value_text: String(item?.value_text || "")
|
|
});
|
|
}
|
|
await onSaveRequestDataValues({
|
|
message_id: currentMessageId,
|
|
items: payloadItems
|
|
});
|
|
closeClientDataModal();
|
|
} catch (error) {
|
|
setClientDataModal((prev) => ({
|
|
...prev,
|
|
saving: false,
|
|
error: error?.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435"
|
|
}));
|
|
}
|
|
};
|
|
const handleRequestRowDragStart = (event, rowItem, rowLocked) => {
|
|
if (rowLocked || dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
setDraggedRequestRowId(String(rowItem.localId || ""));
|
|
setDragOverRequestRowId(String(rowItem.localId || ""));
|
|
try {
|
|
event.dataTransfer.effectAllowed = "move";
|
|
event.dataTransfer.setData("text/plain", String(rowItem.localId || ""));
|
|
} catch (_error) {
|
|
}
|
|
};
|
|
const handleRequestRowDragEnd = () => {
|
|
setDraggedRequestRowId("");
|
|
setDragOverRequestRowId("");
|
|
};
|
|
const appendStatusChangeFiles = (files) => {
|
|
const list = Array.isArray(files) ? files.filter(Boolean) : [];
|
|
if (!list.length) return;
|
|
setStatusChangeModal((prev) => {
|
|
const existing = Array.isArray(prev.files) ? prev.files : [];
|
|
const next = [...existing];
|
|
list.forEach((file) => {
|
|
const duplicate = next.some(
|
|
(item) => item && item.name === file.name && Number(item.size || 0) === Number(file.size || 0) && Number(item.lastModified || 0) === Number(file.lastModified || 0)
|
|
);
|
|
if (!duplicate) next.push(file);
|
|
});
|
|
return { ...prev, files: next };
|
|
});
|
|
};
|
|
const removeStatusChangeFile = (index) => {
|
|
setStatusChangeModal((prev) => {
|
|
const files = Array.isArray(prev.files) ? [...prev.files] : [];
|
|
files.splice(index, 1);
|
|
return { ...prev, files };
|
|
});
|
|
};
|
|
const submitStatusChange = async (event) => {
|
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
if (!row?.id || typeof onChangeStatus !== "function") return;
|
|
const nextStatus = String(statusChangeModal.statusCode || "").trim();
|
|
if (!nextStatus) {
|
|
setStatusChangeModal((prev) => ({ ...prev, error: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043D\u043E\u0432\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441" }));
|
|
return;
|
|
}
|
|
if (nextStatus === String(row?.status_code || "").trim()) {
|
|
setStatusChangeModal((prev) => ({ ...prev, error: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441, \u043E\u0442\u043B\u0438\u0447\u043D\u044B\u0439 \u043E\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E" }));
|
|
return;
|
|
}
|
|
setStatusChangeModal((prev) => ({ ...prev, saving: true, error: "" }));
|
|
try {
|
|
const localValue = String(statusChangeModal.importantDateAt || "").trim();
|
|
const importantDateIso = localValue ? new Date(localValue).toISOString() : "";
|
|
await onChangeStatus({
|
|
requestId: String(row.id),
|
|
statusCode: nextStatus,
|
|
importantDateAt: importantDateIso || null,
|
|
comment: statusChangeModal.comment || "",
|
|
files: statusChangeModal.files || []
|
|
});
|
|
closeStatusChangeModal();
|
|
} catch (error) {
|
|
setStatusChangeModal((prev) => ({ ...prev, saving: false, error: error.message || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441" }));
|
|
}
|
|
};
|
|
const chatTimelineItems = [];
|
|
let previousDate = "";
|
|
const timelineSource = [];
|
|
safeMessages.forEach((item) => {
|
|
timelineSource.push({
|
|
type: "message",
|
|
key: "msg-" + String(item?.id || Math.random()),
|
|
created_at: item?.created_at || null,
|
|
payload: item
|
|
});
|
|
});
|
|
safeAttachments.filter((item) => !String(item?.message_id || "").trim()).forEach((item) => {
|
|
timelineSource.push({
|
|
type: "file",
|
|
key: "file-" + String(item?.id || Math.random()),
|
|
created_at: item?.created_at || null,
|
|
payload: item
|
|
});
|
|
});
|
|
timelineSource.sort((a, b) => {
|
|
const aTime = new Date(a.created_at || 0).getTime();
|
|
const bTime = new Date(b.created_at || 0).getTime();
|
|
if (!Number.isFinite(aTime) && !Number.isFinite(bTime)) return 0;
|
|
if (!Number.isFinite(aTime)) return 1;
|
|
if (!Number.isFinite(bTime)) return -1;
|
|
if (aTime !== bTime) return aTime - bTime;
|
|
return String(a.key).localeCompare(String(b.key), "ru");
|
|
});
|
|
timelineSource.forEach((entry, index) => {
|
|
const dateLabel = fmtDateOnly(entry.created_at);
|
|
const normalizedDate = dateLabel && dateLabel !== "-" ? dateLabel : "\u0411\u0435\u0437 \u0434\u0430\u0442\u044B";
|
|
if (normalizedDate !== previousDate) {
|
|
chatTimelineItems.push({ type: "date", key: "date-" + normalizedDate + "-" + index, label: normalizedDate });
|
|
previousDate = normalizedDate;
|
|
}
|
|
chatTimelineItems.push(entry);
|
|
});
|
|
useEffect4(() => {
|
|
if (chatTab !== "chat") return;
|
|
const listNode = chatListRef.current;
|
|
if (!listNode) return;
|
|
const cursor = String(localActivityCursor || "");
|
|
if (!cursor || cursor === lastAutoScrollCursorRef.current) return;
|
|
lastAutoScrollCursorRef.current = cursor;
|
|
const raf = window.requestAnimationFrame(() => {
|
|
if (!chatListRef.current) return;
|
|
chatListRef.current.scrollTop = chatListRef.current.scrollHeight;
|
|
});
|
|
return () => window.cancelAnimationFrame(raf);
|
|
}, [chatTab, localActivityCursor]);
|
|
const baseRouteNodes = Array.isArray(statusRouteNodes) && statusRouteNodes.length ? statusRouteNodes : row?.status_code ? [{ code: row.status_code, name: String(row?.status_name || statusLabel(row.status_code) || row.status_code), state: "current", note: "\u0422\u0435\u043A\u0443\u0449\u0438\u0439 \u044D\u0442\u0430\u043F \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0438 \u0437\u0430\u044F\u0432\u043A\u0438" }] : [];
|
|
const upcomingImportantDate = useMemo2(() => {
|
|
const source = String(currentImportantDateAt || row?.important_date_at || "").trim();
|
|
if (!source) return "";
|
|
const timestamp = new Date(source).getTime();
|
|
if (!Number.isFinite(timestamp) || timestamp <= Date.now()) return "";
|
|
return new Date(timestamp).toISOString();
|
|
}, [currentImportantDateAt, row?.important_date_at]);
|
|
const routeNodes = useMemo2(() => {
|
|
if (viewerRoleCode !== "CLIENT" && viewerRoleCode !== "LAWYER" || !upcomingImportantDate) return baseRouteNodes;
|
|
if (!Array.isArray(baseRouteNodes) || !baseRouteNodes.length) {
|
|
return [
|
|
{
|
|
code: "__IMPORTANT_DATE__",
|
|
name: "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430",
|
|
state: "pending",
|
|
changed_at: upcomingImportantDate,
|
|
note: "\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C\u043D\u044B\u0439 \u0441\u0440\u043E\u043A"
|
|
}
|
|
];
|
|
}
|
|
const hasVirtualNode = baseRouteNodes.some((node) => String(node?.code || "").trim() === "__IMPORTANT_DATE__");
|
|
if (hasVirtualNode) return baseRouteNodes;
|
|
const currentIndex = baseRouteNodes.findIndex((node) => String(node?.state || "").trim().toLowerCase() === "current");
|
|
const virtualNode = {
|
|
code: "__IMPORTANT_DATE__",
|
|
name: "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430",
|
|
state: "pending",
|
|
changed_at: upcomingImportantDate,
|
|
note: "\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C\u043D\u044B\u0439 \u0441\u0440\u043E\u043A"
|
|
};
|
|
if (currentIndex < 0) return [...baseRouteNodes, virtualNode];
|
|
const next = [...baseRouteNodes];
|
|
next.splice(currentIndex + 1, 0, virtualNode);
|
|
return next;
|
|
}, [baseRouteNodes, upcomingImportantDate, viewerRoleCode]);
|
|
const routeNodesForDisplay = useMemo2(() => {
|
|
if (!Array.isArray(routeNodes) || !routeNodes.length) return [];
|
|
const important = [];
|
|
const current = [];
|
|
const completed = [];
|
|
const pending = [];
|
|
routeNodes.forEach((node) => {
|
|
const code = String(node?.code || "").trim();
|
|
const state = String(node?.state || "pending").trim().toLowerCase();
|
|
if (code === "__IMPORTANT_DATE__") {
|
|
important.push(node);
|
|
return;
|
|
}
|
|
if (state === "current") {
|
|
current.push(node);
|
|
return;
|
|
}
|
|
if (state === "completed") {
|
|
completed.push(node);
|
|
return;
|
|
}
|
|
pending.push(node);
|
|
});
|
|
return [...important, ...current, ...completed.reverse(), ...pending];
|
|
}, [routeNodes]);
|
|
const AttachmentPreviewModal = AttachmentPreviewModalComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const resolveMessageReceiptState = (payload) => {
|
|
const authorType = String(payload?.author_type || "").trim().toUpperCase();
|
|
const isClientAuthor = authorType === "CLIENT";
|
|
const deliveredAt = isClientAuthor ? payload?.delivered_to_staff_at : payload?.delivered_to_client_at;
|
|
const readAt = isClientAuthor ? payload?.read_by_staff_at : payload?.read_by_client_at;
|
|
if (readAt) return { state: "read", label: "\u041F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E" };
|
|
if (deliveredAt) return { state: "delivered", label: "\u0414\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E" };
|
|
return { state: "sent", label: "\u041E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E" };
|
|
};
|
|
const isOutgoingForViewer = (payload) => {
|
|
const authorType = String(payload?.author_type || "").trim().toUpperCase();
|
|
if (!authorType) return false;
|
|
if (viewerRoleCode === "CLIENT") return authorType === "CLIENT";
|
|
if (authorType === "CLIENT") return false;
|
|
const authorAdminUserId = String(payload?.author_admin_user_id || "").trim();
|
|
const currentViewerUserId = String(viewerUserId || "").trim();
|
|
if (authorAdminUserId && currentViewerUserId) return authorAdminUserId === currentViewerUserId;
|
|
const authorName = String(payload?.author_name || "").trim().toLowerCase();
|
|
const viewerName = String(viewerUserName || "").trim().toLowerCase();
|
|
const viewerEmail = String(viewerUserEmail || "").trim().toLowerCase();
|
|
if (authorName && (viewerName && authorName === viewerName || viewerEmail && authorName === viewerEmail)) {
|
|
return true;
|
|
}
|
|
return !viewerName && !viewerEmail ? authorType !== "CLIENT" : false;
|
|
};
|
|
const renderMessageMeta = (payload) => {
|
|
const timeLabel = fmtTimeOnly(payload?.created_at);
|
|
if (!isOutgoingForViewer(payload)) return /* @__PURE__ */ React.createElement("div", { className: "chat-message-time" }, timeLabel);
|
|
const receipt = resolveMessageReceiptState(payload);
|
|
return /* @__PURE__ */ React.createElement("div", { className: "chat-message-meta" }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-time" }, timeLabel), /* @__PURE__ */ React.createElement("span", { className: "chat-message-status " + receipt.state, title: receipt.label, "aria-label": receipt.label }, /* @__PURE__ */ React.createElement("span", { className: "chat-message-status-check first", "aria-hidden": "true" }, "\u2713"), receipt.state !== "sent" ? /* @__PURE__ */ React.createElement("span", { className: "chat-message-status-check second", "aria-hidden": "true" }, "\u2713") : null));
|
|
};
|
|
const renderRequestDataMessageItems = (payload) => {
|
|
const items = Array.isArray(payload?.request_data_items) ? payload.request_data_items : [];
|
|
const allFilled = Boolean(payload?.request_data_all_filled);
|
|
if (!items.length) return /* @__PURE__ */ React.createElement("p", { className: "chat-message-text" }, "\u0417\u0430\u043F\u0440\u043E\u0441");
|
|
if (allFilled) {
|
|
const fileOnly = items.length === 1 && String(items[0]?.field_type || "").toLowerCase() === "file";
|
|
return /* @__PURE__ */ React.createElement("p", { className: "chat-message-text chat-request-data-collapsed" }, fileOnly ? "\u0424\u0430\u0439\u043B" : "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D");
|
|
}
|
|
const visibleItems = items.slice(0, 7);
|
|
const hiddenCount = Math.max(0, items.length - visibleItems.length);
|
|
return /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-list" }, visibleItems.map((item, idx) => /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-item" + (item?.is_filled ? " filled" : ""), key: String(item?.id || idx) }, /* @__PURE__ */ React.createElement("span", { className: "chat-request-data-index" }, item?.is_filled ? /* @__PURE__ */ React.createElement("span", { className: "chat-request-data-check" }, "\u2713") : null, String(item?.index || idx + 1) + "."), /* @__PURE__ */ React.createElement("span", { className: "chat-request-data-label" }, String(item?.label_short || item?.label || "\u041F\u043E\u043B\u0435")))), hiddenCount > 0 ? /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-more" }, "... \u0435\u0449\u0435 ", hiddenCount) : null);
|
|
};
|
|
const resolveServiceMessageContent = (payload) => {
|
|
const messageKind = String(payload?.message_kind || "");
|
|
if (messageKind === "REQUEST_DATA") return null;
|
|
const bodyRaw = String(payload?.body || "").replace(/\r/g, "").trim();
|
|
if (!bodyRaw) return null;
|
|
const lines = bodyRaw.split("\n");
|
|
const firstLine = String(lines[0] || "").trim();
|
|
const restLines = lines.slice(1);
|
|
const normalizeDetail = (value) => String(value || "").trim();
|
|
const withTail = (firstDetail) => [normalizeDetail(firstDetail), ...restLines.map((line) => normalizeDetail(line)).filter(Boolean)].filter(Boolean).join("\n");
|
|
if (firstLine === "\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443" || firstLine.startsWith("\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443:")) {
|
|
return {
|
|
title: "\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443",
|
|
text: withTail(firstLine.startsWith("\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443:") ? firstLine.slice("\u0421\u0447\u0435\u0442 \u043D\u0430 \u043E\u043F\u043B\u0430\u0442\u0443:".length) : "")
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:") || firstLine.startsWith("\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430:")) {
|
|
const source = firstLine.startsWith("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:") ? firstLine : firstLine.slice("\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430:".length);
|
|
const detail = firstLine.startsWith("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:") ? source.slice("\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441:".length) : source;
|
|
return {
|
|
title: "\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441",
|
|
text: withTail(detail)
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442:")) {
|
|
return {
|
|
title: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442",
|
|
text: withTail(firstLine.slice("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442:".length))
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E:")) {
|
|
return {
|
|
title: "\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430",
|
|
text: withTail(firstLine.slice("\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E:".length))
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430:")) {
|
|
return {
|
|
title: "\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430",
|
|
text: withTail(firstLine.slice("\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430:".length))
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u044E\u0440\u0438\u0441\u0442\u0430:")) {
|
|
return {
|
|
title: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442",
|
|
text: withTail(firstLine.slice("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u044E\u0440\u0438\u0441\u0442\u0430:".length))
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435:")) {
|
|
return {
|
|
title: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u044E\u0440\u0438\u0441\u0442",
|
|
text: withTail(firstLine.slice("\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435:".length))
|
|
};
|
|
}
|
|
if (firstLine.startsWith("\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435:")) {
|
|
return {
|
|
title: "\u0421\u043C\u0435\u043D\u0430 \u044E\u0440\u0438\u0441\u0442\u0430",
|
|
text: withTail(firstLine.slice("\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435:".length))
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
const resolveStatusDisplayName = (code, explicitName) => {
|
|
const explicit = String(explicitName || "").trim();
|
|
if (explicit) return explicit;
|
|
const normalizedCode = String(code || "").trim();
|
|
if (!normalizedCode) return "-";
|
|
const optionName = String(statusByCode.get(normalizedCode)?.name || "").trim();
|
|
if (optionName) return optionName;
|
|
const legacyName = String(statusLabel(normalizedCode) || "").trim();
|
|
if (legacyName && legacyName !== normalizedCode) return legacyName;
|
|
return humanizeKey(normalizedCode);
|
|
};
|
|
const formatRequestDataValue = (item) => {
|
|
const type = String(item?.field_type || "string").toLowerCase();
|
|
if (type === "date") {
|
|
const text2 = String(item?.value_text || "").trim();
|
|
return text2 ? fmtDateOnly(text2) : "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E";
|
|
}
|
|
if (type === "file") {
|
|
const attachmentId = String(item?.value_text || "").trim();
|
|
const linkedAttachment = attachmentId ? attachmentById.get(attachmentId) : null;
|
|
const fileMeta = item?.value_file || (linkedAttachment ? {
|
|
attachment_id: linkedAttachment.id,
|
|
file_name: linkedAttachment.file_name,
|
|
mime_type: linkedAttachment.mime_type,
|
|
size_bytes: linkedAttachment.size_bytes,
|
|
download_url: linkedAttachment.download_url
|
|
} : null);
|
|
return fileMeta || null;
|
|
}
|
|
const text = String(item?.value_text || "").trim();
|
|
return text || "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E";
|
|
};
|
|
const currentStatusName = resolveStatusDisplayName(row?.status_code, row?.status_name || "");
|
|
const dataRequestProgress = useMemo2(() => {
|
|
const rows = Array.isArray(dataRequestModal.rows) ? dataRequestModal.rows : [];
|
|
const total = rows.length;
|
|
const filled = rows.filter((rowItem) => Boolean(rowItem?.is_filled || String(rowItem?.value_text || "").trim())).length;
|
|
return { total, filled };
|
|
}, [dataRequestModal.rows]);
|
|
return /* @__PURE__ */ React.createElement("div", { className: "block" }, /* @__PURE__ */ React.createElement("div", { className: "request-workspace-layout" }, /* @__PURE__ */ React.createElement("div", { className: "request-main-column" }, /* @__PURE__ */ React.createElement("div", { className: "block" }, /* @__PURE__ */ React.createElement("div", { className: "request-card-head" }, /* @__PURE__ */ React.createElement("h3", null, "\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430"), /* @__PURE__ */ React.createElement("div", { className: "request-card-head-actions" }, canRequestData ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn request-card-status-btn",
|
|
"data-tooltip": "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441",
|
|
"aria-label": "\u0421\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441",
|
|
onClick: () => openStatusChangeModal(),
|
|
disabled: loading || !row
|
|
},
|
|
"\u21C4"
|
|
) : null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn request-card-data-btn",
|
|
"data-tooltip": "\u0414\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u044F\u0432\u043A\u0438",
|
|
"aria-label": "\u0414\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u044F\u0432\u043A\u0438",
|
|
onClick: () => setRequestDataListOpen(true),
|
|
disabled: loading || !row
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M4 5h16v2H4V5Zm0 6h16v2H4v-2Zm0 6h10v2H4v-2Z", fill: "currentColor" }))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn request-card-finance-btn",
|
|
"data-tooltip": "\u0424\u0438\u043D\u0430\u043D\u0441\u044B \u0437\u0430\u044F\u0432\u043A\u0438",
|
|
"aria-label": "\u0424\u0438\u043D\u0430\u043D\u0441\u044B \u0437\u0430\u044F\u0432\u043A\u0438",
|
|
onClick: () => setFinanceOpen(true),
|
|
disabled: loading || !row
|
|
},
|
|
"$"
|
|
))), /* @__PURE__ */ React.createElement("div", { className: "request-card-head-spacer", "aria-hidden": "true" }), loading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...") : row ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-card-grid request-card-grid-compact" }, showTopicStatusInCard ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0422\u0435\u043C\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, String(row.topic_name || row.topic_code || "-"))), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u0430\u0442\u0443\u0441"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, currentStatusName))) : null, /* @__PURE__ */ React.createElement("div", { className: "request-field request-field-span-2 request-field-description" }, /* @__PURE__ */ React.createElement("div", { className: "request-field-head" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B"), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn request-field-expand-btn",
|
|
"data-tooltip": "\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435",
|
|
"aria-label": "\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435",
|
|
onClick: () => setDescriptionOpen(true)
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M4 9V4h5v2H6v3H4zm10-5h6v6h-2V6h-4V4zM4 15h2v3h3v2H4v-5zm14 3v-3h2v5h-5v-2h3z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
)), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, row.description ? String(row.description) : "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")), showContactsInCard ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041A\u043B\u0438\u0435\u043D\u0442"), /* @__PURE__ */ React.createElement(
|
|
"span",
|
|
{
|
|
className: "request-field-value" + (clientHasPhone ? " has-tooltip request-contact-value" : ""),
|
|
"data-tooltip": clientHasPhone ? clientPhone : void 0
|
|
},
|
|
clientLabel
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u042E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement(
|
|
"span",
|
|
{
|
|
className: "request-field-value" + (lawyerHasPhone ? " has-tooltip request-contact-value" : ""),
|
|
"data-tooltip": lawyerHasPhone ? lawyerPhone : void 0
|
|
},
|
|
lawyerLabel
|
|
))) : null, canSeeCreatedUpdatedInCard ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u043E\u0437\u0434\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row.created_at))), /* @__PURE__ */ React.createElement("div", { className: "request-field" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row.updated_at)))) : null), /* @__PURE__ */ React.createElement("div", { className: "request-status-route" }, /* @__PURE__ */ React.createElement("h4", null, "\u041C\u0430\u0440\u0448\u0440\u0443\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"), routeNodesForDisplay.length ? /* @__PURE__ */ React.createElement("ol", { className: "request-route-list", id: "request-status-route" }, routeNodesForDisplay.map((node, index) => {
|
|
const state = String(node?.state || "pending");
|
|
const code = String(node?.code || "").trim();
|
|
const rawName = String(node?.name || "").trim();
|
|
const name = resolveStatusDisplayName(code, rawName && rawName !== code ? rawName : "");
|
|
const note = String(node?.note || "").trim();
|
|
const isImportantDateNode = code === "__IMPORTANT_DATE__";
|
|
const changedAtSource = String(node?.changed_at || "").trim() || (isImportantDateNode ? String(currentImportantDateAt || row?.important_date_at || "").trim() : "");
|
|
const changedAt = changedAtSource ? fmtDate(changedAtSource) : "";
|
|
const className = "route-item " + (state === "current" ? "current" : state === "completed" ? "completed" : "pending") + (isImportantDateNode ? " important-date" : "");
|
|
return /* @__PURE__ */ React.createElement("li", { className, key: (node?.code || "node") + "-" + index }, /* @__PURE__ */ React.createElement("span", { className: "route-dot" }), /* @__PURE__ */ React.createElement("div", { className: "route-body" }, /* @__PURE__ */ React.createElement("b", null, name), isImportantDateNode ? /* @__PURE__ */ React.createElement("p", null, "\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C\u043D\u044B\u0439 \u0441\u0440\u043E\u043A: " + (changedAt || "-")) : /* @__PURE__ */ React.createElement(React.Fragment, null, note ? /* @__PURE__ */ React.createElement("p", null, note) : null, /* @__PURE__ */ React.createElement("div", { className: "muted route-time" }, "\u0414\u0430\u0442\u0430 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F: ", changedAt || "-"))));
|
|
})) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041C\u0430\u0440\u0448\u0440\u0443\u0442 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u0434\u043B\u044F \u0442\u0435\u043C\u044B \u043D\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D"))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435"))), /* @__PURE__ */ React.createElement("div", { className: "block request-chat-block" }, /* @__PURE__ */ React.createElement("div", { className: "request-chat-head" }, /* @__PURE__ */ React.createElement("h3", null, "\u041A\u043E\u043C\u043C\u0443\u043D\u0438\u043A\u0430\u0446\u0438\u044F"), /* @__PURE__ */ React.createElement("div", { className: "request-chat-tabs", role: "tablist", "aria-label": "\u041A\u043E\u043C\u043C\u0443\u043D\u0438\u043A\u0430\u0446\u0438\u044F" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
role: "tab",
|
|
"aria-selected": chatTab === "chat",
|
|
className: "tab-btn" + (chatTab === "chat" ? " active" : ""),
|
|
onClick: () => setChatTab("chat")
|
|
},
|
|
"\u0427\u0430\u0442"
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
role: "tab",
|
|
"aria-selected": chatTab === "files",
|
|
className: "tab-btn" + (chatTab === "files" ? " active" : ""),
|
|
onClick: () => setChatTab("files")
|
|
},
|
|
"\u0424\u0430\u0439\u043B\u044B" + (safeAttachments.length ? " (" + safeAttachments.length + ")" : "")
|
|
))), /* @__PURE__ */ React.createElement("div", { className: "request-chat-live-row", "aria-live": "polite" }, /* @__PURE__ */ React.createElement("span", { className: "chat-live-dot" + (liveMode === "degraded" ? " degraded" : "") }), /* @__PURE__ */ React.createElement("span", { className: "request-chat-live-text" }, typingHintText || (liveMode === "degraded" ? "\u0421\u0432\u044F\u0437\u044C \u043D\u0435\u0441\u0442\u0430\u0431\u0438\u043B\u044C\u043D\u0430, \u0432\u043A\u043B\u044E\u0447\u0435\u043D backoff" : "\u041E\u043D\u043B\u0430\u0439\u043D"))), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: idMap.fileInput,
|
|
ref: fileInputRef,
|
|
type: "file",
|
|
multiple: true,
|
|
onChange: onInputFiles,
|
|
disabled: loading || fileUploading,
|
|
style: { position: "absolute", width: "1px", height: "1px", opacity: 0, pointerEvents: "none" }
|
|
}
|
|
), chatTab === "chat" ? /* @__PURE__ */ React.createElement(React.Fragment, null, messagesHasMore ? /* @__PURE__ */ React.createElement("div", { className: "request-chat-history-actions" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "btn secondary",
|
|
onClick: onLoadOlderMessages,
|
|
disabled: loading || fileUploading || messagesLoadingMore
|
|
},
|
|
messagesLoadingMore ? "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0438\u0441\u0442\u043E\u0440\u0438\u0438..." : "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F"
|
|
)) : null, /* @__PURE__ */ React.createElement("ul", { className: "simple-list request-modal-list request-chat-list", id: idMap.messagesList, ref: chatListRef }, chatTimelineItems.length ? chatTimelineItems.map(
|
|
(entry) => entry.type === "date" ? /* @__PURE__ */ React.createElement("li", { key: entry.key, className: "chat-date-divider" }, /* @__PURE__ */ React.createElement("span", null, entry.label)) : entry.type === "file" ? /* @__PURE__ */ React.createElement(
|
|
"li",
|
|
{
|
|
key: entry.key,
|
|
className: "chat-message " + (String(entry.payload?.responsible || "").toUpperCase().includes("\u041A\u041B\u0418\u0415\u041D\u0422") ? "incoming" : "outgoing")
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "chat-message-author" }, String(entry.payload?.responsible || "\u0421\u0438\u0441\u0442\u0435\u043C\u0430")),
|
|
/* @__PURE__ */ React.createElement("div", { className: "chat-message-bubble" }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-files" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "chat-message-file-chip",
|
|
onClick: () => openAttachmentFromMessage(entry.payload),
|
|
title: String(entry.payload?.file_name || "\u0424\u0430\u0439\u043B")
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"),
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(entry.payload?.file_name || "\u0424\u0430\u0439\u043B"))
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "chat-message-time" }, fmtTimeOnly(entry.payload?.created_at)))
|
|
) : (() => {
|
|
const messageKind = String(entry.payload?.message_kind || "");
|
|
const isRequestDataMessage = messageKind === "REQUEST_DATA";
|
|
const serviceMessageContent = resolveServiceMessageContent(entry.payload);
|
|
const requestDataInteractive = isRequestDataMessage && (canRequestData || canFillRequestData);
|
|
const bubbleClass = "chat-message-bubble" + (isRequestDataMessage ? " chat-request-data-bubble" : "") + (entry.payload?.request_data_all_filled ? " all-filled" : "") + (isRequestDataMessage && canFillRequestData ? " request-data-message-btn" : "");
|
|
const isOutgoing = isOutgoingForViewer(entry.payload);
|
|
const itemClass = "chat-message " + (isOutgoing ? "outgoing" : "incoming") + (isRequestDataMessage && canFillRequestData ? " request-data-item" + (entry.payload?.request_data_all_filled ? " done" : "") : "");
|
|
return /* @__PURE__ */ React.createElement("li", { key: entry.key, className: itemClass }, /* @__PURE__ */ React.createElement("div", { className: "chat-message-author" }, String(entry.payload?.author_name || entry.payload?.author_type || "\u0421\u0438\u0441\u0442\u0435\u043C\u0430")), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: bubbleClass,
|
|
onClick: requestDataInteractive ? () => canRequestData ? openEditDataRequestModal(String(entry.payload?.id || "")) : openClientDataRequestModal(String(entry.payload?.id || "")) : void 0,
|
|
role: requestDataInteractive ? "button" : void 0,
|
|
tabIndex: requestDataInteractive ? 0 : void 0,
|
|
onKeyDown: requestDataInteractive ? (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
if (canRequestData) openEditDataRequestModal(String(entry.payload?.id || ""));
|
|
else openClientDataRequestModal(String(entry.payload?.id || ""));
|
|
}
|
|
} : void 0
|
|
},
|
|
String(entry.payload?.message_kind || "") === "REQUEST_DATA" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "chat-request-data-head" }, "\u0417\u0430\u043F\u0440\u043E\u0441"), renderRequestDataMessageItems(entry.payload)) : /* @__PURE__ */ React.createElement(React.Fragment, null, serviceMessageContent?.title ? /* @__PURE__ */ React.createElement("div", { className: "chat-service-head" }, serviceMessageContent.title) : null, serviceMessageContent ? serviceMessageContent.text ? /* @__PURE__ */ React.createElement("p", { className: "chat-message-text" }, serviceMessageContent.text) : null : /* @__PURE__ */ React.createElement("p", { className: "chat-message-text" }, entry.payload?.body_loaded === false ? "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F..." : String(entry.payload?.body || ""))),
|
|
(() => {
|
|
if (String(entry.payload?.message_kind || "") === "REQUEST_DATA") return null;
|
|
const messageId = String(entry.payload?.id || "").trim();
|
|
if (!messageId) return null;
|
|
const messageFiles = attachmentsByMessageId.get(messageId) || [];
|
|
if (!messageFiles.length) return null;
|
|
return /* @__PURE__ */ React.createElement("div", { className: "chat-message-files" }, messageFiles.map((file) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
key: String(file.id),
|
|
className: "chat-message-file-chip",
|
|
onClick: () => openAttachmentFromMessage(file),
|
|
title: String(file.file_name || "\u0424\u0430\u0439\u043B")
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"),
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(file.file_name || "\u0424\u0430\u0439\u043B"))
|
|
)));
|
|
})(),
|
|
renderMessageMeta(entry.payload)
|
|
));
|
|
})()
|
|
) : /* @__PURE__ */ React.createElement("li", { className: "muted chat-empty-state" }, "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u043D\u0435\u0442")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit: onSendMessage }, /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "field request-chat-composer-dropzone" + (dropActive ? " drag-active" : ""),
|
|
onDragOver: (event) => {
|
|
event.preventDefault();
|
|
setDropActive(true);
|
|
},
|
|
onDragLeave: (event) => {
|
|
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
setDropActive(false);
|
|
},
|
|
onDrop: onDropFiles
|
|
},
|
|
/* @__PURE__ */ React.createElement("label", { htmlFor: idMap.messageBody }, "\u041D\u043E\u0432\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435"),
|
|
/* @__PURE__ */ React.createElement(
|
|
"textarea",
|
|
{
|
|
id: idMap.messageBody,
|
|
placeholder: messagePlaceholder,
|
|
value: messageDraft,
|
|
onChange: onMessageChange,
|
|
onFocus: () => setComposerFocused(true),
|
|
onBlur: () => setComposerFocused(false),
|
|
disabled: loading || fileUploading
|
|
}
|
|
),
|
|
/* @__PURE__ */ React.createElement("div", { className: "request-drop-hint muted" }, "\u041F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B \u0441\u044E\u0434\u0430 \u0438\u043B\u0438 \u043F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u0435 \u0441\u043A\u0440\u0435\u043F\u043A\u043E\u0439")
|
|
), hasPendingFiles ? /* @__PURE__ */ React.createElement("div", { className: "request-pending-files" }, pendingFiles.map((file, index) => /* @__PURE__ */ React.createElement("div", { className: "pending-file-chip", key: (file.name || "file") + "-" + String(file.lastModified || index) }, /* @__PURE__ */ React.createElement("span", { className: "pending-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), /* @__PURE__ */ React.createElement("span", { className: "pending-file-name" }, file.name), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "pending-file-remove",
|
|
"aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0430\u0439\u043B " + file.name,
|
|
onClick: () => onRemoveSelectedFile(index)
|
|
},
|
|
"\xD7"
|
|
))), /* @__PURE__ */ React.createElement("button", { type: "button", className: "btn secondary btn-sm", onClick: onClearSelectedFiles }, "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F")) : null, /* @__PURE__ */ React.createElement("div", { className: "request-chat-composer-actions" }, canRequestData ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary btn-sm",
|
|
type: "button",
|
|
onClick: openCreateDataRequestModal,
|
|
disabled: loading || fileUploading
|
|
},
|
|
"\u0417\u0430\u043F\u0440\u043E\u0441\u0438\u0442\u044C"
|
|
) : null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "icon-btn file-action-btn composer-attach-btn",
|
|
type: "button",
|
|
"data-tooltip": "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B",
|
|
"aria-label": "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B",
|
|
onClick: () => fileInputRef.current?.click(),
|
|
disabled: loading || fileUploading
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M8.6 13.8 15 7.4a3 3 0 0 1 4.2 4.2l-8.1 8.1a5 5 0 1 1-7.1-7.1l8.6-8.6a1 1 0 0 1 1.4 1.4l-8.6 8.6a3 3 0 1 0 4.2 4.2l8.1-8.1a1 1 0 0 0-1.4-1.4l-6.4 6.4a1 1 0 0 1-1.4-1.4z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn",
|
|
id: idMap.sendButton,
|
|
type: "submit",
|
|
disabled: loading || fileUploading || !canSubmit
|
|
},
|
|
"\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C"
|
|
)))) : /* @__PURE__ */ React.createElement("div", { className: "request-files-tab" }, /* @__PURE__ */ React.createElement("ul", { className: "simple-list request-modal-list", id: idMap.filesList }, safeAttachments.length ? safeAttachments.map((item) => /* @__PURE__ */ React.createElement("li", { key: String(item.id) }, /* @__PURE__ */ React.createElement("div", null, item.file_name || "\u0424\u0430\u0439\u043B"), /* @__PURE__ */ React.createElement("div", { className: "muted request-modal-item-meta" }, String(item.mime_type || "application/octet-stream") + " \u2022 " + fmtBytes(item.size_bytes) + " \u2022 " + fmtDate(item.created_at)), /* @__PURE__ */ React.createElement("div", { className: "request-file-actions" }, item.download_url && detectAttachmentPreviewKind(item.file_name, item.mime_type) !== "none" ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "icon-btn file-action-btn",
|
|
type: "button",
|
|
"data-tooltip": "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440",
|
|
onClick: () => openPreview(item),
|
|
"aria-label": "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440"
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M12 5C6.8 5 3 9.2 2 12c1 2.8 4.8 7 10 7s9-4.2 10-7c-1-2.8-4.8-7-10-7zm0 11a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2.2A1.8 1.8 0 1 0 12 10a1.8 1.8 0 0 0 0 3.8z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
) : null, item.download_url ? /* @__PURE__ */ React.createElement(
|
|
"a",
|
|
{
|
|
className: "icon-btn file-action-btn request-file-link-icon",
|
|
"data-tooltip": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C",
|
|
"aria-label": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C: " + String(item.file_name || "\u0444\u0430\u0439\u043B"),
|
|
href: item.download_url,
|
|
target: "_blank",
|
|
rel: "noreferrer"
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M12 3a1 1 0 0 1 1 1v8.17l2.58-2.58a1 1 0 1 1 1.42 1.42l-4.3 4.3a1 1 0 0 1-1.4 0l-4.3-4.3a1 1 0 0 1 1.42-1.42L11 12.17V4a1 1 0 0 1 1-1zm-7 14a1 1 0 0 1 1 1v1h12v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
) : null))) : /* @__PURE__ */ React.createElement("li", { className: "muted" }, "\u0424\u0430\u0439\u043B\u043E\u0432 \u043F\u043E\u043A\u0430 \u043D\u0435\u0442")), /* @__PURE__ */ React.createElement("div", { className: "request-files-tab-actions" }, /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439: " + String(safeMessages.length) + " \u2022 \u041E\u0431\u0449\u0438\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0444\u0430\u0439\u043B\u043E\u0432: " + fmtBytes(totalFilesBytes)))))), StatusLine ? /* @__PURE__ */ React.createElement(StatusLine, { status }) : null, AttachmentPreviewModal ? /* @__PURE__ */ React.createElement(
|
|
AttachmentPreviewModal,
|
|
{
|
|
open: preview.open,
|
|
title: "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0444\u0430\u0439\u043B\u0430",
|
|
url: preview.url,
|
|
fileName: preview.fileName,
|
|
mimeType: preview.mimeType,
|
|
onClose: closePreview
|
|
}
|
|
) : null, /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "overlay" + (clientDataModal.open ? " open" : ""),
|
|
onClick: closeClientDataModal,
|
|
"aria-hidden": clientDataModal.open ? "false" : "true",
|
|
id: idMap.dataRequestOverlay
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "modal request-data-summary-modal data-request-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0417\u0430\u043F\u0440\u043E\u0441 \u0434\u0430\u043D\u043D\u044B\u0445"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, row?.track_number ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E \u0437\u0430\u043F\u0440\u043E\u0441\u0443 \u044E\u0440\u0438\u0441\u0442\u0430")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeClientDataModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit: submitClientDataModal }, /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-list", id: idMap.dataRequestItems }, clientDataModal.loading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...") : (clientDataModal.items || []).length ? (clientDataModal.items || []).map((item, index) => {
|
|
const fieldType = String(item?.field_type || "string").toLowerCase();
|
|
const fileMeta = item?.value_file;
|
|
return /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-row", key: String(item.localId || index) }, /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-label" }, String(index + 1) + ". " + String(item?.label || item?.key || "\u041F\u043E\u043B\u0435")), /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-value" }, fieldType === "text" ? /* @__PURE__ */ React.createElement(
|
|
"textarea",
|
|
{
|
|
value: String(item?.value_text || ""),
|
|
onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }),
|
|
rows: 3,
|
|
disabled: clientDataModal.saving || clientDataModal.loading
|
|
}
|
|
) : fieldType === "date" ? /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
type: "date",
|
|
value: String(item?.value_text || "").slice(0, 10),
|
|
onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }),
|
|
disabled: clientDataModal.saving || clientDataModal.loading
|
|
}
|
|
) : fieldType === "number" ? /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
type: "number",
|
|
step: "any",
|
|
value: String(item?.value_text || ""),
|
|
onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }),
|
|
disabled: clientDataModal.saving || clientDataModal.loading
|
|
}
|
|
) : fieldType === "file" ? /* @__PURE__ */ React.createElement("div", { className: "stack" }, fileMeta && fileMeta.download_url ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "chat-message-file-chip",
|
|
onClick: () => openAttachmentFromMessage(fileMeta)
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"),
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(fileMeta.file_name || "\u0424\u0430\u0439\u043B"))
|
|
) : null, /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
type: "file",
|
|
onChange: (event) => updateClientDataItem(item.localId, {
|
|
pendingFile: event.target.files && event.target.files[0] ? event.target.files[0] : null
|
|
}),
|
|
disabled: clientDataModal.saving || clientDataModal.loading
|
|
}
|
|
), item?.pendingFile ? /* @__PURE__ */ React.createElement("span", { className: "muted" }, String(item.pendingFile.name || "")) : null) : /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
type: "text",
|
|
value: String(item?.value_text || ""),
|
|
onChange: (event) => updateClientDataItem(item.localId, { value_text: event.target.value }),
|
|
disabled: clientDataModal.saving || clientDataModal.loading
|
|
}
|
|
)));
|
|
}) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u041D\u0435\u0442 \u043F\u043E\u043B\u0435\u0439 \u0434\u043B\u044F \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F.")), clientDataModal.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, clientDataModal.error) : null, /* @__PURE__ */ React.createElement("div", { className: "request-data-status" + (clientDataModal.status ? " ok" : ""), id: idMap.dataRequestStatus }, clientDataModal.status || ""), /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "submit",
|
|
className: "btn btn-sm request-data-submit-btn",
|
|
id: idMap.dataRequestSave,
|
|
disabled: clientDataModal.loading || clientDataModal.saving
|
|
},
|
|
clientDataModal.saving ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435..." : "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"
|
|
))))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "overlay" + (statusChangeModal.open ? " open" : ""),
|
|
onClick: closeStatusChangeModal,
|
|
"aria-hidden": statusChangeModal.open ? "false" : "true"
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "modal request-status-change-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, row?.track_number ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441 \u0438 \u0432\u0430\u0436\u043D\u0443\u044E \u0434\u0430\u0442\u0443")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeStatusChangeModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
ref: statusChangeFileInputRef,
|
|
type: "file",
|
|
multiple: true,
|
|
onChange: (event) => {
|
|
appendStatusChangeFiles(Array.from(event.target && event.target.files || []));
|
|
event.target.value = "";
|
|
},
|
|
style: { position: "absolute", width: "1px", height: "1px", opacity: 0, pointerEvents: "none" }
|
|
}
|
|
), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit: submitStatusChange }, /* @__PURE__ */ React.createElement("div", { className: "request-status-change-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "status-change-next-status" }, "\u041D\u043E\u0432\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441"), /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "status-change-next-status",
|
|
value: statusChangeModal.statusCode,
|
|
onChange: (nextValue) => setStatusChangeModal((prev) => ({ ...prev, statusCode: nextValue, error: "" })),
|
|
disabled: statusChangeModal.saving || loading,
|
|
options: [
|
|
{ value: "", label: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441" },
|
|
...statusOptions.filter((item) => item.code !== String(row?.status_code || "").trim()).filter(
|
|
(item) => Array.isArray(statusChangeModal.allowedStatusCodes) && statusChangeModal.allowedStatusCodes.length ? statusChangeModal.allowedStatusCodes.includes(item.code) : true
|
|
).map((item) => ({
|
|
value: item.code,
|
|
label: item.name + (item.groupName ? " \u2022 " + item.groupName : "")
|
|
}))
|
|
],
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441"
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "status-change-important-date" }, "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430 (\u0434\u0435\u0434\u043B\u0430\u0439\u043D)"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "status-change-important-date",
|
|
type: "datetime-local",
|
|
value: statusChangeModal.importantDateAt,
|
|
onChange: (event) => setStatusChangeModal((prev) => ({ ...prev, importantDateAt: event.target.value, error: "" })),
|
|
disabled: statusChangeModal.saving || loading
|
|
}
|
|
))), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "status-change-comment" }, "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u043A \u0441\u043C\u0435\u043D\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u0430"), /* @__PURE__ */ React.createElement(
|
|
"textarea",
|
|
{
|
|
id: "status-change-comment",
|
|
placeholder: "\u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u044E \u0438 \u0447\u0430\u0442 (\u0435\u0441\u043B\u0438 \u0443\u043A\u0430\u0437\u0430\u043D)",
|
|
value: statusChangeModal.comment,
|
|
onChange: (event) => setStatusChangeModal((prev) => ({ ...prev, comment: event.target.value })),
|
|
disabled: statusChangeModal.saving || loading
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "request-status-change-files" }, /* @__PURE__ */ React.createElement("div", { className: "request-status-change-files-head" }, /* @__PURE__ */ React.createElement("b", null, "\u0412\u043B\u043E\u0436\u0435\u043D\u0438\u044F"), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn file-action-btn",
|
|
"data-tooltip": "\u041F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B",
|
|
onClick: () => statusChangeFileInputRef.current?.click(),
|
|
disabled: statusChangeModal.saving || loading
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M8.6 13.8 15 7.4a3 3 0 0 1 4.2 4.2l-8.1 8.1a5 5 0 1 1-7.1-7.1l8.6-8.6a1 1 0 0 1 1.4 1.4l-8.6 8.6a3 3 0 1 0 4.2 4.2l8.1-8.1a1 1 0 0 0-1.4-1.4l-6.4 6.4a1 1 0 0 1-1.4-1.4z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
)), Array.isArray(statusChangeModal.files) && statusChangeModal.files.length ? /* @__PURE__ */ React.createElement("div", { className: "request-pending-files" }, statusChangeModal.files.map((file, index) => /* @__PURE__ */ React.createElement("div", { className: "pending-file-chip", key: (file.name || "file") + "-" + String(file.lastModified || index) }, /* @__PURE__ */ React.createElement("span", { className: "pending-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), /* @__PURE__ */ React.createElement("span", { className: "pending-file-name" }, file.name), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "pending-file-remove",
|
|
"aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0430\u0439\u043B " + file.name,
|
|
onClick: () => removeStatusChangeFile(index)
|
|
},
|
|
"\xD7"
|
|
)))) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0424\u0430\u0439\u043B\u044B \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B")), /* @__PURE__ */ React.createElement("div", { className: "request-status-history-block" }, /* @__PURE__ */ React.createElement("div", { className: "request-status-history-head" }, /* @__PURE__ */ React.createElement("b", null, "\u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432"), /* @__PURE__ */ React.createElement("span", { className: "muted" }, safeStatusHistory.length ? String(safeStatusHistory.length) + " \u0437\u0430\u043F\u0438\u0441\u0435\u0439" : "\u041D\u0435\u0442 \u0437\u0430\u043F\u0438\u0441\u0435\u0439")), /* @__PURE__ */ React.createElement("ol", { className: "request-route-list request-status-history-list" }, safeStatusHistory.length ? safeStatusHistory.map((item, index) => {
|
|
const statusCode = String(item?.to_status || "");
|
|
const statusMeta = statusByCode.get(statusCode);
|
|
const itemClass = "route-item request-status-history-route-item " + (index === 0 ? "current" : "completed");
|
|
return /* @__PURE__ */ React.createElement("li", { key: String(item?.id || index), className: itemClass }, /* @__PURE__ */ React.createElement("span", { className: "route-dot" }), /* @__PURE__ */ React.createElement("div", { className: "route-body" }, /* @__PURE__ */ React.createElement("div", { className: "request-status-history-row" }, /* @__PURE__ */ React.createElement("b", null, resolveStatusDisplayName(statusCode, item?.to_status_name || statusMeta?.name || "")), statusMeta?.isTerminal ? /* @__PURE__ */ React.createElement("span", { className: "request-status-history-chip" }, "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439") : null), /* @__PURE__ */ React.createElement("div", { className: "muted route-time" }, fmtShortDateTime(item?.changed_at)), /* @__PURE__ */ React.createElement("div", { className: "request-status-history-meta" }, /* @__PURE__ */ React.createElement("span", null, "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430: " + fmtShortDateTime(item?.important_date_at)), /* @__PURE__ */ React.createElement("span", null, "\u0414\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C: " + formatDuration(item?.duration_seconds))), String(item?.comment || "").trim() ? /* @__PURE__ */ React.createElement("div", { className: "request-status-history-comment" }, String(item.comment)) : null));
|
|
}) : /* @__PURE__ */ React.createElement("li", { className: "muted" }, "\u0418\u0441\u0442\u043E\u0440\u0438\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432 \u043F\u043E\u043A\u0430 \u043F\u0443\u0441\u0442\u0430\u044F"))), statusChangeModal.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, statusChangeModal.error) : null, /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "submit",
|
|
className: "btn btn-sm request-data-submit-btn",
|
|
disabled: statusChangeModal.saving || loading
|
|
},
|
|
statusChangeModal.saving ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435..." : "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C"
|
|
))))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "overlay" + (financeOpen ? " open" : ""),
|
|
onClick: closeFinanceModal,
|
|
"aria-hidden": financeOpen ? "false" : "true"
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "modal request-finance-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0424\u0438\u043D\u0430\u043D\u0441\u044B \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, row?.track_number ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0414\u0430\u043D\u043D\u044B\u0435 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435")), /* @__PURE__ */ React.createElement("div", { className: "modal-head-actions" }, typeof onIssueInvoice === "function" ? !financeIssueForm.open ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "btn secondary btn-sm",
|
|
onClick: openFinanceIssueForm,
|
|
disabled: loading || !row
|
|
},
|
|
"\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0447\u0435\u0442"
|
|
) : /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "btn secondary btn-sm",
|
|
onClick: closeFinanceIssueForm,
|
|
disabled: financeIssueForm.saving
|
|
},
|
|
"\u0421\u043A\u0440\u044B\u0442\u044C \u0444\u043E\u0440\u043C\u0443"
|
|
) : null, /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeFinanceModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7"))), /* @__PURE__ */ React.createElement("div", { className: "request-finance-layout" }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-summary" }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-summary-card accent" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C"), /* @__PURE__ */ React.createElement("span", { className: "request-finance-summary-value" }, fmtAmount(finance?.request_cost ?? row?.request_cost))), /* @__PURE__ */ React.createElement("div", { className: "request-finance-summary-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E"), /* @__PURE__ */ React.createElement("span", { className: "request-finance-summary-value" }, fmtAmount(finance?.paid_total))), /* @__PURE__ */ React.createElement("div", { className: "request-finance-summary-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0414\u0430\u0442\u0430 \u043E\u043F\u043B\u0430\u0442\u044B"), /* @__PURE__ */ React.createElement("span", { className: "request-finance-summary-value" }, fmtShortDateTime(finance?.last_paid_at ?? row?.paid_at))), canSeeRate ? /* @__PURE__ */ React.createElement("div", { className: "request-finance-summary-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u0430\u0432\u043A\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-finance-summary-value" }, fmtAmount(finance?.effective_rate ?? row?.effective_rate))) : null), typeof onIssueInvoice === "function" && financeIssueForm.open ? /* @__PURE__ */ React.createElement("div", { className: "request-finance-actions" }, /* @__PURE__ */ React.createElement("form", { className: "stack request-finance-issue-form", onSubmit: submitFinanceIssueForm }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-issue-head" }, /* @__PURE__ */ React.createElement("h4", null, "\u041D\u043E\u0432\u044B\u0439 \u0441\u0447\u0435\u0442"), /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u0441\u0443\u043C\u043C\u0443 \u0438 \u0440\u0435\u043A\u0432\u0438\u0437\u0438\u0442\u044B \u043F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A\u0430")), /* @__PURE__ */ React.createElement("div", { className: "request-finance-issue-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-finance-invoice-amount" }, "\u0421\u0443\u043C\u043C\u0430"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "request-finance-invoice-amount",
|
|
type: "number",
|
|
min: "0.01",
|
|
step: "0.01",
|
|
value: financeIssueForm.amount,
|
|
onChange: (event) => setFinanceIssueForm((prev) => ({ ...prev, amount: event.target.value, error: "" })),
|
|
disabled: financeIssueForm.saving || loading,
|
|
placeholder: "0.00"
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-finance-invoice-payer" }, "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "request-finance-invoice-payer",
|
|
type: "text",
|
|
value: financeIssueForm.payerDisplayName,
|
|
onChange: (event) => setFinanceIssueForm((prev) => ({ ...prev, payerDisplayName: event.target.value, error: "" })),
|
|
disabled: financeIssueForm.saving || loading,
|
|
placeholder: "\u0424\u0418\u041E / \u043A\u043E\u043C\u043F\u0430\u043D\u0438\u044F"
|
|
}
|
|
))), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-finance-invoice-service" }, "\u0423\u0441\u043B\u0443\u0433\u0430"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "request-finance-invoice-service",
|
|
type: "text",
|
|
value: financeIssueForm.serviceDescription,
|
|
onChange: (event) => setFinanceIssueForm((prev) => ({ ...prev, serviceDescription: event.target.value, error: "" })),
|
|
disabled: financeIssueForm.saving || loading,
|
|
placeholder: "\u042E\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0443\u0441\u043B\u0443\u0433\u0438"
|
|
}
|
|
)), financeIssueForm.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, financeIssueForm.error) : null, /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right request-finance-actions-inline" }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "btn secondary btn-sm", onClick: closeFinanceIssueForm, disabled: financeIssueForm.saving }, "\u041E\u0442\u043C\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("button", { type: "submit", className: "btn btn-sm", disabled: financeIssueForm.saving || loading }, financeIssueForm.saving ? "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435..." : "\u0412\u044B\u0441\u0442\u0430\u0432\u0438\u0442\u044C")))) : null), /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoices" }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoices-head" }, /* @__PURE__ */ React.createElement("h4", null, "\u0421\u0447\u0435\u0442\u0430"), /* @__PURE__ */ React.createElement("span", { className: "muted" }, safeInvoices.length ? String(safeInvoices.length) + " \u0448\u0442." : "\u041D\u0435\u0442 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u0447\u0435\u0442\u043E\u0432")), safeInvoices.length ? /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-list" }, safeInvoices.map((item) => /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-row", key: String(item?.id || item?.invoice_number || item?.issued_at || "-") }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-meta" }, /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-number" }, /* @__PURE__ */ React.createElement("code", null, String(item?.invoice_number || "-"))), /* @__PURE__ */ React.createElement("div", { className: "request-finance-invoice-details" }, /* @__PURE__ */ React.createElement("span", null, invoiceStatusLabel(item?.status)), /* @__PURE__ */ React.createElement("span", null, fmtAmount(item?.amount) + " " + String(item?.currency || "RUB")), /* @__PURE__ */ React.createElement("span", null, "\u0421\u043E\u0437\u0434\u0430\u043D: " + fmtDate(item?.issued_at)), /* @__PURE__ */ React.createElement("span", null, "\u041E\u043F\u043B\u0430\u0447\u0435\u043D: " + fmtDate(item?.paid_at)))), typeof onDownloadInvoicePdf === "function" ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn request-finance-invoice-download-btn",
|
|
onClick: () => onDownloadInvoicePdf(item),
|
|
disabled: loading,
|
|
"aria-label": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0441\u0447\u0435\u0442 PDF",
|
|
"data-tooltip": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C PDF"
|
|
},
|
|
"\u2B07"
|
|
) : null))) : /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-empty" }, "\u0421\u0447\u0435\u0442\u0430 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u044F\u043B\u0438\u0441\u044C")))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "overlay" + (descriptionOpen ? " open" : ""),
|
|
onClick: () => setDescriptionOpen(false),
|
|
"aria-hidden": descriptionOpen ? "false" : "true"
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "modal request-description-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, row?.track_number ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0417\u0430\u044F\u0432\u043A\u0430"), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-headline" }, /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, String(row?.topic_name || row?.topic_code || "\u0422\u0435\u043C\u0430 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430")), /* @__PURE__ */ React.createElement("span", { className: "request-description-status-chip" }, currentStatusName))), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: () => setDescriptionOpen(false), "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-body" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-main" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-title" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B")), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-text" }, row?.description ? String(row.description) : "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-side" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-meta-wrap" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-meta" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041A\u043B\u0438\u0435\u043D\u0442"), /* @__PURE__ */ React.createElement(
|
|
"span",
|
|
{
|
|
className: "request-field-value" + (clientHasPhone ? " has-tooltip request-contact-value" : ""),
|
|
"data-tooltip": clientHasPhone ? clientPhone : void 0
|
|
},
|
|
clientLabel
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u042E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement(
|
|
"span",
|
|
{
|
|
className: "request-field-value" + (lawyerHasPhone ? " has-tooltip request-contact-value" : ""),
|
|
"data-tooltip": lawyerHasPhone ? lawyerPhone : void 0
|
|
},
|
|
lawyerLabel
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u043E\u0437\u0434\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row?.created_at))), /* @__PURE__ */ React.createElement("div", { className: "request-description-meta-item" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row?.updated_at))))), /* @__PURE__ */ React.createElement("div", { className: "request-description-modal-facts" }, /* @__PURE__ */ React.createElement("div", { className: "request-description-fact-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, row?.track_number ? String(row.track_number) : "\u2014")), /* @__PURE__ */ React.createElement("div", { className: "request-description-fact-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0422\u0435\u043C\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, String(row?.topic_name || row?.topic_code || "\u041D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430"))), /* @__PURE__ */ React.createElement("div", { className: "request-description-fact-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0421\u0442\u0430\u0442\u0443\u0441"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, currentStatusName)), /* @__PURE__ */ React.createElement("div", { className: "request-description-fact-card" }, /* @__PURE__ */ React.createElement("span", { className: "request-field-label" }, "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430"), /* @__PURE__ */ React.createElement("span", { className: "request-field-value" }, fmtShortDateTime(row?.important_date_at)))))))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "overlay" + (dataRequestModal.open ? " open" : ""),
|
|
onClick: closeDataRequestModal,
|
|
"aria-hidden": dataRequestModal.open ? "false" : "true"
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "modal request-data-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, dataRequestModal.messageId ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0434\u0430\u043D\u043D\u044B\u0445" : "\u0417\u0430\u043F\u0440\u043E\u0441 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, row?.track_number ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u043E\u043B\u044F \u0434\u043B\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: closeDataRequestModal, "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "stack" }, /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-data-request-template-select" }, "\u0428\u0430\u0431\u043B\u043E\u043D \u0437\u0430\u043F\u0440\u043E\u0441\u0430 (\u043F\u043E\u0438\u0441\u043A)"), /* @__PURE__ */ React.createElement("div", { className: "request-data-combobox" }, /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "request-data-request-template-select",
|
|
name: "request_template_search_nohistory",
|
|
type: "text",
|
|
value: dataRequestModal.requestTemplateQuery,
|
|
onChange: (event) => setDataRequestModal((prev) => ({
|
|
...prev,
|
|
requestTemplateQuery: event.target.value,
|
|
selectedRequestTemplateId: "",
|
|
templateStatus: "",
|
|
error: ""
|
|
})),
|
|
onFocus: (event) => {
|
|
event.currentTarget.removeAttribute("readonly");
|
|
setRequestTemplateSuggestOpen(true);
|
|
},
|
|
onBlur: (event) => {
|
|
event.currentTarget.setAttribute("readonly", "readonly");
|
|
window.setTimeout(() => setRequestTemplateSuggestOpen(false), 120);
|
|
},
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate,
|
|
placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0448\u0430\u0431\u043B\u043E\u043D\u0430",
|
|
readOnly: true,
|
|
autoComplete: "new-password",
|
|
autoCorrect: "off",
|
|
autoCapitalize: "none",
|
|
spellCheck: false,
|
|
"aria-autocomplete": "list",
|
|
"data-1p-ignore": "true",
|
|
"data-lpignore": "true"
|
|
}
|
|
), requestTemplateBadge ? /* @__PURE__ */ React.createElement("span", { className: "request-data-template-badge " + requestTemplateBadge.kind }, requestTemplateBadge.label) : null, requestTemplateSuggestOpen && filteredRequestTemplates.length ? /* @__PURE__ */ React.createElement("div", { className: "request-data-suggest-list", role: "listbox", "aria-label": "\u0428\u0430\u0431\u043B\u043E\u043D\u044B \u0437\u0430\u043F\u0440\u043E\u0441\u0430" }, filteredRequestTemplates.map((tpl) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
key: String(tpl.id),
|
|
type: "button",
|
|
className: "request-data-suggest-item",
|
|
onMouseDown: (event) => {
|
|
event.preventDefault();
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
requestTemplateQuery: String(tpl.name || ""),
|
|
selectedRequestTemplateId: String(tpl.id || ""),
|
|
error: "",
|
|
templateStatus: ""
|
|
}));
|
|
setRequestTemplateSuggestOpen(false);
|
|
void applyRequestTemplateById(String(tpl.id || ""), String(tpl.name || ""));
|
|
}
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", null, String(tpl.name || "\u0428\u0430\u0431\u043B\u043E\u043D"))
|
|
))) : null)), /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-actions-inline" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn",
|
|
"data-tooltip": !canSaveSelectedRequestTemplate ? "\u0427\u0443\u0436\u043E\u0439 \u0448\u0430\u0431\u043B\u043E\u043D \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0434\u043B\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F" : requestTemplateActionMode === "save" ? "\u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" : requestTemplateActionMode === "create" ? "\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u0448\u0430\u0431\u043B\u043E\u043D" : "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u0448\u0430\u0431\u043B\u043E\u043D\u0430",
|
|
onClick: saveCurrentDataRequestTemplate,
|
|
disabled: !canSaveSelectedRequestTemplate || dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate
|
|
},
|
|
dataRequestModal.savingTemplate ? "\u2026" : requestTemplateActionMode === "create" ? "\u271A" : "\u{1F4BE}"
|
|
))), dataRequestModal.templateStatus ? /* @__PURE__ */ React.createElement("div", { className: "status ok" }, dataRequestModal.templateStatus) : null, canRequestData && dataRequestModal.messageId ? /* @__PURE__ */ React.createElement("div", { className: "request-data-progress-line" }, /* @__PURE__ */ React.createElement("span", { className: "request-data-progress-chip" }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C: " + String(dataRequestProgress.filled) + " / " + String(dataRequestProgress.total))) : null, /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "request-data-template-select" }, "\u041F\u043E\u043B\u0435 \u0434\u0430\u043D\u043D\u044B\u0445 (\u043F\u043E\u0438\u0441\u043A \u043F\u043E \u0441\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0443)"), /* @__PURE__ */ React.createElement("div", { className: "request-data-combobox" }, /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "request-data-template-select",
|
|
name: "request_field_search_nohistory",
|
|
type: "text",
|
|
value: dataRequestModal.catalogFieldQuery,
|
|
onChange: (event) => setDataRequestModal((prev) => ({
|
|
...prev,
|
|
catalogFieldQuery: event.target.value,
|
|
selectedCatalogTemplateId: "",
|
|
templateStatus: "",
|
|
error: ""
|
|
})),
|
|
onFocus: (event) => {
|
|
event.currentTarget.removeAttribute("readonly");
|
|
setCatalogFieldSuggestOpen(true);
|
|
},
|
|
onBlur: (event) => {
|
|
event.currentTarget.setAttribute("readonly", "readonly");
|
|
window.setTimeout(() => setCatalogFieldSuggestOpen(false), 120);
|
|
},
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate,
|
|
placeholder: "\u041D\u0430\u0447\u043D\u0438\u0442\u0435 \u0432\u0432\u043E\u0434\u0438\u0442\u044C \u043D\u0430\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u043E\u043B\u044F",
|
|
readOnly: true,
|
|
autoComplete: "new-password",
|
|
autoCorrect: "off",
|
|
autoCapitalize: "none",
|
|
spellCheck: false,
|
|
"aria-autocomplete": "list",
|
|
"data-1p-ignore": "true",
|
|
"data-lpignore": "true"
|
|
}
|
|
), catalogFieldSuggestOpen && filteredCatalogFields.length ? /* @__PURE__ */ React.createElement("div", { className: "request-data-suggest-list", role: "listbox", "aria-label": "\u041F\u043E\u043B\u044F \u0434\u0430\u043D\u043D\u044B\u0445" }, filteredCatalogFields.map((tpl) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
key: String(tpl.id),
|
|
type: "button",
|
|
className: "request-data-suggest-item",
|
|
onMouseDown: (event) => {
|
|
event.preventDefault();
|
|
setDataRequestModal((prev) => ({
|
|
...prev,
|
|
catalogFieldQuery: String(tpl.label || tpl.key || ""),
|
|
selectedCatalogTemplateId: String(tpl.id || ""),
|
|
error: "",
|
|
templateStatus: ""
|
|
}));
|
|
setCatalogFieldSuggestOpen(false);
|
|
}
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", null, String(tpl.label || tpl.key)),
|
|
/* @__PURE__ */ React.createElement("small", null, String(tpl.value_type || "string"))
|
|
))) : null)), /* @__PURE__ */ React.createElement("div", { className: "request-data-modal-actions-inline" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn",
|
|
"data-tooltip": catalogFieldActionMode === "add" ? "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u043E\u043B\u0435 \u0438\u0437 \u0441\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0430" : "\u0421\u043E\u0437\u0434\u0430\u0442\u044C \u043D\u043E\u0432\u043E\u0435 \u043F\u043E\u043B\u0435",
|
|
onClick: addSelectedTemplateRow,
|
|
disabled: !String(dataRequestModal.catalogFieldQuery || "").trim() && !selectedCatalogFieldCandidate || dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate
|
|
},
|
|
catalogFieldActionMode === "add" ? "+" : "\u271A"
|
|
))), /* @__PURE__ */ React.createElement("div", { className: "request-data-rows" }, (dataRequestModal.rows || []).length ? (dataRequestModal.rows || []).map((rowItem, idx) => /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "request-data-row" + (String(draggedRequestRowId) === String(rowItem.localId) ? " dragging" : "") + (String(dragOverRequestRowId) === String(rowItem.localId) && String(draggedRequestRowId) !== String(rowItem.localId) ? " drag-over" : "") + (viewerRoleCode === "LAWYER" && rowItem?.is_filled ? " row-locked" : ""),
|
|
key: rowItem.localId,
|
|
onDragOver: (event) => {
|
|
if (!draggedRequestRowId) return;
|
|
event.preventDefault();
|
|
if (viewerRoleCode === "LAWYER" && rowItem?.is_filled) return;
|
|
setDragOverRequestRowId(String(rowItem.localId || ""));
|
|
},
|
|
onDrop: (event) => {
|
|
if (!draggedRequestRowId) return;
|
|
event.preventDefault();
|
|
if (viewerRoleCode === "LAWYER" && rowItem?.is_filled) return;
|
|
moveDataRequestRowToIndex(draggedRequestRowId, idx);
|
|
handleRequestRowDragEnd();
|
|
}
|
|
},
|
|
/* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn request-data-row-index-handle",
|
|
"data-tooltip": viewerRoleCode === "LAWYER" && rowItem?.is_filled ? "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u043B\u0435: \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0435\u043D\u0438\u0435 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E" : "\u041F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0434\u043B\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u043F\u043E\u0440\u044F\u0434\u043A\u0430",
|
|
draggable: !(viewerRoleCode === "LAWYER" && rowItem?.is_filled),
|
|
onDragStart: (event) => handleRequestRowDragStart(event, rowItem, viewerRoleCode === "LAWYER" && rowItem?.is_filled),
|
|
onDragEnd: handleRequestRowDragEnd,
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && rowItem?.is_filled,
|
|
"aria-label": "\u041F\u043E\u0440\u044F\u0434\u043E\u043A \u043F\u043E\u043B\u044F " + String(idx + 1)
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", null, idx + 1)
|
|
),
|
|
/* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", null, "\u041D\u0430\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
value: rowItem.label,
|
|
onChange: (event) => updateDataRequestRow(rowItem.localId, { label: event.target.value }),
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && rowItem?.is_filled
|
|
}
|
|
)),
|
|
/* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", null, "\u0422\u0438\u043F"), /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
value: rowItem.field_type || "string",
|
|
onChange: (nextValue) => updateDataRequestRow(rowItem.localId, { field_type: nextValue }),
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && rowItem?.is_filled,
|
|
options: requestDataTypeOptions.map((option) => ({ value: option.value, label: option.label })),
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043F"
|
|
}
|
|
)),
|
|
/* @__PURE__ */ React.createElement("div", { className: "request-data-row-controls" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn danger request-data-row-action-btn",
|
|
"data-tooltip": viewerRoleCode === "LAWYER" && rowItem?.is_filled ? "\u042E\u0440\u0438\u0441\u0442 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043D\u043E\u0435 \u043F\u043E\u043B\u0435" : "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u0435",
|
|
onClick: () => removeDataRequestRow(rowItem.localId),
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate || viewerRoleCode === "LAWYER" && rowItem?.is_filled
|
|
},
|
|
"\xD7"
|
|
)),
|
|
canRequestData && (rowItem?.is_filled || String(rowItem?.value_text || "").trim()) ? /* @__PURE__ */ React.createElement("div", { className: "request-data-row-client-value" }, /* @__PURE__ */ React.createElement("span", { className: "request-data-row-client-label" }, "\u0417\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C:"), String(rowItem?.field_type || "").toLowerCase() === "file" ? rowItem?.value_file && rowItem.value_file.download_url ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "chat-message-file-chip",
|
|
onClick: () => openAttachmentFromMessage(rowItem.value_file)
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"),
|
|
/* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(rowItem.value_file.file_name || "\u0424\u0430\u0439\u043B"))
|
|
) : /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u0424\u0430\u0439\u043B \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D") : /* @__PURE__ */ React.createElement("span", { className: "request-data-row-client-text" }, String(rowItem?.field_type || "").toLowerCase() === "date" ? fmtDateOnly(rowItem?.value_text) : String(rowItem?.value_text || "").trim().slice(0, 140))) : null
|
|
)) : /* @__PURE__ */ React.createElement("div", { className: "muted" }, "\u041F\u043E\u043B\u044F \u0434\u043B\u044F \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u0435\u0449\u0435 \u043D\u0435 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u044B"))), dataRequestModal.error ? /* @__PURE__ */ React.createElement("div", { className: "status error" }, dataRequestModal.error) : null, /* @__PURE__ */ React.createElement("div", { className: "modal-actions modal-actions-right" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "btn btn-sm request-data-submit-btn",
|
|
onClick: submitDataRequestModal,
|
|
disabled: dataRequestModal.loading || dataRequestModal.saving || dataRequestModal.savingTemplate
|
|
},
|
|
dataRequestModal.saving ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435..." : "\u041E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C"
|
|
)))
|
|
), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "overlay" + (requestDataListOpen ? " open" : ""),
|
|
onClick: () => setRequestDataListOpen(false),
|
|
"aria-hidden": requestDataListOpen ? "false" : "true"
|
|
},
|
|
/* @__PURE__ */ React.createElement("div", { className: "modal request-data-summary-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0414\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted request-finance-subtitle" }, row?.track_number ? "\u0417\u0430\u044F\u0432\u043A\u0430 " + String(row.track_number) : "")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: () => setRequestDataListOpen(false), "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-list" }, requestDataListItems.length ? requestDataListItems.map((item) => {
|
|
const value = formatRequestDataValue(item);
|
|
const isFile = String(item?.field_type || "").toLowerCase() === "file";
|
|
return /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-row", key: String(item.id || item.key) }, /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-label" }, String(item.label || humanizeKey(item.key))), /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-value" }, isFile ? value && typeof value === "object" ? /* @__PURE__ */ React.createElement("div", { className: "request-data-summary-file" }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "chat-message-file-chip", onClick: () => downloadAttachment(value) }, /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-icon", "aria-hidden": "true" }, "\u{1F4CE}"), /* @__PURE__ */ React.createElement("span", { className: "chat-message-file-name" }, String(value.file_name || "\u0424\u0430\u0439\u043B")))) : /* @__PURE__ */ React.createElement("span", { className: "muted" }, "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E") : String(value || "\u041D\u0435 \u0437\u0430\u043F\u043E\u043B\u043D\u0435\u043D\u043E")));
|
|
}) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0435 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442")))
|
|
));
|
|
}
|
|
|
|
// app/web/admin/features/tables/AvailableTablesSection.jsx
|
|
function AvailableTablesSection({
|
|
tables,
|
|
status,
|
|
onRefresh,
|
|
onToggleActive,
|
|
DataTableComponent,
|
|
StatusLineComponent,
|
|
IconButtonComponent
|
|
}) {
|
|
const tableState = tables?.availableTables || { rows: [] };
|
|
const DataTable = DataTableComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const IconButton = IconButtonComponent;
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0441\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0446"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0421\u043A\u0440\u044B\u0442\u0430\u044F \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u0430\u044F \u0432\u043A\u043B\u0430\u0434\u043A\u0430. \u0414\u043E\u0441\u0442\u0443\u043F \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u043F\u043E \u043F\u0440\u044F\u043C\u043E\u0439 \u0441\u0441\u044B\u043B\u043A\u0435.")), /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onRefresh, title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(RefreshIcon, null))), /* @__PURE__ */ React.createElement(
|
|
DataTable,
|
|
{
|
|
headers: [
|
|
{ key: "label", label: "\u0422\u0430\u0431\u043B\u0438\u0446\u0430" },
|
|
{ key: "table", label: "\u041A\u043E\u0434" },
|
|
{ key: "section", label: "\u0420\u0430\u0437\u0434\u0435\u043B" },
|
|
{ key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430" },
|
|
{ key: "updated_at", label: "\u041E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430" },
|
|
{ key: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439" },
|
|
{ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" }
|
|
],
|
|
rows: tableState.rows,
|
|
emptyColspan: 7,
|
|
renderRow: (row) => /* @__PURE__ */ React.createElement("tr", { key: String(row.table || row.label) }, /* @__PURE__ */ React.createElement("td", null, row.label || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("code", null, row.table || "-")), /* @__PURE__ */ React.createElement("td", null, row.section || "-"), /* @__PURE__ */ React.createElement("td", null, boolLabel(Boolean(row.is_active))), /* @__PURE__ */ React.createElement("td", null, fmtDate(row.updated_at)), /* @__PURE__ */ React.createElement("td", null, row.responsible || "-"), /* @__PURE__ */ React.createElement("td", null, /* @__PURE__ */ React.createElement("div", { className: "table-actions" }, /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: row.is_active ? "\u23F8" : "\u25B6",
|
|
tooltip: row.is_active ? "\u0414\u0435\u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0446\u0443" : "\u0410\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0446\u0443",
|
|
onClick: () => onToggleActive(row.table, !Boolean(row.is_active))
|
|
}
|
|
))))
|
|
}
|
|
), /* @__PURE__ */ React.createElement(StatusLine, { status }));
|
|
}
|
|
|
|
// app/web/admin/hooks/useAdminApi.js
|
|
function useAdminApi(token) {
|
|
const { useCallback: useCallback2 } = React;
|
|
return useCallback2(
|
|
async (path, options, tokenOverride) => {
|
|
const opts = options || {};
|
|
const authToken = tokenOverride !== void 0 ? tokenOverride : token;
|
|
const headers = { "Content-Type": "application/json", ...opts.headers || {} };
|
|
if (opts.auth !== false) {
|
|
if (!authToken) throw new Error("\u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438");
|
|
headers.Authorization = "Bearer " + authToken;
|
|
}
|
|
const response = await fetch(path, {
|
|
method: opts.method || "GET",
|
|
headers,
|
|
body: opts.body ? JSON.stringify(opts.body) : void 0
|
|
});
|
|
const text = await response.text();
|
|
let payload;
|
|
try {
|
|
payload = text ? JSON.parse(text) : {};
|
|
} catch (_) {
|
|
payload = { raw: text };
|
|
}
|
|
if (!response.ok) {
|
|
const message = payload && (payload.detail || payload.error || payload.raw) || "HTTP " + response.status;
|
|
if (response.status === 401 && opts.auth !== false) {
|
|
try {
|
|
localStorage.removeItem(LS_TOKEN);
|
|
sessionStorage.setItem(ADMIN_AUTH_REDIRECT_REASON_KEY, "expired");
|
|
} catch (_) {
|
|
}
|
|
if (typeof window !== "undefined") {
|
|
const target = "/admin.html";
|
|
if (window.location.pathname !== target || window.location.search) {
|
|
window.location.replace(target);
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
}
|
|
}
|
|
const error = new Error(translateApiError(String(message)));
|
|
error.httpStatus = Number(response.status || 0);
|
|
throw error;
|
|
}
|
|
return payload;
|
|
},
|
|
[token]
|
|
);
|
|
}
|
|
|
|
// app/web/admin/hooks/useAdminCatalogLoaders.js
|
|
function useAdminCatalogLoaders({ api, setStatus, setTableState, setReferenceRowsMap, buildUniversalQuery: buildUniversalQuery2 }) {
|
|
const { useCallback: useCallback2 } = React;
|
|
const loadAvailableTables = useCallback2(
|
|
async (tokenOverride) => {
|
|
setStatus("availableTables", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", "");
|
|
try {
|
|
const data = await api("/api/admin/crud/meta/available-tables", {}, tokenOverride);
|
|
const rows = Array.isArray(data.rows) ? data.rows : [];
|
|
setTableState("availableTables", {
|
|
filters: [],
|
|
sort: null,
|
|
offset: 0,
|
|
total: rows.length,
|
|
showAll: true,
|
|
rows
|
|
});
|
|
setStatus("availableTables", "\u0421\u043F\u0438\u0441\u043E\u043A \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok");
|
|
return true;
|
|
} catch (error) {
|
|
setStatus("availableTables", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
return false;
|
|
}
|
|
},
|
|
[api, setStatus, setTableState]
|
|
);
|
|
const loadReferenceRows = useCallback2(
|
|
async (catalogRows, tokenOverride) => {
|
|
const rows = Array.isArray(catalogRows) ? catalogRows : [];
|
|
const byTable = {};
|
|
rows.forEach((item) => {
|
|
const table = String(item?.table || "");
|
|
if (!table) return;
|
|
byTable[table] = item;
|
|
});
|
|
const references = /* @__PURE__ */ new Set();
|
|
rows.forEach((item) => {
|
|
(item?.columns || []).forEach((column) => {
|
|
const meta = normalizeReferenceMeta(column?.reference);
|
|
if (meta?.table) references.add(meta.table);
|
|
});
|
|
});
|
|
if (!references.size) {
|
|
setReferenceRowsMap({});
|
|
return;
|
|
}
|
|
const nextMap = {};
|
|
await Promise.all(
|
|
Array.from(references.values()).map(async (table) => {
|
|
const meta = byTable[table];
|
|
const endpoint = String(meta?.query_endpoint || "/api/admin/crud/" + table + "/query");
|
|
const sort = Array.isArray(meta?.default_sort) && meta.default_sort.length ? meta.default_sort : [{ field: "created_at", dir: "desc" }];
|
|
try {
|
|
const data = await api(
|
|
endpoint,
|
|
{
|
|
method: "POST",
|
|
body: buildUniversalQuery2([], sort, 500, 0)
|
|
},
|
|
tokenOverride
|
|
);
|
|
nextMap[table] = Array.isArray(data?.rows) ? data.rows : [];
|
|
} catch (_) {
|
|
nextMap[table] = [];
|
|
}
|
|
})
|
|
);
|
|
setReferenceRowsMap(nextMap);
|
|
},
|
|
[api, buildUniversalQuery2, setReferenceRowsMap]
|
|
);
|
|
return {
|
|
loadAvailableTables,
|
|
loadReferenceRows
|
|
};
|
|
}
|
|
|
|
// app/web/admin/hooks/useKanban.js
|
|
function normalizeKanbanColumns(rows, columns) {
|
|
const safeRows = Array.isArray(rows) ? rows : [];
|
|
const safeColumns = Array.isArray(columns) ? columns : [];
|
|
const canonicalByLabel = /* @__PURE__ */ new Map();
|
|
const aliases = /* @__PURE__ */ new Map();
|
|
const mergedColumns = [];
|
|
safeColumns.forEach((column, index) => {
|
|
const key = String(column?.key || "").trim();
|
|
if (!key) return;
|
|
const label = String(column?.label || key).trim() || key;
|
|
const labelKey = label.toLocaleLowerCase("ru-RU");
|
|
const sortOrder = Number.isFinite(Number(column?.sort_order)) ? Number(column.sort_order) : index;
|
|
if (!canonicalByLabel.has(labelKey)) {
|
|
const canonical2 = {
|
|
key,
|
|
label,
|
|
sort_order: sortOrder,
|
|
total: 0
|
|
};
|
|
canonicalByLabel.set(labelKey, canonical2);
|
|
mergedColumns.push(canonical2);
|
|
return;
|
|
}
|
|
const canonical = canonicalByLabel.get(labelKey);
|
|
if (canonical && canonical.key !== key) aliases.set(key, canonical.key);
|
|
});
|
|
if (!aliases.size) {
|
|
return { rows: safeRows, columns: safeColumns };
|
|
}
|
|
const remapGroup = (groupKey) => {
|
|
const normalized = String(groupKey || "").trim();
|
|
if (!normalized) return normalized;
|
|
return aliases.get(normalized) || normalized;
|
|
};
|
|
const normalizedRows = safeRows.map((row) => ({
|
|
...row,
|
|
status_group: remapGroup(row?.status_group),
|
|
available_transitions: Array.isArray(row?.available_transitions) ? row.available_transitions.map((transition) => ({
|
|
...transition,
|
|
target_group: remapGroup(transition?.target_group)
|
|
})) : []
|
|
}));
|
|
const totals = /* @__PURE__ */ new Map();
|
|
normalizedRows.forEach((row) => {
|
|
const key = String(row?.status_group || "").trim();
|
|
if (!key) return;
|
|
totals.set(key, Number(totals.get(key) || 0) + 1);
|
|
});
|
|
const normalizedColumns = mergedColumns.map((column) => ({
|
|
...column,
|
|
total: Number(totals.get(String(column.key || "").trim()) || 0)
|
|
}));
|
|
return { rows: normalizedRows, columns: normalizedColumns };
|
|
}
|
|
function useKanban({ api, setStatus, setTableState, tablesRef }) {
|
|
const { useCallback: useCallback2, useState: useState4 } = React;
|
|
const [kanbanData, setKanbanData] = useState4({
|
|
rows: [],
|
|
columns: KANBAN_GROUPS,
|
|
total: 0,
|
|
truncated: false
|
|
});
|
|
const [kanbanLoading, setKanbanLoading] = useState4(false);
|
|
const [kanbanSortModal, setKanbanSortModal] = useState4({
|
|
open: false,
|
|
value: "created_newest"
|
|
});
|
|
const [kanbanSortApplied, setKanbanSortApplied] = useState4(false);
|
|
const loadKanban = useCallback2(
|
|
async (tokenOverride, options) => {
|
|
const opts = options || {};
|
|
const currentKanbanState = tablesRef.current.kanban || createTableState();
|
|
const activeFilters = Array.isArray(opts.filtersOverride) ? [...opts.filtersOverride] : [...currentKanbanState.filters || []];
|
|
const currentSortMode = Array.isArray(currentKanbanState.sort) && currentKanbanState.sort[0] ? String(currentKanbanState.sort[0].field || "") : "";
|
|
const activeSortMode = String(opts.sortModeOverride || currentSortMode || kanbanSortModal.value || "created_newest").trim() || "created_newest";
|
|
const params = new URLSearchParams({ limit: "400", sort_mode: activeSortMode });
|
|
if (activeFilters.length) params.set("filters", JSON.stringify(activeFilters));
|
|
setKanbanLoading(true);
|
|
setStatus("kanban", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", "");
|
|
try {
|
|
const data = await api("/api/admin/requests/kanban?" + params.toString(), {}, tokenOverride);
|
|
const rawRows = Array.isArray(data.rows) ? data.rows : [];
|
|
const rawColumns = Array.isArray(data.columns) && data.columns.length ? data.columns : KANBAN_GROUPS;
|
|
const normalized = normalizeKanbanColumns(rawRows, rawColumns);
|
|
const rows = Array.isArray(normalized.rows) ? normalized.rows : rawRows;
|
|
const columns = Array.isArray(normalized.columns) && normalized.columns.length ? normalized.columns : rawColumns;
|
|
setKanbanData({
|
|
rows,
|
|
columns,
|
|
total: Number(data.total || rows.length),
|
|
truncated: Boolean(data.truncated)
|
|
});
|
|
setTableState("kanban", {
|
|
...currentKanbanState,
|
|
filters: activeFilters,
|
|
sort: [{ field: activeSortMode, dir: "asc" }],
|
|
rows,
|
|
total: Number(data.total || rows.length),
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
const tail = Boolean(data.truncated) ? " \u041F\u043E\u043A\u0430\u0437\u0430\u043D\u0430 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043D\u0430\u044F \u0432\u044B\u0431\u043E\u0440\u043A\u0430." : "";
|
|
setStatus("kanban", "\u041A\u0430\u043D\u0431\u0430\u043D \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D." + tail, "ok");
|
|
} catch (error) {
|
|
setStatus("kanban", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
} finally {
|
|
setKanbanLoading(false);
|
|
}
|
|
},
|
|
[api, kanbanSortModal.value, setStatus, setTableState, tablesRef]
|
|
);
|
|
const openKanbanSortModal = useCallback2(() => {
|
|
const tableState = tablesRef.current.kanban || createTableState();
|
|
const currentMode = Array.isArray(tableState.sort) && tableState.sort[0] ? String(tableState.sort[0].field || "") : "";
|
|
setKanbanSortModal({
|
|
open: true,
|
|
value: currentMode || "created_newest"
|
|
});
|
|
setStatus("kanbanSort", "", "");
|
|
}, [setStatus, tablesRef]);
|
|
const closeKanbanSortModal = useCallback2(() => {
|
|
setKanbanSortModal((prev) => ({ ...prev, open: false }));
|
|
setStatus("kanbanSort", "", "");
|
|
}, [setStatus]);
|
|
const updateKanbanSortMode = useCallback2((event) => {
|
|
setKanbanSortModal((prev) => ({ ...prev, value: String(event.target.value || "created_newest") }));
|
|
}, []);
|
|
const submitKanbanSortModal = useCallback2(
|
|
async (event) => {
|
|
event.preventDefault();
|
|
const nextMode = String(kanbanSortModal.value || "created_newest");
|
|
const tableState = tablesRef.current.kanban || createTableState();
|
|
setTableState("kanban", {
|
|
...tableState,
|
|
sort: [{ field: nextMode, dir: "asc" }],
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
setKanbanSortApplied(true);
|
|
closeKanbanSortModal();
|
|
await loadKanban(void 0, { sortModeOverride: nextMode });
|
|
},
|
|
[closeKanbanSortModal, kanbanSortModal.value, loadKanban, setTableState, tablesRef]
|
|
);
|
|
const resetKanbanState = useCallback2(() => {
|
|
setKanbanSortModal({ open: false, value: "created_newest" });
|
|
setKanbanSortApplied(false);
|
|
setKanbanData({ rows: [], columns: KANBAN_GROUPS, total: 0, truncated: false });
|
|
setKanbanLoading(false);
|
|
}, []);
|
|
return {
|
|
kanbanData,
|
|
kanbanLoading,
|
|
kanbanSortModal,
|
|
kanbanSortApplied,
|
|
loadKanban,
|
|
openKanbanSortModal,
|
|
closeKanbanSortModal,
|
|
updateKanbanSortMode,
|
|
submitKanbanSortModal,
|
|
resetKanbanState
|
|
};
|
|
}
|
|
|
|
// app/web/admin/hooks/useRequestWorkspace.js
|
|
var DEFAULT_INVOICE_REQUISITES = Object.freeze({
|
|
issuer_name: '\u041E\u041E\u041E "\u0410\u0443\u0434\u0438\u0442\u043E\u0440\u044B \u043A\u043E\u0440\u043F\u043E\u0440\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u0438"',
|
|
issuer_inn: "7604226740",
|
|
issuer_kpp: "760401001",
|
|
issuer_address: "\u0433. \u042F\u0440\u043E\u0441\u043B\u0430\u0432\u043B\u044C, \u0443\u043B. \u0411\u043E\u0433\u0434\u0430\u043D\u043E\u0432\u0438\u0447\u0430, 6\u0410",
|
|
bank_name: '\u0410\u041E "\u0410\u041B\u042C\u0424\u0410-\u0411\u0410\u041D\u041A"',
|
|
bank_bik: "044525593",
|
|
bank_account: "40702810501860000582",
|
|
bank_corr_account: "30101810200000000593"
|
|
});
|
|
var UPLOAD_MAX_ATTEMPTS = 4;
|
|
async function buildStorageUploadError(response, fallbackMessage) {
|
|
const base = String(fallbackMessage || "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435");
|
|
const status = Number(response?.status || 0);
|
|
const statusText = String(response?.statusText || "").trim();
|
|
let details = "";
|
|
try {
|
|
details = String(await response.text() || "").replace(/\s+/g, " ").trim();
|
|
} catch (_) {
|
|
details = "";
|
|
}
|
|
if (details.length > 180) details = details.slice(0, 180) + "...";
|
|
const parts = [];
|
|
if (status > 0) parts.push("HTTP " + status + (statusText ? " " + statusText : ""));
|
|
if (details) parts.push(details);
|
|
return parts.length ? base + " (" + parts.join("; ") + ")" : base;
|
|
}
|
|
function wait(ms) {
|
|
return new Promise((resolve) => window.setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
}
|
|
function nextUploadRetryDelayMs(attempt) {
|
|
const base = Math.min(1200 * Math.pow(2, Math.max(0, Number(attempt || 1) - 1)), 7e3);
|
|
const jitter = Math.floor(Math.random() * 250);
|
|
return base + jitter;
|
|
}
|
|
function isRetryableUploadError(error) {
|
|
const status = Number(error?.httpStatus || error?.status || 0);
|
|
if ([408, 425, 429, 500, 502, 503, 504].includes(status)) return true;
|
|
if (status > 0) return false;
|
|
const message = String(error?.message || "").toLowerCase();
|
|
if (!message) return true;
|
|
return message.includes("networkerror") || message.includes("failed to fetch") || message.includes("load failed") || message.includes("network request failed") || message.includes("timeout");
|
|
}
|
|
function sortRowsByCreatedAt(rows) {
|
|
return [...rows].sort((left, right) => {
|
|
const leftTs = new Date(left?.created_at || left?.updated_at || 0).getTime();
|
|
const rightTs = new Date(right?.created_at || right?.updated_at || 0).getTime();
|
|
if (Number.isFinite(leftTs) && Number.isFinite(rightTs) && leftTs !== rightTs) return leftTs - rightTs;
|
|
return String(left?.id || "").localeCompare(String(right?.id || ""), "ru");
|
|
});
|
|
}
|
|
function mergeRowsById(existingRows, incomingRows) {
|
|
const merged = /* @__PURE__ */ new Map();
|
|
(Array.isArray(existingRows) ? existingRows : []).forEach((row) => {
|
|
const key = String(row?.id || "").trim();
|
|
if (key) merged.set(key, row);
|
|
});
|
|
(Array.isArray(incomingRows) ? incomingRows : []).forEach((row) => {
|
|
const key = String(row?.id || "").trim();
|
|
if (key) merged.set(key, row);
|
|
});
|
|
return sortRowsByCreatedAt(Array.from(merged.values()));
|
|
}
|
|
function getOldestMessageCursor(rows) {
|
|
const sorted = sortRowsByCreatedAt(Array.isArray(rows) ? rows : []);
|
|
const first = sorted[0];
|
|
if (!first) return null;
|
|
const beforeId = String(first.id || "").trim();
|
|
const beforeCreatedAt = String(first.created_at || first.updated_at || "").trim();
|
|
if (!beforeId || !beforeCreatedAt) return null;
|
|
return { beforeId, beforeCreatedAt };
|
|
}
|
|
function collectDeferredMessageIds(rows) {
|
|
return (Array.isArray(rows) ? rows : []).filter((row) => row && typeof row === "object" && row.body_loaded === false && String(row.id || "").trim()).map((row) => String(row.id).trim());
|
|
}
|
|
function normalizeMessageAuthors(rows, users) {
|
|
const usersByEmail = new Map(
|
|
(Array.isArray(users) ? users : []).filter((user) => user && user.email).map((user) => [String(user.email).toLowerCase(), String(user.name || user.email)])
|
|
);
|
|
return (Array.isArray(rows) ? rows : []).map((item) => {
|
|
if (!item || typeof item !== "object") return item;
|
|
const authorType = String(item.author_type || "").toUpperCase();
|
|
const authorName = String(item.author_name || "").trim();
|
|
if ((authorType === "LAWYER" || authorType === "SYSTEM") && authorName.includes("@")) {
|
|
const mapped = usersByEmail.get(authorName.toLowerCase());
|
|
if (mapped) return { ...item, author_name: mapped };
|
|
}
|
|
return item;
|
|
});
|
|
}
|
|
function buildFinanceSummaryFromInvoices(financeSummaryData, rowData, invoices) {
|
|
if (financeSummaryData && typeof financeSummaryData === "object") return financeSummaryData;
|
|
const paidInvoices = (Array.isArray(invoices) ? invoices : []).filter(
|
|
(item) => String(item?.status || "").toUpperCase() === "PAID"
|
|
);
|
|
const paidTotal = paidInvoices.reduce((acc, item) => {
|
|
const amount = Number(item?.amount || 0);
|
|
return Number.isFinite(amount) ? acc + amount : acc;
|
|
}, 0);
|
|
const latestPaidAt = paidInvoices.reduce((latest, item) => {
|
|
const raw = item?.paid_at;
|
|
const ts = raw ? new Date(raw).getTime() : Number.NaN;
|
|
if (!Number.isFinite(ts)) return latest;
|
|
if (!latest) return String(raw);
|
|
const latestTs = new Date(latest).getTime();
|
|
return ts > latestTs ? String(raw) : latest;
|
|
}, "");
|
|
return {
|
|
request_cost: rowData?.request_cost ?? null,
|
|
effective_rate: rowData?.effective_rate ?? null,
|
|
paid_total: Math.round((paidTotal + Number.EPSILON) * 100) / 100,
|
|
last_paid_at: latestPaidAt || rowData?.paid_at || null
|
|
};
|
|
}
|
|
function useRequestWorkspace(options) {
|
|
const { useCallback: useCallback2, useRef: useRef4, useState: useState4 } = React;
|
|
const opts = options || {};
|
|
const api = opts.api;
|
|
const setStatus = opts.setStatus;
|
|
const setActiveSection = opts.setActiveSection;
|
|
const token = opts.token || "";
|
|
const users = Array.isArray(opts.users) ? opts.users : [];
|
|
const resolveAdminObjectSrc2 = opts.resolveAdminObjectSrc;
|
|
const [requestModal, setRequestModal] = useState4(createRequestModalState());
|
|
const requestOpenGuardRef = useRef4({ requestId: "", ts: 0 });
|
|
const resetRequestWorkspaceState = useCallback2(() => {
|
|
setRequestModal(createRequestModalState());
|
|
requestOpenGuardRef.current = { requestId: "", ts: 0 };
|
|
}, []);
|
|
const hydrateRequestMessageBodies = useCallback2(
|
|
async (requestId, rows) => {
|
|
const targetRequestId = String(requestId || "").trim();
|
|
const ids = collectDeferredMessageIds(rows);
|
|
if (!api || !targetRequestId || !ids.length) return null;
|
|
try {
|
|
const payload = await api("/api/admin/chat/requests/" + targetRequestId + "/message-bodies", {
|
|
method: "POST",
|
|
body: { ids }
|
|
});
|
|
const nextRows = Array.isArray(payload?.rows) ? payload.rows : [];
|
|
if (!nextRows.length) return payload || null;
|
|
setRequestModal((prev) => {
|
|
if (String(prev.requestId || "") !== targetRequestId) return prev;
|
|
return {
|
|
...prev,
|
|
messages: mergeRowsById(prev.messages, nextRows)
|
|
};
|
|
});
|
|
return payload || null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
},
|
|
[api]
|
|
);
|
|
const updateRequestModalMessageDraft = useCallback2((event) => {
|
|
const value = event.target.value;
|
|
setRequestModal((prev) => ({ ...prev, messageDraft: value }));
|
|
}, []);
|
|
const appendRequestModalFiles = useCallback2((files) => {
|
|
const list = Array.isArray(files) ? files.filter(Boolean) : [];
|
|
if (!list.length) return;
|
|
setRequestModal((prev) => {
|
|
const existing = Array.isArray(prev.selectedFiles) ? prev.selectedFiles : [];
|
|
const next = [...existing];
|
|
list.forEach((file) => {
|
|
const duplicate = next.some(
|
|
(item) => item && item.name === file.name && Number(item.size || 0) === Number(file.size || 0) && Number(item.lastModified || 0) === Number(file.lastModified || 0)
|
|
);
|
|
if (!duplicate) next.push(file);
|
|
});
|
|
return { ...prev, selectedFiles: next };
|
|
});
|
|
}, []);
|
|
const removeRequestModalFile = useCallback2((index) => {
|
|
setRequestModal((prev) => {
|
|
const existing = Array.isArray(prev.selectedFiles) ? [...prev.selectedFiles] : [];
|
|
existing.splice(index, 1);
|
|
return { ...prev, selectedFiles: existing };
|
|
});
|
|
}, []);
|
|
const clearRequestModalFiles = useCallback2(() => {
|
|
setRequestModal((prev) => ({ ...prev, selectedFiles: [] }));
|
|
}, []);
|
|
const uploadRequestAttachmentWithRetry = useCallback2(
|
|
async ({ requestId, file, messageId }) => {
|
|
if (!api) throw new Error("API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D");
|
|
const targetRequestId = String(requestId || "").trim();
|
|
if (!targetRequestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430");
|
|
if (!file) throw new Error("\u0424\u0430\u0439\u043B \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D");
|
|
const mimeType = String(file.type || "application/octet-stream");
|
|
const runUploadStepWithRetry = async (label, action) => {
|
|
let lastError = null;
|
|
let attemptsUsed = 0;
|
|
for (let attempt = 1; attempt <= UPLOAD_MAX_ATTEMPTS; attempt += 1) {
|
|
attemptsUsed = attempt;
|
|
try {
|
|
return await action(attempt);
|
|
} catch (error) {
|
|
lastError = error;
|
|
const canRetry = attempt < UPLOAD_MAX_ATTEMPTS && isRetryableUploadError(error);
|
|
if (!canRetry) break;
|
|
await wait(nextUploadRetryDelayMs(attempt));
|
|
}
|
|
}
|
|
const reason = String(lastError?.message || "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u0435\u0442\u0438");
|
|
throw new Error(label + ": " + reason + " (\u043F\u043E\u043F\u044B\u0442\u043E\u043A: " + attemptsUsed + ")");
|
|
};
|
|
const init = await runUploadStepWithRetry("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0447\u0430\u0442\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0443 \u0444\u0430\u0439\u043B\u0430", async () => {
|
|
return api("/api/admin/uploads/init", {
|
|
method: "POST",
|
|
body: {
|
|
file_name: file.name,
|
|
mime_type: mimeType,
|
|
size_bytes: file.size,
|
|
scope: "REQUEST_ATTACHMENT",
|
|
request_id: targetRequestId
|
|
}
|
|
});
|
|
});
|
|
await runUploadStepWithRetry("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435", async () => {
|
|
const putResp = await fetch(init.presigned_url, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": mimeType },
|
|
body: file
|
|
});
|
|
if (putResp.ok) return null;
|
|
const error = new Error(await buildStorageUploadError(putResp, "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435"));
|
|
error.httpStatus = Number(putResp.status || 0);
|
|
throw error;
|
|
});
|
|
return runUploadStepWithRetry("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0443 \u0444\u0430\u0439\u043B\u0430", async () => {
|
|
return api("/api/admin/uploads/complete", {
|
|
method: "POST",
|
|
body: {
|
|
key: init.key,
|
|
file_name: file.name,
|
|
mime_type: mimeType,
|
|
size_bytes: file.size,
|
|
scope: "REQUEST_ATTACHMENT",
|
|
request_id: targetRequestId,
|
|
message_id: messageId || null
|
|
}
|
|
});
|
|
});
|
|
},
|
|
[api]
|
|
);
|
|
const loadRequestModalData = useCallback2(
|
|
async (requestId, loadOptions) => {
|
|
if (!api || !requestId) return;
|
|
const localOpts = loadOptions || {};
|
|
const showLoading = localOpts.showLoading !== false;
|
|
if (showLoading) {
|
|
setRequestModal((prev) => ({
|
|
...prev,
|
|
loading: true,
|
|
requestId,
|
|
requestData: null,
|
|
financeSummary: null,
|
|
invoices: [],
|
|
statusRouteNodes: [],
|
|
messagesHasMore: false,
|
|
messagesLoadingMore: false,
|
|
messagesLoadedCount: 0,
|
|
messagesTotal: 0
|
|
}));
|
|
}
|
|
try {
|
|
const workspaceData = await api("/api/admin/requests/" + requestId + "/workspace?include_related=false");
|
|
const row = workspaceData?.request || null;
|
|
const messagesData = { rows: workspaceData?.messages || [] };
|
|
const statusRouteData = workspaceData?.status_route || { nodes: [] };
|
|
const financeSummaryData = workspaceData?.finance_summary || null;
|
|
const usersById = new Map(users.filter((user) => user && user.id).map((user) => [String(user.id), user]));
|
|
const rowData = row && typeof row === "object" ? { ...row } : row;
|
|
if (rowData && typeof rowData === "object") {
|
|
const assignedLawyerId = String(rowData.assigned_lawyer_id || "").trim();
|
|
if (assignedLawyerId) {
|
|
const lawyer = usersById.get(assignedLawyerId);
|
|
if (lawyer) {
|
|
rowData.assigned_lawyer_name = rowData.assigned_lawyer_name || lawyer.name || lawyer.email || assignedLawyerId;
|
|
rowData.assigned_lawyer_phone = rowData.assigned_lawyer_phone || lawyer.phone || null;
|
|
}
|
|
}
|
|
}
|
|
const normalizedMessages = normalizeMessageAuthors(messagesData.rows || [], users);
|
|
setRequestModal((prev) => ({
|
|
...prev,
|
|
loading: false,
|
|
requestId: rowData?.id || requestId,
|
|
trackNumber: String(rowData?.track_number || ""),
|
|
requestData: rowData,
|
|
financeSummary: buildFinanceSummaryFromInvoices(financeSummaryData, rowData, []),
|
|
invoices: [],
|
|
statusRouteNodes: Array.isArray(statusRouteData?.nodes) ? statusRouteData.nodes : [],
|
|
statusHistory: Array.isArray(statusRouteData?.history) ? statusRouteData.history : [],
|
|
availableStatuses: Array.isArray(statusRouteData?.available_statuses) ? statusRouteData.available_statuses : [],
|
|
currentImportantDateAt: String(statusRouteData?.current_important_date_at || rowData?.important_date_at || ""),
|
|
messages: normalizedMessages,
|
|
messagesHasMore: Boolean(workspaceData?.messages_has_more),
|
|
messagesLoadingMore: false,
|
|
messagesLoadedCount: Number(workspaceData?.messages_loaded_count || normalizedMessages.length || 0),
|
|
messagesTotal: Number(workspaceData?.messages_total || normalizedMessages.length || 0),
|
|
attachments: [],
|
|
selectedFiles: [],
|
|
fileUploading: false
|
|
}));
|
|
void hydrateRequestMessageBodies(requestId, normalizedMessages);
|
|
void Promise.all([
|
|
api("/api/admin/uploads/request-attachments/" + requestId),
|
|
api("/api/admin/invoices/by-request/" + requestId),
|
|
api("/api/admin/requests/" + requestId + "/status-route")
|
|
]).then(([attachmentsData, invoicesData, nextStatusRouteData]) => {
|
|
const attachments = (attachmentsData?.rows || []).map((item) => ({
|
|
...item,
|
|
download_url: resolveAdminObjectSrc2(item.s3_key, token)
|
|
}));
|
|
const invoices = Array.isArray(invoicesData?.rows) ? invoicesData.rows : [];
|
|
setRequestModal((prev) => {
|
|
if (String(prev.requestId || "") !== String(requestId)) return prev;
|
|
return {
|
|
...prev,
|
|
attachments,
|
|
invoices,
|
|
financeSummary: buildFinanceSummaryFromInvoices(prev.financeSummary, prev.requestData, invoices),
|
|
statusRouteNodes: Array.isArray(nextStatusRouteData?.nodes) ? nextStatusRouteData.nodes : [],
|
|
statusHistory: Array.isArray(nextStatusRouteData?.history) ? nextStatusRouteData.history : [],
|
|
availableStatuses: Array.isArray(nextStatusRouteData?.available_statuses) ? nextStatusRouteData.available_statuses : [],
|
|
currentImportantDateAt: String(nextStatusRouteData?.current_important_date_at || prev.currentImportantDateAt || "")
|
|
};
|
|
});
|
|
}).catch(() => null);
|
|
if (showLoading && typeof setStatus === "function") setStatus("requestModal", "", "");
|
|
} catch (error) {
|
|
setRequestModal((prev) => ({
|
|
...prev,
|
|
loading: false,
|
|
requestId,
|
|
requestData: null,
|
|
financeSummary: null,
|
|
invoices: [],
|
|
statusRouteNodes: [],
|
|
statusHistory: [],
|
|
availableStatuses: [],
|
|
currentImportantDateAt: "",
|
|
messages: [],
|
|
messagesHasMore: false,
|
|
messagesLoadingMore: false,
|
|
messagesLoadedCount: 0,
|
|
messagesTotal: 0,
|
|
attachments: [],
|
|
selectedFiles: [],
|
|
fileUploading: false
|
|
}));
|
|
if (typeof setStatus === "function") setStatus("requestModal", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, hydrateRequestMessageBodies, resolveAdminObjectSrc2, setStatus, token, users]
|
|
);
|
|
const refreshRequestModal = useCallback2(async () => {
|
|
if (!requestModal.requestId) return;
|
|
await loadRequestModalData(requestModal.requestId, { showLoading: true });
|
|
}, [loadRequestModalData, requestModal.requestId]);
|
|
const openRequestDetails = useCallback2(
|
|
async (requestId, event, options2) => {
|
|
if (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
if (!requestId) return;
|
|
const normalizedRequestId = String(requestId);
|
|
const now = Date.now();
|
|
const prev = requestOpenGuardRef.current;
|
|
if (prev.requestId === normalizedRequestId && now - prev.ts < 900) return;
|
|
requestOpenGuardRef.current = { requestId: normalizedRequestId, ts: now };
|
|
if (window.location.pathname !== "/admin.html" || window.location.search) {
|
|
window.history.replaceState(null, "", "/admin.html");
|
|
}
|
|
if (typeof setStatus === "function") setStatus("requestModal", "", "");
|
|
if (typeof setActiveSection === "function") setActiveSection("requestWorkspace");
|
|
await loadRequestModalData(normalizedRequestId, { showLoading: true });
|
|
const preset = options2 && typeof options2 === "object" ? options2.statusChangePreset : null;
|
|
if (preset) {
|
|
setRequestModal((prev2) => ({ ...prev2, pendingStatusChangePreset: preset }));
|
|
}
|
|
},
|
|
[loadRequestModalData, setActiveSection, setStatus]
|
|
);
|
|
const submitRequestModalMessage = useCallback2(
|
|
async (event) => {
|
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
if (!api) return;
|
|
const requestId = requestModal.requestId;
|
|
const body = String(requestModal.messageDraft || "").trim();
|
|
const files = Array.isArray(requestModal.selectedFiles) ? requestModal.selectedFiles : [];
|
|
if (!requestId || !body && !files.length) return;
|
|
try {
|
|
setRequestModal((prev) => ({ ...prev, fileUploading: true }));
|
|
if (typeof setStatus === "function") {
|
|
setStatus("requestModal", files.length ? "\u041E\u0442\u043F\u0440\u0430\u0432\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0438 \u0444\u0430\u0439\u043B\u043E\u0432..." : "\u041E\u0442\u043F\u0440\u0430\u0432\u043A\u0430 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F...", "");
|
|
}
|
|
let messageId = null;
|
|
if (body) {
|
|
const message = await api("/api/admin/chat/requests/" + requestId + "/messages", {
|
|
method: "POST",
|
|
body: { body }
|
|
});
|
|
messageId = String(message?.id || "").trim() || null;
|
|
}
|
|
for (const file of files) {
|
|
await uploadRequestAttachmentWithRetry({ requestId, file, messageId });
|
|
}
|
|
setRequestModal((prev) => ({ ...prev, messageDraft: "", selectedFiles: [], fileUploading: false }));
|
|
const successMessage = body && files.length ? "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0438 \u0444\u0430\u0439\u043B\u044B \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B" : files.length ? "\u0424\u0430\u0439\u043B\u044B \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u044B" : "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E";
|
|
if (typeof setStatus === "function") setStatus("requestModal", successMessage, "ok");
|
|
await loadRequestModalData(requestId, { showLoading: false });
|
|
} catch (error) {
|
|
setRequestModal((prev) => ({ ...prev, fileUploading: false }));
|
|
if (typeof setStatus === "function") setStatus("requestModal", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438: " + error.message, "error");
|
|
}
|
|
},
|
|
[
|
|
api,
|
|
loadRequestModalData,
|
|
requestModal.messageDraft,
|
|
requestModal.requestId,
|
|
requestModal.selectedFiles,
|
|
setStatus,
|
|
uploadRequestAttachmentWithRetry
|
|
]
|
|
);
|
|
const loadRequestDataTemplates = useCallback2(
|
|
async (documentName) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId) return { rows: [], documents: [] };
|
|
const query = documentName ? "?document=" + encodeURIComponent(String(documentName)) : "";
|
|
return api("/api/admin/chat/requests/" + requestId + "/data-request-templates" + query);
|
|
},
|
|
[api, requestModal.requestId]
|
|
);
|
|
const loadRequestDataBatch = useCallback2(
|
|
async (messageId) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId || !messageId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430");
|
|
return api("/api/admin/chat/requests/" + requestId + "/data-requests/" + encodeURIComponent(String(messageId)));
|
|
},
|
|
[api, requestModal.requestId]
|
|
);
|
|
const loadRequestDataTemplateDetails = useCallback2(
|
|
async (templateId) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId || !templateId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D \u0448\u0430\u0431\u043B\u043E\u043D");
|
|
return api(
|
|
"/api/admin/chat/requests/" + requestId + "/data-request-templates/" + encodeURIComponent(String(templateId))
|
|
);
|
|
},
|
|
[api, requestModal.requestId]
|
|
);
|
|
const saveRequestDataTemplate = useCallback2(
|
|
async (payload) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430");
|
|
return api("/api/admin/chat/requests/" + requestId + "/data-request-templates", {
|
|
method: "POST",
|
|
body: payload || {}
|
|
});
|
|
},
|
|
[api, requestModal.requestId]
|
|
);
|
|
const saveRequestDataBatch = useCallback2(
|
|
async (payload) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430");
|
|
const result = await api("/api/admin/chat/requests/" + requestId + "/data-requests", {
|
|
method: "POST",
|
|
body: payload || {}
|
|
});
|
|
await loadRequestModalData(requestId, { showLoading: false });
|
|
return result;
|
|
},
|
|
[api, loadRequestModalData, requestModal.requestId]
|
|
);
|
|
const clearPendingStatusChangePreset = useCallback2(() => {
|
|
setRequestModal((prev) => ({ ...prev, pendingStatusChangePreset: null }));
|
|
}, []);
|
|
const probeRequestLive = useCallback2(
|
|
async ({ cursor } = {}) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId) return { has_updates: false, typing: [], cursor: null };
|
|
const query = cursor ? "?cursor=" + encodeURIComponent(String(cursor)) : "";
|
|
const payload = await api("/api/admin/chat/requests/" + requestId + "/live" + query);
|
|
if (payload && payload.has_updates) {
|
|
const nextMessages = normalizeMessageAuthors(payload?.messages || [], users);
|
|
const nextAttachments = (payload?.attachments || []).map((item) => ({
|
|
...item,
|
|
download_url: resolveAdminObjectSrc2(item?.s3_key, token)
|
|
}));
|
|
if (nextMessages.length || nextAttachments.length) {
|
|
setRequestModal((prev) => {
|
|
const mergedMessages = mergeRowsById(prev.messages, nextMessages);
|
|
const previousCount = Array.isArray(prev.messages) ? prev.messages.length : 0;
|
|
const addedCount = Math.max(0, mergedMessages.length - previousCount);
|
|
return {
|
|
...prev,
|
|
messages: mergedMessages,
|
|
messagesLoadedCount: Number(prev.messagesLoadedCount || previousCount) + addedCount,
|
|
messagesTotal: Number(prev.messagesTotal || previousCount) + addedCount,
|
|
attachments: mergeRowsById(prev.attachments, nextAttachments)
|
|
};
|
|
});
|
|
}
|
|
}
|
|
return payload || { has_updates: false, typing: [], cursor: null };
|
|
},
|
|
[api, requestModal.requestId, resolveAdminObjectSrc2, token, users]
|
|
);
|
|
const loadOlderRequestMessages = useCallback2(async () => {
|
|
const requestId = String(requestModal.requestId || "").trim();
|
|
const cursor = getOldestMessageCursor(requestModal.messages);
|
|
if (!api || !requestId || requestModal.messagesLoadingMore || !requestModal.messagesHasMore) return null;
|
|
setRequestModal((prev) => ({ ...prev, messagesLoadingMore: true }));
|
|
try {
|
|
const query = new URLSearchParams({ include_body: "false" });
|
|
if (cursor?.beforeId && cursor?.beforeCreatedAt) {
|
|
query.set("before_id", cursor.beforeId);
|
|
query.set("before_created_at", cursor.beforeCreatedAt);
|
|
} else {
|
|
query.set("before_count", String(Number(requestModal.messagesLoadedCount || 0)));
|
|
}
|
|
const payload = await api("/api/admin/chat/requests/" + requestId + "/messages-window?" + query.toString());
|
|
const nextMessages = normalizeMessageAuthors(payload?.rows || [], users);
|
|
setRequestModal((prev) => ({
|
|
...prev,
|
|
messagesLoadingMore: false,
|
|
...(function() {
|
|
const merged = mergeRowsById(nextMessages, prev.messages);
|
|
return {
|
|
messages: merged,
|
|
messagesHasMore: Boolean(payload?.has_more),
|
|
messagesLoadedCount: merged.length,
|
|
messagesTotal: Number(prev.messagesTotal || merged.length || 0)
|
|
};
|
|
})()
|
|
}));
|
|
void hydrateRequestMessageBodies(requestId, nextMessages);
|
|
return payload || null;
|
|
} catch (error) {
|
|
setRequestModal((prev) => ({ ...prev, messagesLoadingMore: false }));
|
|
if (typeof setStatus === "function") setStatus("requestModal", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0438\u0441\u0442\u043E\u0440\u0438\u0438: " + error.message, "error");
|
|
return null;
|
|
}
|
|
}, [
|
|
api,
|
|
requestModal.messagesHasMore,
|
|
requestModal.messagesLoadedCount,
|
|
requestModal.messagesLoadingMore,
|
|
requestModal.requestId,
|
|
setStatus,
|
|
hydrateRequestMessageBodies,
|
|
users
|
|
]);
|
|
const setRequestTyping = useCallback2(
|
|
async ({ typing } = {}) => {
|
|
const requestId = requestModal.requestId;
|
|
if (!api || !requestId) return { status: "skipped", typing: false };
|
|
return api("/api/admin/chat/requests/" + requestId + "/typing", {
|
|
method: "POST",
|
|
body: { typing: Boolean(typing) }
|
|
});
|
|
},
|
|
[api, requestModal.requestId]
|
|
);
|
|
const submitRequestStatusChange = useCallback2(
|
|
async ({ requestId, statusCode, importantDateAt, comment, files } = {}) => {
|
|
if (!api) throw new Error("API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D");
|
|
const targetRequestId = String(requestId || requestModal.requestId || "").trim();
|
|
if (!targetRequestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430");
|
|
const nextStatus = String(statusCode || "").trim();
|
|
if (!nextStatus) throw new Error("\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u0442\u0443\u0441");
|
|
const body = {
|
|
status_code: nextStatus,
|
|
important_date_at: importantDateAt || null,
|
|
comment: String(comment || "").trim() || null
|
|
};
|
|
if (typeof setStatus === "function") setStatus("requestModal", "\u0421\u043C\u0435\u043D\u0430 \u0441\u0442\u0430\u0442\u0443\u0441\u0430...", "");
|
|
const result = await api("/api/admin/requests/" + targetRequestId + "/status-change", {
|
|
method: "POST",
|
|
body
|
|
});
|
|
const attachedFiles = Array.isArray(files) ? files.filter(Boolean) : [];
|
|
const commentText = String(comment || "").trim();
|
|
const availableStatuses = Array.isArray(requestModal.availableStatuses) ? requestModal.availableStatuses : [];
|
|
const statusName = availableStatuses.find((item) => String(item?.code || "").trim() === String(result?.to_status || nextStatus).trim())?.name;
|
|
const nextStatusLabel = String(statusName || result?.to_status || nextStatus).trim() || nextStatus;
|
|
const importantDateRaw = String(result?.important_date_at || importantDateAt || "").trim();
|
|
const importantDateLabel = importantDateRaw ? fmtShortDateTime(importantDateRaw) : "";
|
|
const serviceLines = [`\u0418\u0437\u043C\u0435\u043D\u0438\u043B\u0441\u044F \u0441\u0442\u0430\u0442\u0443\u0441: "${nextStatusLabel}"`];
|
|
if (importantDateRaw) {
|
|
serviceLines.push("\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430: " + (importantDateLabel && importantDateLabel !== "-" ? importantDateLabel : importantDateRaw));
|
|
}
|
|
if (commentText) serviceLines.push(commentText);
|
|
let messageId = null;
|
|
const serviceMessageBody = serviceLines.filter(Boolean).join("\n").trim();
|
|
if (serviceMessageBody) {
|
|
const message = await api("/api/admin/chat/requests/" + targetRequestId + "/messages", {
|
|
method: "POST",
|
|
body: { body: serviceMessageBody }
|
|
});
|
|
messageId = String(message?.id || "").trim() || null;
|
|
}
|
|
for (const file of attachedFiles) {
|
|
await uploadRequestAttachmentWithRetry({ requestId: targetRequestId, file, messageId });
|
|
}
|
|
if (typeof setStatus === "function") setStatus("requestModal", "\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u044F\u0432\u043A\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok");
|
|
await loadRequestModalData(targetRequestId, { showLoading: false });
|
|
return result;
|
|
},
|
|
[api, loadRequestModalData, requestModal.availableStatuses, requestModal.requestId, setStatus, uploadRequestAttachmentWithRetry]
|
|
);
|
|
const issueRequestInvoice = useCallback2(
|
|
async ({ requestId, amount, serviceDescription, payerDisplayName } = {}) => {
|
|
if (!api) throw new Error("API \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D");
|
|
const targetRequestId = String(requestId || requestModal.requestId || "").trim();
|
|
if (!targetRequestId) throw new Error("\u041D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0430");
|
|
const parsedAmount = Number(amount);
|
|
if (!Number.isFinite(parsedAmount) || parsedAmount <= 0) {
|
|
throw new Error("\u0421\u0443\u043C\u043C\u0430 \u0441\u0447\u0435\u0442\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0431\u044B\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0443\u043B\u044F");
|
|
}
|
|
const roundedAmount = Math.round((parsedAmount + Number.EPSILON) * 100) / 100;
|
|
const rowData = requestModal.requestData && typeof requestModal.requestData === "object" ? requestModal.requestData : null;
|
|
const payerName = String(payerDisplayName || rowData?.client_name || "").trim() || "\u041A\u043B\u0438\u0435\u043D\u0442";
|
|
const serviceLabel = String(serviceDescription || "").trim() || "\u042E\u0440\u0438\u0434\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0443\u0441\u043B\u0443\u0433\u0438";
|
|
const trackNumber = String(rowData?.track_number || requestModal.trackNumber || "").trim();
|
|
const topicLabel = String(rowData?.topic_name || rowData?.topic_code || "").trim();
|
|
if (typeof setStatus === "function") setStatus("requestModal", "\u0412\u044B\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u043C \u0441\u0447\u0435\u0442...", "");
|
|
const created = await api("/api/admin/invoices", {
|
|
method: "POST",
|
|
body: {
|
|
request_id: targetRequestId,
|
|
status: "WAITING_PAYMENT",
|
|
amount: roundedAmount,
|
|
currency: "RUB",
|
|
payer_display_name: payerName,
|
|
payer_details: {
|
|
...DEFAULT_INVOICE_REQUISITES,
|
|
request_track_number: trackNumber,
|
|
service_description: serviceLabel,
|
|
topic_name: topicLabel
|
|
}
|
|
}
|
|
});
|
|
await loadRequestModalData(targetRequestId, { showLoading: false });
|
|
if (typeof setStatus === "function") {
|
|
const invoiceNumber = String(created?.invoice_number || "").trim();
|
|
setStatus("requestModal", invoiceNumber ? "\u0421\u0447\u0435\u0442 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D: " + invoiceNumber : "\u0421\u0447\u0435\u0442 \u0432\u044B\u0441\u0442\u0430\u0432\u043B\u0435\u043D", "ok");
|
|
}
|
|
return created;
|
|
},
|
|
[api, loadRequestModalData, requestModal.requestData, requestModal.requestId, requestModal.trackNumber, setStatus]
|
|
);
|
|
return {
|
|
requestModal,
|
|
setRequestModal,
|
|
requestOpenGuardRef,
|
|
resetRequestWorkspaceState,
|
|
updateRequestModalMessageDraft,
|
|
appendRequestModalFiles,
|
|
removeRequestModalFile,
|
|
clearRequestModalFiles,
|
|
loadRequestModalData,
|
|
refreshRequestModal,
|
|
openRequestDetails,
|
|
clearPendingStatusChangePreset,
|
|
submitRequestStatusChange,
|
|
submitRequestModalMessage,
|
|
probeRequestLive,
|
|
loadOlderRequestMessages,
|
|
setRequestTyping,
|
|
loadRequestDataTemplates,
|
|
loadRequestDataBatch,
|
|
loadRequestDataTemplateDetails,
|
|
saveRequestDataTemplate,
|
|
saveRequestDataBatch,
|
|
issueRequestInvoice
|
|
};
|
|
}
|
|
|
|
// app/web/admin/hooks/useTableActions.js
|
|
function useTableActions({ api, setStatus, resolveTableConfig, tablesRef, setTableState, setDictionaries, buildUniversalQuery: buildUniversalQuery2 }) {
|
|
const { useCallback: useCallback2 } = React;
|
|
const loadTable = useCallback2(
|
|
async (tableKey, options, tokenOverride) => {
|
|
const opts = options || {};
|
|
const config = resolveTableConfig(tableKey);
|
|
if (!config) return false;
|
|
const current = tablesRef.current[tableKey] || createTableState();
|
|
const next = {
|
|
...current,
|
|
filters: Array.isArray(opts.filtersOverride) ? [...opts.filtersOverride] : [...current.filters || []],
|
|
sort: Array.isArray(opts.sortOverride) ? [...opts.sortOverride] : Array.isArray(current.sort) ? [...current.sort] : null,
|
|
rows: [...current.rows || []]
|
|
};
|
|
if (opts.resetOffset) {
|
|
next.offset = 0;
|
|
next.showAll = false;
|
|
}
|
|
if (opts.loadAll) {
|
|
next.offset = 0;
|
|
next.showAll = true;
|
|
}
|
|
const statusKey = tableKey;
|
|
setStatus(statusKey, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", "");
|
|
try {
|
|
const activeSort = next.sort && next.sort.length ? next.sort : config.sort;
|
|
let limit = next.showAll ? Math.max(next.total || PAGE_SIZE, PAGE_SIZE) : PAGE_SIZE;
|
|
const offset = next.showAll ? 0 : next.offset;
|
|
let data = await api(
|
|
config.endpoint,
|
|
{
|
|
method: "POST",
|
|
body: buildUniversalQuery2(next.filters, activeSort, limit, offset)
|
|
},
|
|
tokenOverride
|
|
);
|
|
next.total = Number(data.total || 0);
|
|
next.rows = data.rows || [];
|
|
if (next.showAll && next.total > next.rows.length) {
|
|
limit = next.total;
|
|
data = await api(
|
|
config.endpoint,
|
|
{
|
|
method: "POST",
|
|
body: buildUniversalQuery2(next.filters, activeSort, limit, 0)
|
|
},
|
|
tokenOverride
|
|
);
|
|
next.total = Number(data.total || next.total);
|
|
next.rows = data.rows || [];
|
|
}
|
|
if (!next.showAll && next.total > 0 && next.offset >= next.total) {
|
|
next.offset = Math.floor((next.total - 1) / PAGE_SIZE) * PAGE_SIZE;
|
|
setTableState(tableKey, next);
|
|
return loadTable(tableKey, {}, tokenOverride);
|
|
}
|
|
setTableState(tableKey, next);
|
|
if (tableKey === "requests") {
|
|
setDictionaries((prev) => {
|
|
const map = new Map((prev.topics || []).map((topic) => [topic.code, topic]));
|
|
(next.rows || []).forEach((row) => {
|
|
if (!row.topic_code || map.has(row.topic_code)) return;
|
|
map.set(row.topic_code, { code: row.topic_code, name: row.topic_code });
|
|
});
|
|
return { ...prev, topics: sortByName(Array.from(map.values())) };
|
|
});
|
|
}
|
|
if (tableKey === "topics") {
|
|
setDictionaries((prev) => {
|
|
const map = new Map((prev.topics || []).map((t) => [t.code, t]));
|
|
(next.rows || []).forEach((row) => {
|
|
if (row.code) map.set(row.code, { code: row.code, name: row.name || row.code });
|
|
});
|
|
return { ...prev, topics: sortByName(Array.from(map.values())) };
|
|
});
|
|
}
|
|
if (tableKey === "statuses") {
|
|
setDictionaries((prev) => {
|
|
const map = new Map(Object.entries(STATUS_LABELS).map(([code, name]) => [code, { code, name }]));
|
|
(next.rows || []).forEach((row) => {
|
|
if (!row.code) return;
|
|
map.set(row.code, { code: row.code, name: row.name || statusLabel(row.code) });
|
|
});
|
|
return { ...prev, statuses: sortByName(Array.from(map.values())) };
|
|
});
|
|
}
|
|
if (tableKey === "formFields" || tableKey === "form_fields") {
|
|
setDictionaries((prev) => {
|
|
const set = new Set(DEFAULT_FORM_FIELD_TYPES);
|
|
(next.rows || []).forEach((row) => {
|
|
if (row?.type) set.add(row.type);
|
|
});
|
|
const fieldKeys = (next.rows || []).filter((row) => row && row.key).map((row) => ({ key: row.key, label: row.label || row.key })).sort((a, b) => String(a.label || a.key).localeCompare(String(b.label || b.key), "ru"));
|
|
return {
|
|
...prev,
|
|
formFieldTypes: Array.from(set.values()).sort((a, b) => String(a).localeCompare(String(b), "ru")),
|
|
formFieldKeys: fieldKeys
|
|
};
|
|
});
|
|
}
|
|
if (tableKey === "users" || tableKey === "admin_users") {
|
|
setDictionaries((prev) => {
|
|
const map = new Map((prev.users || []).map((user) => [user.id, user]));
|
|
(next.rows || []).forEach((row) => {
|
|
map.set(row.id, {
|
|
id: row.id,
|
|
name: row.name || "",
|
|
email: row.email || "",
|
|
role: row.role || "",
|
|
is_active: Boolean(row.is_active)
|
|
});
|
|
});
|
|
return { ...prev, users: Array.from(map.values()) };
|
|
});
|
|
}
|
|
setStatus(statusKey, "\u0421\u043F\u0438\u0441\u043E\u043A \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok");
|
|
return true;
|
|
} catch (error) {
|
|
setStatus(statusKey, "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
return false;
|
|
}
|
|
},
|
|
[api, buildUniversalQuery2, resolveTableConfig, setDictionaries, setStatus, setTableState, tablesRef]
|
|
);
|
|
const loadPrevPage = useCallback2(
|
|
(tableKey) => {
|
|
const tableState = tablesRef.current[tableKey] || createTableState();
|
|
const next = { ...tableState, offset: Math.max(0, tableState.offset - PAGE_SIZE), showAll: false };
|
|
setTableState(tableKey, next);
|
|
loadTable(tableKey, {});
|
|
},
|
|
[loadTable, setTableState, tablesRef]
|
|
);
|
|
const loadNextPage = useCallback2(
|
|
(tableKey) => {
|
|
const tableState = tablesRef.current[tableKey] || createTableState();
|
|
if (tableState.offset + PAGE_SIZE >= tableState.total) return;
|
|
const next = { ...tableState, offset: tableState.offset + PAGE_SIZE, showAll: false };
|
|
setTableState(tableKey, next);
|
|
loadTable(tableKey, {});
|
|
},
|
|
[loadTable, setTableState, tablesRef]
|
|
);
|
|
const loadAllRows = useCallback2(
|
|
(tableKey) => {
|
|
const tableState = tablesRef.current[tableKey] || createTableState();
|
|
if (!tableState.total) return;
|
|
const next = { ...tableState, offset: 0, showAll: true };
|
|
setTableState(tableKey, next);
|
|
loadTable(tableKey, { loadAll: true });
|
|
},
|
|
[loadTable, setTableState, tablesRef]
|
|
);
|
|
const toggleTableSort = useCallback2(
|
|
(tableKey, field) => {
|
|
const tableState = tablesRef.current[tableKey] || createTableState();
|
|
const currentSort = Array.isArray(tableState.sort) ? tableState.sort[0] : null;
|
|
const dir = currentSort && currentSort.field === field ? currentSort.dir === "asc" ? "desc" : "asc" : "asc";
|
|
const sortOverride = [{ field, dir }];
|
|
const next = { ...tableState, sort: sortOverride, offset: 0, showAll: false };
|
|
setTableState(tableKey, next);
|
|
loadTable(tableKey, { resetOffset: true, sortOverride });
|
|
},
|
|
[loadTable, setTableState, tablesRef]
|
|
);
|
|
return {
|
|
loadTable,
|
|
loadPrevPage,
|
|
loadNextPage,
|
|
loadAllRows,
|
|
toggleTableSort
|
|
};
|
|
}
|
|
|
|
// app/web/admin/hooks/useTableFilterActions.js
|
|
function useTableFilterActions({
|
|
filterModal,
|
|
closeFilterModal,
|
|
getFieldDef,
|
|
loadKanban,
|
|
loadTable,
|
|
setStatus,
|
|
setTableState,
|
|
tablesRef
|
|
}) {
|
|
const { useCallback: useCallback2 } = React;
|
|
const applyFilterModal = useCallback2(
|
|
async (event) => {
|
|
if (event && typeof event.preventDefault === "function") event.preventDefault();
|
|
if (!filterModal.tableKey) return;
|
|
const fieldDef = getFieldDef(filterModal.tableKey, filterModal.field);
|
|
if (!fieldDef) {
|
|
setStatus("filter", "\u041F\u043E\u043B\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043E", "error");
|
|
return;
|
|
}
|
|
let value;
|
|
if (fieldDef.type === "boolean") {
|
|
value = filterModal.rawValue === "true";
|
|
} else if (fieldDef.type === "number") {
|
|
if (String(filterModal.rawValue || "").trim() === "") {
|
|
setStatus("filter", "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043B\u043E", "error");
|
|
return;
|
|
}
|
|
value = Number(filterModal.rawValue);
|
|
if (Number.isNaN(value)) {
|
|
setStatus("filter", "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E", "error");
|
|
return;
|
|
}
|
|
} else {
|
|
value = String(filterModal.rawValue || "").trim();
|
|
if (!value) {
|
|
setStatus("filter", "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u0430", "error");
|
|
return;
|
|
}
|
|
}
|
|
const tableState = tablesRef.current[filterModal.tableKey] || createTableState();
|
|
const nextFilters = [...tableState.filters || []];
|
|
const nextClause = { field: fieldDef.field, op: filterModal.op, value };
|
|
if (Number.isInteger(filterModal.editIndex) && filterModal.editIndex >= 0 && filterModal.editIndex < nextFilters.length) {
|
|
nextFilters[filterModal.editIndex] = nextClause;
|
|
} else {
|
|
const existingIndex = nextFilters.findIndex((item) => item.field === nextClause.field && item.op === nextClause.op);
|
|
if (existingIndex >= 0) nextFilters[existingIndex] = nextClause;
|
|
else nextFilters.push(nextClause);
|
|
}
|
|
setTableState(filterModal.tableKey, {
|
|
...tableState,
|
|
filters: nextFilters,
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
closeFilterModal();
|
|
if (filterModal.tableKey === "kanban") {
|
|
await loadKanban(void 0, { filtersOverride: nextFilters });
|
|
} else {
|
|
await loadTable(filterModal.tableKey, { resetOffset: true, filtersOverride: nextFilters });
|
|
}
|
|
},
|
|
[closeFilterModal, filterModal, getFieldDef, loadKanban, loadTable, setStatus, setTableState, tablesRef]
|
|
);
|
|
const clearFiltersFromModal = useCallback2(async () => {
|
|
if (!filterModal.tableKey) return;
|
|
const tableState = tablesRef.current[filterModal.tableKey] || createTableState();
|
|
setTableState(filterModal.tableKey, {
|
|
...tableState,
|
|
filters: [],
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
closeFilterModal();
|
|
if (filterModal.tableKey === "kanban") {
|
|
await loadKanban(void 0, { filtersOverride: [] });
|
|
} else {
|
|
await loadTable(filterModal.tableKey, { resetOffset: true, filtersOverride: [] });
|
|
}
|
|
}, [closeFilterModal, filterModal.tableKey, loadKanban, loadTable, setTableState, tablesRef]);
|
|
const removeFilterChip = useCallback2(
|
|
async (tableKey, index) => {
|
|
const tableState = tablesRef.current[tableKey] || createTableState();
|
|
const nextFilters = [...tableState.filters || []];
|
|
nextFilters.splice(index, 1);
|
|
setTableState(tableKey, {
|
|
...tableState,
|
|
filters: nextFilters,
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
if (tableKey === "kanban") {
|
|
await loadKanban(void 0, { filtersOverride: nextFilters });
|
|
} else {
|
|
await loadTable(tableKey, { resetOffset: true, filtersOverride: nextFilters });
|
|
}
|
|
},
|
|
[loadKanban, loadTable, setTableState, tablesRef]
|
|
);
|
|
return {
|
|
applyFilterModal,
|
|
clearFiltersFromModal,
|
|
removeFilterChip
|
|
};
|
|
}
|
|
|
|
// app/web/admin/hooks/useTablesState.js
|
|
function createInitialTablesState() {
|
|
return {
|
|
kanban: createTableState(),
|
|
requests: createTableState(),
|
|
serviceRequests: createTableState(),
|
|
invoices: createTableState(),
|
|
quotes: createTableState(),
|
|
topics: createTableState(),
|
|
statuses: createTableState(),
|
|
formFields: createTableState(),
|
|
topicRequiredFields: createTableState(),
|
|
topicDataTemplates: createTableState(),
|
|
statusTransitions: createTableState(),
|
|
users: createTableState(),
|
|
userTopics: createTableState(),
|
|
availableTables: createTableState()
|
|
};
|
|
}
|
|
function useTablesState() {
|
|
const { useCallback: useCallback2, useEffect: useEffect4, useRef: useRef4, useState: useState4 } = React;
|
|
const [tables, setTables] = useState4(createInitialTablesState);
|
|
const [tableCatalog, setTableCatalog] = useState4([]);
|
|
const [referenceRowsMap, setReferenceRowsMap] = useState4({});
|
|
const tablesRef = useRef4(tables);
|
|
useEffect4(() => {
|
|
tablesRef.current = tables;
|
|
}, [tables]);
|
|
const setTableState = useCallback2((tableKey, next) => {
|
|
setTables((prev) => ({ ...prev, [tableKey]: next }));
|
|
}, []);
|
|
const resetTablesState = useCallback2(() => {
|
|
setTables(createInitialTablesState());
|
|
setTableCatalog([]);
|
|
setReferenceRowsMap({});
|
|
}, []);
|
|
return {
|
|
tables,
|
|
setTables,
|
|
tablesRef,
|
|
setTableState,
|
|
resetTablesState,
|
|
tableCatalog,
|
|
setTableCatalog,
|
|
referenceRowsMap,
|
|
setReferenceRowsMap
|
|
};
|
|
}
|
|
|
|
// app/web/admin/shared/AvatarCropEditor.jsx
|
|
var { useCallback, useEffect: useEffect2, useRef: useRef2, useState: useState2 } = React;
|
|
var VIEWPORT_PX = 320;
|
|
var ZOOM_MIN = 1;
|
|
var ZOOM_MAX = 4;
|
|
var ZOOM_STEP = 0.01;
|
|
function AvatarCropEditor({ imageFile, initialCrop, onApply, onCancel }) {
|
|
const [objectUrl, setObjectUrl] = useState2(null);
|
|
const [naturalW, setNaturalW] = useState2(0);
|
|
const [naturalH, setNaturalH] = useState2(0);
|
|
const [loaded, setLoaded] = useState2(false);
|
|
const [panX, setPanX] = useState2(initialCrop?.x ?? 0);
|
|
const [panY, setPanY] = useState2(initialCrop?.y ?? 0);
|
|
const [zoom, setZoom] = useState2(
|
|
Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, initialCrop?.zoom ?? 1))
|
|
);
|
|
const dragRef = useRef2(null);
|
|
const viewportRef = useRef2(null);
|
|
useEffect2(() => {
|
|
if (!imageFile) return;
|
|
const url = URL.createObjectURL(imageFile);
|
|
setObjectUrl(url);
|
|
setLoaded(false);
|
|
setPanX(initialCrop?.x ?? 0);
|
|
setPanY(initialCrop?.y ?? 0);
|
|
setZoom(Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, initialCrop?.zoom ?? 1)));
|
|
return () => URL.revokeObjectURL(url);
|
|
}, [imageFile, initialCrop]);
|
|
const handleImageLoad = useCallback(
|
|
(event) => {
|
|
setNaturalW(event.currentTarget.naturalWidth);
|
|
setNaturalH(event.currentTarget.naturalHeight);
|
|
setLoaded(true);
|
|
},
|
|
[]
|
|
);
|
|
const minSide = Math.min(naturalW, naturalH) || 1;
|
|
const cropSrcW = minSide / zoom;
|
|
const cropSrcH = minSide / zoom;
|
|
const displayScale = naturalW && naturalH ? VIEWPORT_PX / cropSrcW : 1;
|
|
const offsetX = (naturalW - cropSrcW) / 2;
|
|
const offsetY = (naturalH - cropSrcH) / 2;
|
|
const cx = naturalW / 2 + panX * offsetX;
|
|
const cy = naturalH / 2 + panY * offsetY;
|
|
const imgW = naturalW * displayScale;
|
|
const imgH = naturalH * displayScale;
|
|
const imgLeft = VIEWPORT_PX / 2 - cx * displayScale;
|
|
const imgTop = VIEWPORT_PX / 2 - cy * displayScale;
|
|
const stopDrag = useCallback(() => {
|
|
dragRef.current = null;
|
|
window.removeEventListener("mousemove", onMouseMove);
|
|
window.removeEventListener("mouseup", onMouseUp);
|
|
window.removeEventListener("touchmove", onTouchMove);
|
|
window.removeEventListener("touchend", onMouseUp);
|
|
}, []);
|
|
const applyDelta = useCallback(
|
|
(dxPx, dyPx) => {
|
|
if (!dragRef.current) return;
|
|
const { startPanX, startPanY } = dragRef.current;
|
|
const panDx = offsetX > 0 ? dxPx / (displayScale * offsetX) : 0;
|
|
const panDy = offsetY > 0 ? dyPx / (displayScale * offsetY) : 0;
|
|
setPanX(Math.max(-1, Math.min(1, startPanX - panDx)));
|
|
setPanY(Math.max(-1, Math.min(1, startPanY - panDy)));
|
|
},
|
|
[displayScale, offsetX, offsetY]
|
|
);
|
|
const onMouseMove = useCallback(
|
|
(event) => {
|
|
if (!dragRef.current) return;
|
|
applyDelta(
|
|
event.clientX - dragRef.current.startX,
|
|
event.clientY - dragRef.current.startY
|
|
);
|
|
},
|
|
[applyDelta]
|
|
);
|
|
const onTouchMove = useCallback(
|
|
(event) => {
|
|
if (!dragRef.current) return;
|
|
event.preventDefault();
|
|
const t = event.touches[0];
|
|
applyDelta(
|
|
t.clientX - dragRef.current.startX,
|
|
t.clientY - dragRef.current.startY
|
|
);
|
|
},
|
|
[applyDelta]
|
|
);
|
|
const onMouseUp = useCallback(() => stopDrag(), [stopDrag]);
|
|
const startDrag = useCallback(
|
|
(clientX, clientY) => {
|
|
dragRef.current = {
|
|
startX: clientX,
|
|
startY: clientY,
|
|
startPanX: panX,
|
|
startPanY: panY
|
|
};
|
|
window.addEventListener("mousemove", onMouseMove);
|
|
window.addEventListener("mouseup", onMouseUp);
|
|
window.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
window.addEventListener("touchend", onMouseUp);
|
|
},
|
|
[panX, panY, onMouseMove, onMouseUp, onTouchMove]
|
|
);
|
|
const handleMouseDown = useCallback(
|
|
(event) => {
|
|
event.preventDefault();
|
|
startDrag(event.clientX, event.clientY);
|
|
},
|
|
[startDrag]
|
|
);
|
|
const handleTouchStart = useCallback(
|
|
(event) => {
|
|
const t = event.touches[0];
|
|
startDrag(t.clientX, t.clientY);
|
|
},
|
|
[startDrag]
|
|
);
|
|
useEffect2(
|
|
() => () => stopDrag(),
|
|
[stopDrag]
|
|
);
|
|
const handleApply = useCallback(() => {
|
|
if (!imageFile || !onApply) return;
|
|
onApply({
|
|
file: imageFile,
|
|
cropJson: { x: panX, y: panY, zoom }
|
|
});
|
|
}, [imageFile, onApply, panX, panY, zoom]);
|
|
return /* @__PURE__ */ React.createElement("div", { className: "avatar-crop-editor" }, /* @__PURE__ */ React.createElement("p", { className: "avatar-crop-hint" }, "\u041F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0432\u044B\u0431\u0440\u0430\u0442\u044C \u043E\u0431\u043B\u0430\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441\u0430"), /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
ref: viewportRef,
|
|
className: "avatar-crop-viewport",
|
|
onMouseDown: handleMouseDown,
|
|
onTouchStart: handleTouchStart,
|
|
"aria-label": "\u041E\u0431\u043B\u0430\u0441\u0442\u044C \u043A\u0430\u0434\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0430\u0432\u0430\u0442\u0430\u0440\u0430"
|
|
},
|
|
objectUrl ? /* @__PURE__ */ React.createElement(
|
|
"img",
|
|
{
|
|
src: objectUrl,
|
|
alt: "",
|
|
draggable: false,
|
|
onLoad: handleImageLoad,
|
|
style: loaded ? {
|
|
position: "absolute",
|
|
width: imgW + "px",
|
|
height: imgH + "px",
|
|
left: imgLeft + "px",
|
|
top: imgTop + "px",
|
|
pointerEvents: "none",
|
|
userSelect: "none"
|
|
} : { opacity: 0 }
|
|
}
|
|
) : null,
|
|
!loaded ? /* @__PURE__ */ React.createElement("span", { className: "avatar-crop-loading" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026") : null
|
|
), /* @__PURE__ */ React.createElement("div", { className: "avatar-crop-controls" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "avatar-crop-zoom" }, "\u041C\u0430\u0441\u0448\u0442\u0430\u0431"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "avatar-crop-zoom",
|
|
type: "range",
|
|
min: ZOOM_MIN,
|
|
max: ZOOM_MAX,
|
|
step: ZOOM_STEP,
|
|
value: zoom,
|
|
onChange: (event) => setZoom(parseFloat(event.target.value))
|
|
}
|
|
), /* @__PURE__ */ React.createElement("span", null, zoom.toFixed(1), "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "avatar-crop-actions" }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "button", onClick: handleApply, disabled: !loaded }, "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onCancel }, "\u041E\u0442\u043C\u0435\u043D\u0430")));
|
|
}
|
|
|
|
// app/web/admin/shared/RecordModal.jsx
|
|
var { useEffect: useEffect3, useRef: useRef3, useState: useState3 } = React;
|
|
function RecordModal({
|
|
open,
|
|
title,
|
|
tableKey,
|
|
mode,
|
|
fields,
|
|
form,
|
|
status,
|
|
accessToken,
|
|
onClose,
|
|
onChange,
|
|
onSubmit,
|
|
onUploadField,
|
|
onUploadFieldWithCrop,
|
|
onRecropAvatar,
|
|
onApplyRecrop,
|
|
OverlayComponent,
|
|
IconButtonComponent,
|
|
UserAvatarComponent,
|
|
StatusLineComponent
|
|
}) {
|
|
const Overlay = OverlayComponent;
|
|
const IconButton = IconButtonComponent;
|
|
const UserAvatar = UserAvatarComponent;
|
|
const StatusLine = StatusLineComponent;
|
|
const [avatarPreviewOpen, setAvatarPreviewOpen] = useState3(false);
|
|
const [userEditing, setUserEditing] = useState3(false);
|
|
const [cropFile, setCropFile] = useState3(null);
|
|
const [cropInitial, setCropInitial] = useState3(null);
|
|
const isRecropRef = useRef3(false);
|
|
const avatarUploadRef = useRef3(null);
|
|
const visibleFields = (fields || []).filter((field) => {
|
|
if (field.hidden) return false;
|
|
if (typeof field.visibleWhen !== "function") return true;
|
|
try {
|
|
return Boolean(field.visibleWhen(form || {}));
|
|
} catch (_) {
|
|
return true;
|
|
}
|
|
});
|
|
const isUserModal = tableKey === "users";
|
|
const avatarField = isUserModal ? visibleFields.find((field) => field.key === "avatar_url") : null;
|
|
const topicField = isUserModal ? visibleFields.find((field) => field.key === "primary_topic_code") : null;
|
|
const formFields = isUserModal ? visibleFields.filter((field) => field.key !== "avatar_url") : visibleFields;
|
|
const fieldMap = new Map(visibleFields.map((field) => [field.key, field]));
|
|
const avatarValue = String(form?.avatar_url || "").trim();
|
|
const userName = String(form?.name || "").trim();
|
|
const userEmail = String(form?.email || "").trim();
|
|
const userPhone = String(form?.phone || "").trim();
|
|
const userRole = roleLabel(form?.role);
|
|
const topicOptions = topicField && typeof topicField.options === "function" ? topicField.options(form || {}) : [];
|
|
const currentTopicValue = String(form?.primary_topic_code || "").trim();
|
|
const userTopic = (topicOptions.find((option) => String(option?.value || "") === currentTopicValue)?.label || currentTopicValue || "").trim() || "\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D";
|
|
const defaultRate = String(form?.default_rate || "").trim();
|
|
const salaryPercent = String(form?.salary_percent || "").trim();
|
|
const userActiveRaw = String(form?.is_active ?? "");
|
|
const activeLabel = userActiveRaw === "false" ? "\u041D\u0435\u0430\u043A\u0442\u0438\u0432\u0435\u043D" : userActiveRaw === "true" || !userActiveRaw ? "\u0410\u043A\u0442\u0438\u0432\u0435\u043D" : "\u0421\u0442\u0430\u0442\u0443\u0441 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D";
|
|
const avatarPreviewSrc = avatarValue ? resolveAvatarSrc(avatarValue, accessToken, 512) : "";
|
|
const statusTone = userActiveRaw === "false" ? "danger" : userActiveRaw === "true" || !userActiveRaw ? "success" : "warn";
|
|
const isCreateMode = isUserModal && mode === "create";
|
|
useEffect3(() => {
|
|
if (!isUserModal) {
|
|
setUserEditing(false);
|
|
setAvatarPreviewOpen(false);
|
|
return;
|
|
}
|
|
setUserEditing(isCreateMode);
|
|
setAvatarPreviewOpen(false);
|
|
}, [isCreateMode, isUserModal, open, tableKey]);
|
|
if (!open) return null;
|
|
const renderField = (field) => {
|
|
const value = form[field.key] ?? "";
|
|
const options = typeof field.options === "function" ? field.options(form || {}) : [];
|
|
const id = "record-field-" + field.key;
|
|
const disabled = Boolean(field.readOnly) || (typeof field.readOnlyWhen === "function" ? Boolean(field.readOnlyWhen(form || {})) : false);
|
|
if (field.type === "textarea" || field.type === "json") {
|
|
return /* @__PURE__ */ React.createElement(
|
|
"textarea",
|
|
{
|
|
id,
|
|
value,
|
|
onChange: (event) => onChange(field.key, event.target.value),
|
|
placeholder: field.placeholder || "",
|
|
required: Boolean(field.required),
|
|
disabled
|
|
}
|
|
);
|
|
}
|
|
if (field.type === "boolean") {
|
|
return /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id,
|
|
value,
|
|
onChange: (nextValue) => onChange(field.key, nextValue),
|
|
options: [
|
|
{ value: "true", label: "\u0414\u0430" },
|
|
{ value: "false", label: "\u041D\u0435\u0442" }
|
|
],
|
|
disabled,
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"
|
|
}
|
|
);
|
|
}
|
|
if (field.type === "reference" || field.type === "enum") {
|
|
const extraOptions = Array.isArray(field.extraOptions) ? field.extraOptions : [];
|
|
const hasCurrentValue = String(value || "").trim() !== "" && [...extraOptions, ...options].some((option) => String(option?.value || "") === String(value));
|
|
const selectOptions = [];
|
|
if (field.optional) selectOptions.push({ value: "", label: "-" });
|
|
if (!hasCurrentValue && String(value || "").trim() !== "") selectOptions.push({ value: String(value), label: String(value) });
|
|
extraOptions.forEach((option) => selectOptions.push({ value: String(option.value), label: option.label }));
|
|
options.forEach((option) => selectOptions.push({ value: String(option.value), label: option.label }));
|
|
return /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id,
|
|
value,
|
|
onChange: (nextValue) => onChange(field.key, nextValue),
|
|
options: selectOptions,
|
|
disabled,
|
|
placeholder: field.optional ? "-" : field.placeholder || "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"
|
|
}
|
|
);
|
|
}
|
|
if (field.uploadScope) {
|
|
return /* @__PURE__ */ React.createElement("div", { className: "field-inline" }, /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id,
|
|
type: "text",
|
|
value,
|
|
onChange: (event) => onChange(field.key, event.target.value),
|
|
placeholder: field.placeholder || "",
|
|
required: Boolean(field.required),
|
|
disabled
|
|
}
|
|
), /* @__PURE__ */ React.createElement("label", { className: "btn secondary btn-sm", style: { whiteSpace: "nowrap", opacity: disabled ? 0.6 : 1, pointerEvents: disabled ? "none" : "auto" } }, "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C", /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
type: "file",
|
|
accept: field.accept || "*/*",
|
|
style: { display: "none" },
|
|
onChange: (event) => {
|
|
const file = event.target.files && event.target.files[0];
|
|
if (file && onUploadField) onUploadField(field, file);
|
|
event.target.value = "";
|
|
},
|
|
disabled
|
|
}
|
|
)));
|
|
}
|
|
return /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id,
|
|
type: field.type === "number" ? "number" : field.type === "password" ? "password" : "text",
|
|
step: field.type === "number" ? "any" : void 0,
|
|
value,
|
|
onChange: (event) => onChange(field.key, event.target.value),
|
|
placeholder: field.placeholder || "",
|
|
required: Boolean(field.required),
|
|
disabled
|
|
}
|
|
);
|
|
};
|
|
const renderUserCard = (fieldKey) => {
|
|
const field = fieldMap.get(fieldKey);
|
|
if (!field) return null;
|
|
const value = form[fieldKey] ?? "";
|
|
const isPassword = fieldKey === "password";
|
|
const inEdit = isCreateMode || userEditing;
|
|
let content = null;
|
|
if (inEdit) {
|
|
content = renderField(field);
|
|
} else if (isPassword) {
|
|
content = /* @__PURE__ */ React.createElement("span", { className: "record-user-card-value muted" }, "\u041F\u0430\u0440\u043E\u043B\u044C \u0441\u043A\u0440\u044B\u0442");
|
|
} else {
|
|
let displayValue = value;
|
|
if (fieldKey === "role") displayValue = userRole || "\u041D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0430";
|
|
if (fieldKey === "is_active") displayValue = activeLabel;
|
|
if (fieldKey === "primary_topic_code") displayValue = userTopic;
|
|
if (fieldKey === "default_rate") displayValue = defaultRate || "\u2014";
|
|
if (fieldKey === "salary_percent") displayValue = salaryPercent || "\u2014";
|
|
content = /* @__PURE__ */ React.createElement("span", { className: "record-user-card-value" }, String(displayValue || "\u041D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E"));
|
|
}
|
|
return /* @__PURE__ */ React.createElement("div", { className: "record-user-card", key: fieldKey }, /* @__PURE__ */ React.createElement("span", { className: "record-user-card-label" }, field.label), content);
|
|
};
|
|
const renderUserRateCard = () => {
|
|
const inEdit = isCreateMode || userEditing;
|
|
if (inEdit) {
|
|
return /* @__PURE__ */ React.createElement("div", { className: "record-user-card", key: "rate-combo" }, /* @__PURE__ */ React.createElement("span", { className: "record-user-card-label" }, "\u0421\u0442\u0430\u0432\u043A\u0430 / % \u0437\u0430\u0440\u043F\u043B\u0430\u0442\u044B"), /* @__PURE__ */ React.createElement("div", { className: "record-user-rate-grid" }, fieldMap.get("default_rate") ? renderField(fieldMap.get("default_rate")) : null, fieldMap.get("salary_percent") ? renderField(fieldMap.get("salary_percent")) : null));
|
|
}
|
|
return /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-item", key: "rate-combo-view" }, /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-label" }, "\u0421\u0442\u0430\u0432\u043A\u0430 / % \u0437\u0430\u0440\u043F\u043B\u0430\u0442\u044B"), /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-value" }, defaultRate || "\u2014", " / ", salaryPercent || "\u2014"));
|
|
};
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "record-overlay", onClose: (event) => event.target.id === "record-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal" + (isUserModal ? " record-user-modal" : ""), style: { width: isUserModal ? "min(920px, 100%)" : "min(760px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, title), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, isUserModal ? isCreateMode ? "\u0421\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0444\u0438\u043B\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F." : userEditing ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u0440\u043E\u0444\u0438\u043B\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F." : "\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043F\u0440\u043E\u0444\u0438\u043B\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F." : "\u0421\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0437\u0430\u043F\u0438\u0441\u0438.")), /* @__PURE__ */ React.createElement("div", { className: "modal-head-actions" }, isUserModal && !isCreateMode ? userEditing ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("button", { className: "icon-btn", type: "submit", form: "record-modal-form", "data-tooltip": "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C", "aria-label": "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M5 4h11.59a2 2 0 0 1 1.41.59l1.41 1.41A2 2 0 0 1 20 7.41V19a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1Zm1 2v13h12V8.24L15.76 6H15v4a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V6H6Zm4 0v3h3V6h-3Z", fill: "currentColor" }))), /* @__PURE__ */ React.createElement("button", { className: "icon-btn", type: "button", onClick: onClose, "data-tooltip": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C", "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M6.7 6.7a1 1 0 0 1 1.4 0L12 10.58l3.9-3.88a1 1 0 1 1 1.4 1.42L13.42 12l3.88 3.9a1 1 0 1 1-1.42 1.4L12 13.42l-3.9 3.88a1 1 0 0 1-1.4-1.42L10.58 12 6.7 8.1a1 1 0 0 1 0-1.4Z", fill: "currentColor" })))) : /* @__PURE__ */ React.createElement("button", { className: "icon-btn", type: "button", onClick: () => setUserEditing(true), "data-tooltip": "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C", "aria-label": "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M15.86 3.49a2 2 0 0 1 2.83 0l1.82 1.82a2 2 0 0 1 0 2.83l-9.9 9.9a1 1 0 0 1-.45.26l-4 1a1 1 0 0 1-1.21-1.21l1-4a1 1 0 0 1 .26-.45l9.9-9.9Zm1.41 1.42-9.67 9.67-.54 2.16 2.16-.54 9.67-9.67-1.62-1.62Z", fill: "currentColor" }))) : null, /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7"))), /* @__PURE__ */ React.createElement("form", { className: "stack" + (isUserModal ? " record-user-scroll" : ""), id: "record-modal-form", onSubmit }, isUserModal ? /* @__PURE__ */ React.createElement("div", { className: "record-user-top" + (cropFile ? " record-user-top--crop-mode" : "") }, /* @__PURE__ */ React.createElement("div", { className: "record-user-avatar-area" }, cropFile ? (
|
|
// Crop editor takes over the avatar area while selecting focus
|
|
/* @__PURE__ */ React.createElement(
|
|
AvatarCropEditor,
|
|
{
|
|
imageFile: cropFile,
|
|
initialCrop: cropInitial,
|
|
onApply: ({ file, cropJson }) => {
|
|
const wasRecrop = isRecropRef.current;
|
|
isRecropRef.current = false;
|
|
setCropFile(null);
|
|
setCropInitial(null);
|
|
if (wasRecrop && onApplyRecrop) {
|
|
onApplyRecrop(cropJson);
|
|
} else if (onUploadFieldWithCrop) {
|
|
onUploadFieldWithCrop(avatarField, file, cropJson);
|
|
} else if (onUploadField) {
|
|
onUploadField(avatarField, file);
|
|
}
|
|
},
|
|
onCancel: () => {
|
|
isRecropRef.current = false;
|
|
setCropFile(null);
|
|
setCropInitial(null);
|
|
}
|
|
}
|
|
)
|
|
) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "record-user-avatar-shell" + (avatarPreviewSrc ? " interactive" : ""),
|
|
onClick: () => {
|
|
if (avatarPreviewSrc) setAvatarPreviewOpen(true);
|
|
},
|
|
disabled: !avatarPreviewSrc,
|
|
"aria-label": avatarPreviewSrc ? "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0430\u0432\u0430\u0442\u0430\u0440 \u043A\u0440\u0443\u043F\u043D\u043E" : "\u0410\u0432\u0430\u0442\u0430\u0440 \u043D\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D"
|
|
},
|
|
/* @__PURE__ */ React.createElement(UserAvatar, { name: userName, email: userEmail, avatarUrl: avatarValue, accessToken, size: 148 })
|
|
), avatarField && (isCreateMode || userEditing) ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
ref: avatarUploadRef,
|
|
type: "file",
|
|
accept: avatarField.accept || "image/*",
|
|
style: { display: "none" },
|
|
onChange: (event) => {
|
|
const file = event.target.files && event.target.files[0];
|
|
if (file) {
|
|
setCropInitial(null);
|
|
setCropFile(file);
|
|
}
|
|
event.target.value = "";
|
|
}
|
|
}
|
|
), /* @__PURE__ */ React.createElement("div", { className: "record-user-avatar-toolbar" }, /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M12 5a1 1 0 0 1 1 1v6.17l2.59-2.58a1 1 0 1 1 1.41 1.42l-4.29 4.29a1 1 0 0 1-1.42 0L7 11.01a1 1 0 1 1 1.41-1.42L11 12.17V6a1 1 0 0 1 1-1Zm-7 12a1 1 0 0 1 1 1v1h12v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1Z", fill: "currentColor" })),
|
|
tooltip: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0430\u0432\u0430\u0442\u0430\u0440",
|
|
onClick: () => avatarUploadRef.current?.click()
|
|
}
|
|
), avatarValue && form?.avatar_original_key && onRecropAvatar ? /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M7 3a1 1 0 0 1 1 1v1h8V4a1 1 0 1 1 2 0v1h1a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h1V4a1 1 0 0 1 1-1Zm-2 6v10h14V9H5Zm4 2h2v2H9v-2Zm4 0h2v2h-2v-2Zm-4 4h2v2H9v-2Zm4 0h2v2h-2v-2Z", fill: "currentColor" })),
|
|
tooltip: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043A\u0430\u0434\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435",
|
|
onClick: () => {
|
|
isRecropRef.current = true;
|
|
let saved = null;
|
|
try {
|
|
saved = form?.avatar_crop_json ? JSON.parse(form.avatar_crop_json) : null;
|
|
} catch (_) {
|
|
}
|
|
setCropInitial(saved);
|
|
onRecropAvatar(avatarField, form, setCropFile);
|
|
}
|
|
}
|
|
) : null, /* @__PURE__ */ React.createElement(
|
|
IconButton,
|
|
{
|
|
icon: /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M6.7 6.7a1 1 0 0 1 1.4 0L12 10.58l3.9-3.88a1 1 0 1 1 1.4 1.42L13.42 12l3.88 3.9a1 1 0 1 1-1.42 1.4L12 13.42l-3.9 3.88a1 1 0 0 1-1.4-1.42L10.58 12 6.7 8.1a1 1 0 0 1 0-1.4Z", fill: "currentColor" })),
|
|
tooltip: "\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0430\u0432\u0430\u0442\u0430\u0440",
|
|
onClick: () => {
|
|
onChange(avatarField.key, "");
|
|
setAvatarPreviewOpen(false);
|
|
},
|
|
disabled: !avatarValue
|
|
}
|
|
))) : null)), /* @__PURE__ */ React.createElement("div", { className: "record-user-summary" }, /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-head" }, isCreateMode || userEditing ? renderUserCard("name") : /* @__PURE__ */ React.createElement("h4", null, userName || "\u041D\u043E\u0432\u044B\u0439 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C"), isCreateMode || userEditing ? /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-edit-meta" }, renderUserCard("role"), renderUserCard("is_active")) : /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-badges" }, /* @__PURE__ */ React.createElement("span", { className: "record-user-badge" }, userRole || "\u0420\u043E\u043B\u044C \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("span", { className: "record-user-badge status-" + statusTone }, activeLabel))), /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-grid" }, isCreateMode || userEditing ? /* @__PURE__ */ React.createElement(React.Fragment, null, renderUserCard("email"), renderUserCard("phone"), renderUserCard("primary_topic_code"), renderUserRateCard(), renderUserCard("password")) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-item" }, /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-label" }, "Email"), /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-value" }, userEmail || "\u041D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D")), /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-item" }, /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-label" }, "\u0422\u0435\u043B\u0435\u0444\u043E\u043D"), /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-value" }, userPhone || "\u041D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D")), /* @__PURE__ */ React.createElement("div", { className: "record-user-summary-item" }, /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-label" }, "\u041F\u0440\u043E\u0444\u0438\u043B\u044C"), /* @__PURE__ */ React.createElement("span", { className: "record-user-summary-value" }, userTopic)), renderUserRateCard())))) : null, !isUserModal ? /* @__PURE__ */ React.createElement("div", { className: "filters", style: { gridTemplateColumns: "repeat(2, minmax(0,1fr))" } }, formFields.map((field) => /* @__PURE__ */ React.createElement("div", { className: "field", key: field.key, style: field.fullRow ? { gridColumn: "1 / -1" } : void 0 }, /* @__PURE__ */ React.createElement("label", { htmlFor: "record-field-" + field.key }, field.label), renderField(field)))) : null, isUserModal && isCreateMode ? /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")) : null, !isUserModal ? /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")) : null, /* @__PURE__ */ React.createElement(StatusLine, { status }))), isUserModal ? /* @__PURE__ */ React.createElement(Overlay, { open: avatarPreviewOpen, id: "record-avatar-preview-overlay", onClose: () => setAvatarPreviewOpen(false) }, /* @__PURE__ */ React.createElement("div", { className: "modal record-avatar-preview-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, userName || "\u0410\u0432\u0430\u0442\u0430\u0440 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u041F\u0440\u043E\u0441\u0442\u043E\u043C\u043E\u0442\u0440 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: () => setAvatarPreviewOpen(false), "aria-label": "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "record-avatar-preview-body" }, avatarPreviewSrc ? /* @__PURE__ */ React.createElement("img", { className: "record-avatar-preview-image", src: avatarPreviewSrc, alt: userName || userEmail || "avatar" }) : /* @__PURE__ */ React.createElement("div", { className: "record-avatar-preview-empty" }, /* @__PURE__ */ React.createElement(UserAvatar, { name: userName, email: userEmail, avatarUrl: "", accessToken, size: 128 }), /* @__PURE__ */ React.createElement("span", null, "\u0410\u0432\u0430\u0442\u0430\u0440 \u0435\u0449\u0435 \u043D\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D"))))) : null);
|
|
}
|
|
|
|
// app/web/admin.jsx
|
|
var import_qrcode = __toESM(require_browser());
|
|
(function() {
|
|
const { useCallback: useCallback2, useEffect: useEffect4, useMemo: useMemo2, useRef: useRef4, useState: useState4 } = React;
|
|
const LEGACY_HIDDEN_DICTIONARY_TABLES = /* @__PURE__ */ new Set(["formFields", "topicRequiredFields", "statusTransitions"]);
|
|
const NEW_REQUEST_CLIENT_OPTION = "__new_client__";
|
|
function StatusLine({ status }) {
|
|
return /* @__PURE__ */ React.createElement("p", { className: "status" + (status?.kind ? " " + status.kind : "") }, status?.message || "");
|
|
}
|
|
function Section({ active, children, id }) {
|
|
return /* @__PURE__ */ React.createElement("section", { className: "section" + (active ? " active" : ""), id }, children);
|
|
}
|
|
function DataTable({ headers, rows, emptyColspan, renderRow, onSort, sortClause }) {
|
|
return /* @__PURE__ */ React.createElement("div", { className: "table-wrap table-scroll-region" }, /* @__PURE__ */ React.createElement("table", null, /* @__PURE__ */ React.createElement("thead", null, /* @__PURE__ */ React.createElement("tr", null, headers.map((header) => {
|
|
const h = typeof header === "string" ? { key: header, label: header } : header;
|
|
const sortable = Boolean(h.sortable && h.field && onSort);
|
|
const active = Boolean(sortable && sortClause && sortClause.field === h.field);
|
|
const direction = active ? sortClause.dir : "";
|
|
return /* @__PURE__ */ React.createElement(
|
|
"th",
|
|
{
|
|
key: h.key || h.label,
|
|
className: sortable ? "sortable-th" : "",
|
|
onClick: sortable ? () => onSort(h.field) : void 0,
|
|
title: sortable ? "\u041D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438" : void 0
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: sortable ? "sortable-head" : "" }, h.label, sortable ? /* @__PURE__ */ React.createElement("span", { className: "sort-indicator" + (active ? " active" : "") }, direction === "desc" ? "\u2193" : "\u2191") : null)
|
|
);
|
|
}))), /* @__PURE__ */ React.createElement("tbody", null, rows.length ? rows.map((row, index) => renderRow(row, index)) : /* @__PURE__ */ React.createElement("tr", null, /* @__PURE__ */ React.createElement("td", { colSpan: emptyColspan }, "\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445")))));
|
|
}
|
|
function TablePager({ tableState, onPrev, onNext, onLoadAll, onRefresh, onCreate, onOpenFilter }) {
|
|
return /* @__PURE__ */ React.createElement("div", { className: "pager table-footer-bar" }, /* @__PURE__ */ React.createElement("div", null, tableState.showAll ? "\u0412\u0441\u0435\u0433\u043E: " + tableState.total + " \u2022 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0432\u0441\u0435 \u0437\u0430\u043F\u0438\u0441\u0438" : "\u0412\u0441\u0435\u0433\u043E: " + tableState.total + " \u2022 \u0441\u043C\u0435\u0449\u0435\u043D\u0438\u0435: " + tableState.offset), /* @__PURE__ */ React.createElement("div", { className: "table-footer-actions" }, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn table-control-loadall",
|
|
type: "button",
|
|
onClick: onLoadAll,
|
|
disabled: tableState.total === 0 || tableState.showAll || tableState.rows.length >= tableState.total,
|
|
title: "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + tableState.total,
|
|
"aria-label": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0432\u0441\u0435 " + tableState.total
|
|
},
|
|
/* @__PURE__ */ React.createElement(DownloadIcon, null),
|
|
/* @__PURE__ */ React.createElement("span", null, tableState.total)
|
|
), onRefresh ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onRefresh, title: "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C", "aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(RefreshIcon, null)) : null, onCreate ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onCreate, title: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C", "aria-label": "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C" }, /* @__PURE__ */ React.createElement(AddIcon, null)) : null, onOpenFilter ? /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpenFilter, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null)) : null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onPrev, disabled: tableState.showAll || tableState.offset <= 0, title: "\u041D\u0430\u0437\u0430\u0434", "aria-label": "\u041D\u0430\u0437\u0430\u0434" }, /* @__PURE__ */ React.createElement(PrevIcon, null)), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "btn secondary table-control-btn",
|
|
type: "button",
|
|
onClick: onNext,
|
|
disabled: tableState.showAll || tableState.offset + PAGE_SIZE >= tableState.total,
|
|
title: "\u0412\u043F\u0435\u0440\u0435\u0434",
|
|
"aria-label": "\u0412\u043F\u0435\u0440\u0435\u0434"
|
|
},
|
|
/* @__PURE__ */ React.createElement(NextIcon, null)
|
|
)));
|
|
}
|
|
function SidebarNavIcon({ name }) {
|
|
const common = { width: 18, height: 18, "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24" };
|
|
if (name === "dashboard") {
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("path", { d: "M4 5.5A1.5 1.5 0 0 1 5.5 4h4A1.5 1.5 0 0 1 11 5.5v4A1.5 1.5 0 0 1 9.5 11h-4A1.5 1.5 0 0 1 4 9.5v-4Zm9 0A1.5 1.5 0 0 1 14.5 4h4A1.5 1.5 0 0 1 20 5.5v7A1.5 1.5 0 0 1 18.5 14h-4a1.5 1.5 0 0 1-1.5-1.5v-7Zm-9 9A1.5 1.5 0 0 1 5.5 13h4a1.5 1.5 0 0 1 1.5 1.5v4A1.5 1.5 0 0 1 9.5 20h-4A1.5 1.5 0 0 1 4 18.5v-4Zm9 3a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2h-6a1 1 0 0 1-1-1Z", fill: "currentColor" }));
|
|
}
|
|
if (name === "kanban") {
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("path", { d: "M5 4h4a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Zm10 0h4a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1Zm0 12h4a1 1 0 1 1 0 2h-4a1 1 0 1 1 0-2Z", fill: "currentColor" }));
|
|
}
|
|
if (name === "requests") {
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("path", { d: "M6 4h9.2a2 2 0 0 1 1.41.59l2.8 2.8A2 2 0 0 1 20 8.8V18a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Zm1 4h10V6.8L15.2 5H7v3Zm0 4h10v-2H7v2Zm0 4h7v-2H7v2Z", fill: "currentColor" }));
|
|
}
|
|
if (name === "serviceRequests") {
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("path", { d: "M5 4.5h14A1.5 1.5 0 0 1 20.5 6v9.5A1.5 1.5 0 0 1 19 17H9.2l-3.55 2.96A1.2 1.2 0 0 1 3.7 19V6A1.5 1.5 0 0 1 5 4.5Zm2.1 4.4a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2Zm4.9 0a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2Zm4.9 0a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2Z", fill: "currentColor" }));
|
|
}
|
|
if (name === "invoices") {
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("path", { d: "M6 4h12a2 2 0 0 1 2 2v12.5a1.5 1.5 0 0 1-2.56 1.06L15 17.12l-2.44 2.44a1.5 1.5 0 0 1-2.12 0L8 17.12l-2.44 2.44A1.5 1.5 0 0 1 3 18.5V6a2 2 0 0 1 2-2Zm2.5 4.5a1 1 0 0 0 0 2h5a1 1 0 1 0 0-2h-5Zm0 4a1 1 0 1 0 0 2h7a1 1 0 1 0 0-2h-7Z", fill: "currentColor" }));
|
|
}
|
|
if (name === "config") {
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("path", { d: "M12 3.5a2 2 0 0 1 1.86 1.27l.27.68a6.9 6.9 0 0 1 1.31.54l.67-.3a2 2 0 0 1 2.43.75l.7.98a2 2 0 0 1-.18 2.5l-.48.55c.05.35.08.72.08 1.08 0 .37-.03.73-.08 1.08l.48.55a2 2 0 0 1 .18 2.5l-.7.98a2 2 0 0 1-2.43.75l-.67-.3c-.42.23-.86.4-1.31.54l-.27.68A2 2 0 0 1 12 20.5h-1.2a2 2 0 0 1-1.86-1.27l-.27-.68a6.9 6.9 0 0 1-1.31-.54l-.67.3a2 2 0 0 1-2.43-.75l-.7-.98a2 2 0 0 1 .18-2.5l.48-.55A7.7 7.7 0 0 1 4 12c0-.36.03-.73.08-1.08l-.48-.55a2 2 0 0 1-.18-2.5l.7-.98a2 2 0 0 1 2.43-.75l.67.3c.42-.23.86-.4 1.31-.54l.27-.68A2 2 0 0 1 10.8 3.5H12Zm-.6 5.2a3.3 3.3 0 1 0 0 6.6 3.3 3.3 0 0 0 0-6.6Z", fill: "currentColor" }));
|
|
}
|
|
return /* @__PURE__ */ React.createElement("svg", { ...common }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "8", fill: "currentColor" }));
|
|
}
|
|
function FilterToolbar({ filters, onOpen, onRemove, onEdit, getChipLabel, hideAction = false }) {
|
|
return /* @__PURE__ */ React.createElement("div", { className: "filter-toolbar" }, /* @__PURE__ */ React.createElement("div", { className: "filter-chips" }, filters.length ? filters.map((filter, index) => /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "filter-chip",
|
|
key: filter.field + filter.op + index,
|
|
onClick: () => onEdit(index),
|
|
role: "button",
|
|
tabIndex: 0,
|
|
onKeyDown: (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
onEdit(index);
|
|
}
|
|
},
|
|
title: "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440"
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", null, getChipLabel(filter)),
|
|
/* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
"aria-label": "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440",
|
|
onClick: (event) => {
|
|
event.stopPropagation();
|
|
onRemove(index);
|
|
}
|
|
},
|
|
"\xD7"
|
|
)
|
|
)) : /* @__PURE__ */ React.createElement("span", { className: "chip-placeholder" }, "\u0424\u0438\u043B\u044C\u0442\u0440\u044B \u043D\u0435 \u0437\u0430\u0434\u0430\u043D\u044B")), !hideAction ? /* @__PURE__ */ React.createElement("div", { className: "filter-action" }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary table-control-btn", type: "button", onClick: onOpen, title: "\u0424\u0438\u043B\u044C\u0442\u0440", "aria-label": "\u0424\u0438\u043B\u044C\u0442\u0440" }, /* @__PURE__ */ React.createElement(FilterIcon, null))) : null);
|
|
}
|
|
function Overlay({ open, onClose, children, id }) {
|
|
return /* @__PURE__ */ React.createElement("div", { className: "overlay" + (open ? " open" : ""), id, onClick: onClose }, children);
|
|
}
|
|
function IconButton({ icon, tooltip, onClick, tone, disabled = false }) {
|
|
const handleClick = (event) => {
|
|
if (disabled) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (event.nativeEvent && typeof event.nativeEvent.stopImmediatePropagation === "function") {
|
|
event.nativeEvent.stopImmediatePropagation();
|
|
}
|
|
if (typeof onClick === "function") onClick(event);
|
|
};
|
|
const handleAuxClick = (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (event.nativeEvent && typeof event.nativeEvent.stopImmediatePropagation === "function") {
|
|
event.nativeEvent.stopImmediatePropagation();
|
|
}
|
|
};
|
|
return /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "icon-btn" + (tone ? " " + tone : ""),
|
|
type: "button",
|
|
"data-tooltip": tooltip,
|
|
onClick: handleClick,
|
|
onAuxClick: handleAuxClick,
|
|
"aria-label": tooltip,
|
|
disabled
|
|
},
|
|
icon
|
|
);
|
|
}
|
|
function UserAvatar({ name, email, avatarUrl, accessToken, size = 32 }) {
|
|
const [broken, setBroken] = useState4(false);
|
|
useEffect4(() => setBroken(false), [avatarUrl]);
|
|
const initials = userInitials(name, email);
|
|
const bg = avatarColor(name || email || initials);
|
|
const src = resolveAvatarSrc(avatarUrl, accessToken, size);
|
|
const canShowImage = Boolean(src && !broken);
|
|
return /* @__PURE__ */ React.createElement("span", { className: "avatar", style: { width: size + "px", height: size + "px", backgroundColor: bg } }, canShowImage ? /* @__PURE__ */ React.createElement(
|
|
"img",
|
|
{
|
|
src,
|
|
alt: name || email || "avatar",
|
|
loading: "lazy",
|
|
decoding: "async",
|
|
fetchPriority: size >= 64 ? "low" : "auto",
|
|
onError: () => setBroken(true)
|
|
}
|
|
) : /* @__PURE__ */ React.createElement("span", null, initials));
|
|
}
|
|
function LoginScreen({ onSubmit, status }) {
|
|
const [email, setEmail] = useState4("");
|
|
const [password, setPassword] = useState4("");
|
|
const [totpCode, setTotpCode] = useState4("");
|
|
const submit = (event) => {
|
|
event.preventDefault();
|
|
onSubmit(email, password, totpCode);
|
|
};
|
|
return /* @__PURE__ */ React.createElement("div", { className: "login-screen" }, /* @__PURE__ */ React.createElement("div", { className: "login-card" }, /* @__PURE__ */ React.createElement("h2", null, "\u0412\u0445\u043E\u0434 \u0432 \u0430\u0434\u043C\u0438\u043D-\u043F\u0430\u043D\u0435\u043B\u044C"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0435\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430 \u0438\u043B\u0438 \u044E\u0440\u0438\u0441\u0442\u0430."), /* @__PURE__ */ React.createElement("form", { className: "stack", style: { marginTop: "0.7rem" }, onSubmit: submit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "login-email" }, "\u042D\u043B. \u043F\u043E\u0447\u0442\u0430"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "login-email",
|
|
type: "email",
|
|
required: true,
|
|
placeholder: "admin@example.com",
|
|
value: email,
|
|
onChange: (event) => setEmail(event.target.value)
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "login-password" }, "\u041F\u0430\u0440\u043E\u043B\u044C"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "login-password",
|
|
type: "password",
|
|
required: true,
|
|
placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",
|
|
value: password,
|
|
onChange: (event) => setPassword(event.target.value)
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "login-totp" }, "TOTP / \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0439 \u043A\u043E\u0434"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "login-totp",
|
|
type: "text",
|
|
placeholder: "123456 \u0438\u043B\u0438 backup-code",
|
|
value: totpCode,
|
|
onChange: (event) => setTotpCode(event.target.value)
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u0412\u043E\u0439\u0442\u0438"), /* @__PURE__ */ React.createElement(StatusLine, { status }))));
|
|
}
|
|
function FilterModal({
|
|
open,
|
|
tableLabel,
|
|
fields,
|
|
draft,
|
|
status,
|
|
onClose,
|
|
onFieldChange,
|
|
onOpChange,
|
|
onValueChange,
|
|
onSubmit,
|
|
onClear,
|
|
getOperators,
|
|
getFieldOptions
|
|
}) {
|
|
if (!open) return null;
|
|
const selectedField = fields.find((field) => field.field === draft.field) || fields[0] || null;
|
|
const operators = getOperators(selectedField?.type || "text");
|
|
const options = selectedField ? getFieldOptions(selectedField) : [];
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "filter-overlay", onClose: (event) => event.target.id === "filter-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(560px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0424\u0438\u043B\u044C\u0442\u0440 \u0442\u0430\u0431\u043B\u0438\u0446\u044B"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, tableLabel ? (draft.editIndex !== null ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u2022 " : "\u041D\u043E\u0432\u044B\u0439 \u0444\u0438\u043B\u044C\u0442\u0440 \u2022 ") + "\u0422\u0430\u0431\u043B\u0438\u0446\u0430: " + tableLabel : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u043E\u043B\u0435, \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440 \u0438 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "filter-field" }, "\u041F\u043E\u043B\u0435"), /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "filter-field",
|
|
value: draft.field,
|
|
onChange: (nextValue) => onFieldChange({ target: { value: nextValue } }),
|
|
options: fields.map((field) => ({ value: field.field, label: field.label })),
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u043E\u043B\u0435"
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "filter-op" }, "\u041E\u043F\u0435\u0440\u0430\u0442\u043E\u0440"), /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "filter-op",
|
|
value: draft.op,
|
|
onChange: (nextValue) => onOpChange({ target: { value: nextValue } }),
|
|
options: operators.map((op) => ({ value: op, label: OPERATOR_LABELS[op] })),
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440"
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "filter-value" }, selectedField ? "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435: " + selectedField.label : "\u0417\u043D\u0430\u0447\u0435\u043D\u0438\u0435"), !selectedField || selectedField.type === "text" ? /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "text", value: draft.rawValue, onChange: onValueChange, placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435" }) : selectedField.type === "number" ? /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "number", step: "any", value: draft.rawValue, onChange: onValueChange, placeholder: "\u0427\u0438\u0441\u043B\u043E" }) : selectedField.type === "date" ? /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "date", value: draft.rawValue, onChange: onValueChange }) : selectedField.type === "boolean" ? /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "filter-value",
|
|
value: draft.rawValue,
|
|
onChange: (nextValue) => onValueChange({ target: { value: nextValue } }),
|
|
options: [
|
|
{ value: "true", label: "True" },
|
|
{ value: "false", label: "False" }
|
|
],
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"
|
|
}
|
|
) : selectedField.type === "reference" || selectedField.type === "enum" ? /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "filter-value",
|
|
value: draft.rawValue,
|
|
onChange: (nextValue) => onValueChange({ target: { value: nextValue } }),
|
|
options: options.map((option) => ({ value: String(option.value), label: option.label })),
|
|
disabled: !options.length,
|
|
placeholder: !options.length ? "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439" : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435"
|
|
}
|
|
) : /* @__PURE__ */ React.createElement("input", { id: "filter-value", type: "text", value: draft.rawValue, onChange: onValueChange, placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435" })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, draft.editIndex !== null ? "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C" : "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClear }, "\u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u0441\u0435"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status }))));
|
|
}
|
|
function ReassignModal({ open, status, options, value, onChange, onClose, onSubmit, trackNumber }) {
|
|
if (!open) return null;
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "reassign-overlay", onClose: (event) => event.target.id === "reassign-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(520px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u041F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, trackNumber ? "\u0417\u0430\u044F\u0432\u043A\u0430: " + trackNumber : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043D\u043E\u0432\u043E\u0433\u043E \u044E\u0440\u0438\u0441\u0442\u0430")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "reassign-lawyer" }, "\u041D\u043E\u0432\u044B\u0439 \u044E\u0440\u0438\u0441\u0442"), /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "reassign-lawyer",
|
|
value,
|
|
onChange: (nextValue) => onChange({ target: { value: nextValue } }),
|
|
options: options.map((option) => ({ value: String(option.value), label: option.label })),
|
|
disabled: !options.length,
|
|
placeholder: !options.length ? "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u044E\u0440\u0438\u0441\u0442\u043E\u0432" : "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u044E\u0440\u0438\u0441\u0442\u0430"
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit", disabled: !value }, "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status }))));
|
|
}
|
|
function KanbanSortModal({ open, value, status, onChange, onClose, onSubmit }) {
|
|
if (!open) return null;
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "kanban-sort-overlay", onClose: (event) => event.target.id === "kanban-sort-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(520px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u043A\u0430\u043D\u0431\u0430\u043D\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043F\u043E\u0441\u043E\u0431 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438 \u043A\u0430\u0440\u0442\u043E\u0447\u0435\u043A.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "kanban-sort-mode" }, "\u0422\u0438\u043F \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438"), /* @__PURE__ */ React.createElement(
|
|
DropdownField,
|
|
{
|
|
id: "kanban-sort-mode",
|
|
value,
|
|
onChange: (nextValue) => onChange({ target: { value: nextValue } }),
|
|
options: [
|
|
{ value: "created_newest", label: "\u0414\u0430\u0442\u0430 \u0437\u0430\u044F\u0432\u043A\u0438 (\u043D\u043E\u0432\u044B\u0435 \u0441\u0432\u0435\u0440\u0445\u0443)" },
|
|
{ value: "lawyer", label: "\u042E\u0440\u0438\u0441\u0442" },
|
|
{ value: "deadline", label: "\u0414\u0435\u0434\u043B\u0430\u0439\u043D" }
|
|
],
|
|
placeholder: "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0443"
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit" }, "\u041E\u043A"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status }))));
|
|
}
|
|
function TotpSetupModal({
|
|
open,
|
|
status,
|
|
secret,
|
|
uri,
|
|
qrDataUrl,
|
|
code,
|
|
loading,
|
|
onCodeChange,
|
|
onClose,
|
|
onSubmit,
|
|
onCopySecret,
|
|
onCopyUri
|
|
}) {
|
|
if (!open) return null;
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "totp-setup-overlay", onClose: (event) => event.target.id === "totp-setup-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal", style: { width: "min(700px, 100%)" }, onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 2FA"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u0421\u043A\u0430\u043D\u0438\u0440\u0443\u0439\u0442\u0435 QR-\u043A\u043E\u0434 \u0432 Google Authenticator \u0438 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 6-\u0437\u043D\u0430\u0447\u043D\u044B\u043C \u043A\u043E\u0434\u043E\u043C.")), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7")), /* @__PURE__ */ React.createElement("div", { className: "totp-setup-grid" }, /* @__PURE__ */ React.createElement("div", { className: "totp-qr-box" }, qrDataUrl ? /* @__PURE__ */ React.createElement("img", { className: "totp-qr-img", src: qrDataUrl, alt: "QR-\u043A\u043E\u0434 \u0434\u043B\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 2FA" }) : /* @__PURE__ */ React.createElement("p", { className: "muted" }, "QR-\u043A\u043E\u0434 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u043A\u043B\u044E\u0447 \u0432\u0440\u0443\u0447\u043D\u0443\u044E.")), /* @__PURE__ */ React.createElement("div", { className: "stack" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "totp-secret" }, "\u0421\u0435\u043A\u0440\u0435\u0442\u043D\u044B\u0439 \u043A\u043B\u044E\u0447"), /* @__PURE__ */ React.createElement("input", { id: "totp-secret", type: "text", value: secret, readOnly: true })), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "totp-uri" }, "URI (otpauth)"), /* @__PURE__ */ React.createElement("textarea", { id: "totp-uri", rows: 3, value: uri, readOnly: true })), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.5rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onCopySecret }, "\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043B\u044E\u0447"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onCopyUri }, "\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C URI")))), /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "totp-verify-code" }, "\u041A\u043E\u0434 \u0438\u0437 Google Authenticator"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "totp-verify-code",
|
|
type: "text",
|
|
inputMode: "numeric",
|
|
autoComplete: "one-time-code",
|
|
placeholder: "123456",
|
|
value: code,
|
|
onChange: onCodeChange
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit", disabled: loading }, loading ? "\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u043C..." : "\u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C 2FA"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose, disabled: loading }, "\u041E\u0442\u043C\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement(StatusLine, { status }))));
|
|
}
|
|
function AccountModal({
|
|
open,
|
|
status,
|
|
profileLoading,
|
|
saveLoading,
|
|
form,
|
|
currentEmail,
|
|
currentRoleLabel,
|
|
totpStatus,
|
|
onFieldChange,
|
|
onClose,
|
|
onSubmit,
|
|
onSetupTotp,
|
|
onRegenerateBackupCodes,
|
|
onDisableTotp,
|
|
onLogout
|
|
}) {
|
|
if (!open) return null;
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "account-overlay", onClose: (event) => event.target.id === "account-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal account-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h3", null, "\u041B\u0438\u0447\u043D\u044B\u0439 \u043A\u0430\u0431\u0438\u043D\u0435\u0442"), /* @__PURE__ */ React.createElement("p", { className: "muted", style: { marginTop: "0.35rem" } }, "\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u0438 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0441\u0442\u044C \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430.")), /* @__PURE__ */ React.createElement("div", { className: "modal-head-actions" }, /* @__PURE__ */ React.createElement("button", { className: "icon-btn", type: "button", "data-tooltip": "\u0412\u044B\u0439\u0442\u0438 \u0438\u0437 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430", "aria-label": "\u0412\u044B\u0439\u0442\u0438 \u0438\u0437 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430", onClick: onLogout }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M15.4 5.4a1 1 0 0 1 1.4 0l5.2 5.2a1 1 0 0 1 0 1.4l-5.2 5.2a1 1 0 1 1-1.4-1.4l3.5-3.4H9a1 1 0 1 1 0-2h9.9l-3.5-3.4a1 1 0 0 1 0-1.4zM3 4a1 1 0 0 1 1-1h7a1 1 0 1 1 0 2H5v14h6a1 1 0 1 1 0 2H4a1 1 0 0 1-1-1V4z",
|
|
fill: "currentColor"
|
|
}
|
|
))), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7"))), profileLoading ? /* @__PURE__ */ React.createElement("p", { className: "muted" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u0440\u043E\u0444\u0438\u043B\u044F...") : /* @__PURE__ */ React.createElement("form", { className: "stack", onSubmit }, /* @__PURE__ */ React.createElement("div", { className: "account-security-box" }, "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C: ", /* @__PURE__ */ React.createElement("b", null, currentEmail || "-"), /* @__PURE__ */ React.createElement("br", null), "\u0420\u043E\u043B\u044C: ", /* @__PURE__ */ React.createElement("b", null, currentRoleLabel || "-"), /* @__PURE__ */ React.createElement("br", null), "2FA: ", /* @__PURE__ */ React.createElement("b", null, totpStatus.enabled ? "\u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0430" : "\u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0430")), /* @__PURE__ */ React.createElement("div", { className: "account-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-name" }, "\u0418\u043C\u044F"), /* @__PURE__ */ React.createElement("input", { id: "account-name", name: "name", type: "text", value: form.name, onChange: onFieldChange })), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-email" }, "\u041F\u043E\u0447\u0442\u0430"), /* @__PURE__ */ React.createElement("input", { id: "account-email", name: "email", type: "email", value: form.email, onChange: onFieldChange }))), /* @__PURE__ */ React.createElement("div", { className: "account-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-phone" }, "\u0422\u0435\u043B\u0435\u0444\u043E\u043D"), /* @__PURE__ */ React.createElement("input", { id: "account-phone", name: "phone", type: "text", value: form.phone, onChange: onFieldChange })), /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-password" }, "\u041D\u043E\u0432\u044B\u0439 \u043F\u0430\u0440\u043E\u043B\u044C"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "account-password",
|
|
name: "password",
|
|
type: "password",
|
|
autoComplete: "new-password",
|
|
value: form.password,
|
|
onChange: onFieldChange,
|
|
placeholder: "\u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043C\u0435\u043D\u044F\u0435\u0442\u0435"
|
|
}
|
|
))), /* @__PURE__ */ React.createElement("div", { className: "account-modal-grid" }, /* @__PURE__ */ React.createElement("div", { className: "field" }, /* @__PURE__ */ React.createElement("label", { htmlFor: "account-password-confirm" }, "\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043F\u0430\u0440\u043E\u043B\u044F"), /* @__PURE__ */ React.createElement(
|
|
"input",
|
|
{
|
|
id: "account-password-confirm",
|
|
name: "passwordConfirm",
|
|
type: "password",
|
|
autoComplete: "new-password",
|
|
value: form.passwordConfirm,
|
|
onChange: onFieldChange
|
|
}
|
|
)), /* @__PURE__ */ React.createElement("div", { className: "field" })), /* @__PURE__ */ React.createElement("div", { className: "account-security-box" }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", gap: "0.5rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("b", null, "2FA"), ": ", totpStatus.enabled ? "\u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0430" : "\u0412\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u0430"), /* @__PURE__ */ React.createElement("div", { className: "muted" }, "\u0420\u0435\u0436\u0438\u043C: ", String(totpStatus.mode || "-"))), /* @__PURE__ */ React.createElement("div", { style: { marginTop: "0.6rem", display: "flex", gap: "0.45rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onSetupTotp }, "\u041D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C 2FA"), totpStatus.enabled ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onRegenerateBackupCodes }, "Backup-\u043A\u043E\u0434\u044B"), /* @__PURE__ */ React.createElement("button", { className: "btn danger", type: "button", onClick: onDisableTotp }, "\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C 2FA")) : null)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.6rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "btn", type: "submit", disabled: saveLoading }, saveLoading ? "\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u043C..." : "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F"), /* @__PURE__ */ React.createElement("button", { className: "btn secondary", type: "button", onClick: onClose, disabled: saveLoading }, "\u0417\u0430\u043A\u0440\u044B\u0442\u044C")), /* @__PURE__ */ React.createElement(StatusLine, { status }))));
|
|
}
|
|
function AttachmentPreviewModal({ open, title, url, fileName, mimeType, onClose }) {
|
|
const [resolvedUrl, setResolvedUrl] = useState4("");
|
|
const [resolvedText, setResolvedText] = useState4("");
|
|
const [resolvedKind, setResolvedKind] = useState4("");
|
|
const [hint, setHint] = useState4("");
|
|
const [loading, setLoading] = useState4(false);
|
|
const [error, setError] = useState4("");
|
|
const decodeTextPreview = (arrayBuffer) => {
|
|
const bytes = new Uint8Array(arrayBuffer || new ArrayBuffer(0));
|
|
const sampleLength = Math.min(bytes.length, 4096);
|
|
let suspicious = 0;
|
|
for (let i = 0; i < sampleLength; i += 1) {
|
|
const byte = bytes[i];
|
|
if (byte === 0) suspicious += 4;
|
|
else if (byte < 9 || byte > 13 && byte < 32) suspicious += 1;
|
|
}
|
|
if (sampleLength && suspicious / sampleLength > 0.08) return null;
|
|
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes).replace(/\u0000/g, "");
|
|
const normalized = text.length > 2e5 ? text.slice(0, 2e5) + "\n\n[\u0422\u0435\u043A\u0441\u0442 \u043E\u0431\u0440\u0435\u0437\u0430\u043D \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430]" : text;
|
|
return normalized;
|
|
};
|
|
useEffect4(() => {
|
|
if (!open || !url) {
|
|
setResolvedUrl("");
|
|
setResolvedText("");
|
|
setResolvedKind("");
|
|
setHint("");
|
|
setLoading(false);
|
|
setError("");
|
|
return;
|
|
}
|
|
const kind2 = detectAttachmentPreviewKind(fileName, mimeType);
|
|
setResolvedKind(kind2);
|
|
setResolvedText("");
|
|
setHint("");
|
|
if (kind2 === "none") {
|
|
setResolvedUrl("");
|
|
setLoading(false);
|
|
setError("");
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
let objectUrl = "";
|
|
setLoading(true);
|
|
setError("");
|
|
setResolvedUrl("");
|
|
(async () => {
|
|
try {
|
|
const response = await fetch(url, { credentials: "same-origin" });
|
|
if (!response.ok) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430");
|
|
const buffer = await response.arrayBuffer();
|
|
if (cancelled) return;
|
|
if (kind2 === "pdf") {
|
|
const header = new Uint8Array(buffer.slice(0, 5));
|
|
const isPdf = header.length >= 5 && header[0] === 37 && header[1] === 80 && header[2] === 68 && header[3] === 70 && header[4] === 45;
|
|
if (isPdf) {
|
|
setResolvedUrl(String(url));
|
|
setResolvedKind("pdf");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
const textPreview = decodeTextPreview(buffer);
|
|
if (textPreview != null) {
|
|
setResolvedUrl("");
|
|
setResolvedText(textPreview);
|
|
setResolvedKind("text");
|
|
setHint("\u0424\u0430\u0439\u043B \u043F\u043E\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A PDF, \u043D\u043E \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C PDF. \u041F\u043E\u043A\u0430\u0437\u0430\u043D \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440.");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
throw new Error("\u0424\u0430\u0439\u043B \u043F\u043E\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A PDF, \u043D\u043E \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C PDF-\u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u043C.");
|
|
}
|
|
if (kind2 === "text") {
|
|
const textPreview = decodeTextPreview(buffer);
|
|
if (textPreview == null) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u0442\u044C \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430.");
|
|
setResolvedUrl("");
|
|
setResolvedText(textPreview);
|
|
setResolvedKind("text");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
const blob = new Blob([buffer], { type: response.headers.get("content-type") || mimeType || "application/octet-stream" });
|
|
objectUrl = URL.createObjectURL(blob);
|
|
if (cancelled) {
|
|
URL.revokeObjectURL(objectUrl);
|
|
return;
|
|
}
|
|
setResolvedUrl(objectUrl);
|
|
setResolvedKind(kind2);
|
|
setLoading(false);
|
|
} catch (err) {
|
|
if (cancelled) return;
|
|
setError(err instanceof Error ? err.message : "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440");
|
|
setLoading(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
};
|
|
}, [fileName, mimeType, open, url]);
|
|
if (!open || !url) return null;
|
|
const kind = resolvedKind || detectAttachmentPreviewKind(fileName, mimeType);
|
|
return /* @__PURE__ */ React.createElement(Overlay, { open, id: "request-file-preview-overlay", onClose: (event) => event.target.id === "request-file-preview-overlay" && onClose() }, /* @__PURE__ */ React.createElement("div", { className: "modal request-preview-modal", onClick: (event) => event.stopPropagation() }, /* @__PURE__ */ React.createElement("div", { className: "modal-head" }, /* @__PURE__ */ React.createElement("h3", null, title || fileName || "\u041F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u0444\u0430\u0439\u043B\u0430"), /* @__PURE__ */ React.createElement("div", { className: "request-preview-head-actions" }, /* @__PURE__ */ React.createElement(
|
|
"a",
|
|
{
|
|
className: "icon-btn file-action-btn request-preview-download-icon",
|
|
href: url,
|
|
target: "_blank",
|
|
rel: "noreferrer",
|
|
"aria-label": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0444\u0430\u0439\u043B",
|
|
"data-tooltip": "\u0421\u043A\u0430\u0447\u0430\u0442\u044C"
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M12 3a1 1 0 0 1 1 1v8.17l2.58-2.58a1 1 0 1 1 1.42 1.42l-4.3 4.3a1 1 0 0 1-1.4 0l-4.3-4.3a1 1 0 0 1 1.42-1.42L11 12.17V4a1 1 0 0 1 1-1zm-7 14a1 1 0 0 1 1 1v1h12v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
), /* @__PURE__ */ React.createElement("button", { className: "close", type: "button", onClick: onClose }, "\xD7"))), /* @__PURE__ */ React.createElement("div", { className: "request-preview-body" }, loading ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u0440\u0435\u0434\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430...") : null, !loading && !error && hint ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, hint) : null, error ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, error) : null, !loading && !error && kind === "image" && resolvedUrl ? /* @__PURE__ */ React.createElement("img", { className: "request-preview-image", src: resolvedUrl, alt: fileName || "attachment" }) : null, !loading && !error && kind === "video" && resolvedUrl ? /* @__PURE__ */ React.createElement("video", { className: "request-preview-video", src: resolvedUrl, controls: true, preload: "metadata" }) : null, !loading && !error && kind === "pdf" && resolvedUrl ? /* @__PURE__ */ React.createElement("iframe", { className: "request-preview-frame", src: resolvedUrl, title: fileName || "preview" }) : null, !loading && !error && kind === "text" ? /* @__PURE__ */ React.createElement("pre", { className: "request-preview-text" }, resolvedText || "\u0424\u0430\u0439\u043B \u043F\u0443\u0441\u0442.") : null, kind === "none" ? /* @__PURE__ */ React.createElement("p", { className: "request-preview-note" }, "\u0414\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0442\u0438\u043F\u0430 \u0444\u0430\u0439\u043B\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0435 \u0438\u043B\u0438 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435.") : null)));
|
|
}
|
|
function GlobalTooltipLayer() {
|
|
const [tooltip, setTooltip] = useState4({ open: false, text: "", x: 0, y: 0, maxWidth: 320 });
|
|
const activeRef = useRef4(null);
|
|
useEffect4(() => {
|
|
const getTarget = (node) => {
|
|
if (!(node instanceof Element)) return null;
|
|
const el = node.closest("[data-tooltip]");
|
|
if (!el) return null;
|
|
const text = String(el.getAttribute("data-tooltip") || "").trim();
|
|
return text ? el : null;
|
|
};
|
|
const reposition = (el) => {
|
|
if (!(el instanceof Element)) return;
|
|
const text = String(el.getAttribute("data-tooltip") || "").trim();
|
|
if (!text) return;
|
|
const rect = el.getBoundingClientRect();
|
|
const vw = window.innerWidth || 0;
|
|
const maxWidth = Math.min(360, Math.max(140, vw - 24));
|
|
const approxWidth = Math.min(maxWidth, Math.max(80, text.length * 7.1 + 22));
|
|
const centerX = rect.left + rect.width / 2;
|
|
const x = Math.max(12 + approxWidth / 2, Math.min(vw - 12 - approxWidth / 2, centerX));
|
|
const y = Math.max(8, rect.top - 8);
|
|
setTooltip({ open: true, text, x, y, maxWidth });
|
|
};
|
|
const open = (node) => {
|
|
const target = getTarget(node);
|
|
if (!target) return;
|
|
activeRef.current = target;
|
|
reposition(target);
|
|
};
|
|
const closeIfNeeded = (related) => {
|
|
const current = activeRef.current;
|
|
if (!current) return;
|
|
if (related instanceof Element) {
|
|
if (related === current || current.contains(related)) return;
|
|
const nextTarget = getTarget(related);
|
|
if (nextTarget === current) return;
|
|
}
|
|
activeRef.current = null;
|
|
setTooltip((prev) => ({ ...prev, open: false }));
|
|
};
|
|
const onMouseOver = (event) => open(event.target);
|
|
const onFocusIn = (event) => open(event.target);
|
|
const onMouseOut = (event) => closeIfNeeded(event.relatedTarget);
|
|
const onFocusOut = (event) => closeIfNeeded(event.relatedTarget);
|
|
const onUpdatePosition = () => {
|
|
if (activeRef.current) reposition(activeRef.current);
|
|
};
|
|
document.addEventListener("mouseover", onMouseOver, true);
|
|
document.addEventListener("focusin", onFocusIn, true);
|
|
document.addEventListener("mouseout", onMouseOut, true);
|
|
document.addEventListener("focusout", onFocusOut, true);
|
|
window.addEventListener("scroll", onUpdatePosition, true);
|
|
window.addEventListener("resize", onUpdatePosition);
|
|
return () => {
|
|
document.removeEventListener("mouseover", onMouseOver, true);
|
|
document.removeEventListener("focusin", onFocusIn, true);
|
|
document.removeEventListener("mouseout", onMouseOut, true);
|
|
document.removeEventListener("focusout", onFocusOut, true);
|
|
window.removeEventListener("scroll", onUpdatePosition, true);
|
|
window.removeEventListener("resize", onUpdatePosition);
|
|
};
|
|
}, []);
|
|
return /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "global-tooltip-layer" + (tooltip.open ? " open" : ""),
|
|
style: { left: tooltip.x + "px", top: tooltip.y + "px", maxWidth: tooltip.maxWidth + "px" },
|
|
role: "tooltip",
|
|
"aria-hidden": tooltip.open ? "false" : "true"
|
|
},
|
|
tooltip.text
|
|
);
|
|
}
|
|
function App() {
|
|
const routeInfo = useMemo2(() => resolveAdminRoute(window.location.search), []);
|
|
const isRequestWorkspaceRoute = routeInfo.view === "request" && Boolean(routeInfo.requestId);
|
|
const initialSection = isRequestWorkspaceRoute ? "requestWorkspace" : routeInfo.section || "dashboard";
|
|
const [token, setToken] = useState4("");
|
|
const [role, setRole] = useState4("");
|
|
const [email, setEmail] = useState4("");
|
|
const [userId, setUserId] = useState4("");
|
|
const [activeSection, setActiveSection] = useState4(initialSection);
|
|
const dashboardLoadRef = useRef4(0);
|
|
const [dashboardData, setDashboardData] = useState4({
|
|
scope: "",
|
|
cards: [],
|
|
byStatus: {},
|
|
lawyerLoads: [],
|
|
myUnreadByEvent: {},
|
|
myUnreadTotal: 0,
|
|
myUnreadNotificationsTotal: 0,
|
|
unreadForClients: 0,
|
|
unreadForLawyers: 0,
|
|
serviceRequestUnreadTotal: 0,
|
|
deadlineAlertTotal: 0,
|
|
monthRevenue: 0,
|
|
monthExpenses: 0
|
|
});
|
|
const {
|
|
tables,
|
|
tablesRef,
|
|
setTableState,
|
|
resetTablesState,
|
|
tableCatalog,
|
|
setTableCatalog,
|
|
referenceRowsMap,
|
|
setReferenceRowsMap
|
|
} = useTablesState();
|
|
const [dictionaries, setDictionaries] = useState4({
|
|
topics: [],
|
|
statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })),
|
|
formFieldTypes: [...DEFAULT_FORM_FIELD_TYPES],
|
|
formFieldKeys: [],
|
|
users: []
|
|
});
|
|
const [statusMap, setStatusMap] = useState4({});
|
|
const [smsProviderHealth, setSmsProviderHealth] = useState4(null);
|
|
const [totpStatus, setTotpStatus] = useState4({
|
|
mode: "password_totp_optional",
|
|
enabled: false,
|
|
required: false,
|
|
has_backup_codes: false
|
|
});
|
|
const [totpSetupModal, setTotpSetupModal] = useState4({
|
|
open: false,
|
|
secret: "",
|
|
uri: "",
|
|
qrDataUrl: "",
|
|
code: "",
|
|
loading: false
|
|
});
|
|
const [accountModal, setAccountModal] = useState4({
|
|
open: false,
|
|
loading: false,
|
|
saving: false,
|
|
initial: {
|
|
name: "",
|
|
email: "",
|
|
phone: ""
|
|
},
|
|
form: {
|
|
name: "",
|
|
email: "",
|
|
phone: "",
|
|
password: "",
|
|
passwordConfirm: ""
|
|
}
|
|
});
|
|
const [recordModal, setRecordModal] = useState4({
|
|
open: false,
|
|
tableKey: null,
|
|
mode: "create",
|
|
rowId: null,
|
|
form: {}
|
|
});
|
|
const [configActiveKey, setConfigActiveKey] = useState4("");
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState4(() => {
|
|
try {
|
|
return window.localStorage.getItem("law-admin-sidebar-collapsed") === "1";
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
});
|
|
const [referencesExpanded, setReferencesExpanded] = useState4(true);
|
|
const [statusDesignerTopicCode, setStatusDesignerTopicCode] = useState4("");
|
|
const [menuTreeScrollbar, setMenuTreeScrollbar] = useState4({ visible: false, top: 0, height: 0 });
|
|
const [metaEntity, setMetaEntity] = useState4("quotes");
|
|
const [metaJson, setMetaJson] = useState4("");
|
|
const [filterModal, setFilterModal] = useState4({
|
|
open: false,
|
|
tableKey: null,
|
|
field: "",
|
|
op: "=",
|
|
rawValue: "",
|
|
editIndex: null
|
|
});
|
|
const [reassignModal, setReassignModal] = useState4({
|
|
open: false,
|
|
requestId: null,
|
|
trackNumber: "",
|
|
lawyerId: ""
|
|
});
|
|
const initialRouteHandledRef = useRef4(false);
|
|
const statusDesignerLoadedTopicRef = useRef4("");
|
|
const menuTreeRef = useRef4(null);
|
|
const menuTreeDragRef = useRef4(null);
|
|
const setStatus = useCallback2((key, message, kind) => {
|
|
setStatusMap((prev) => ({ ...prev, [key]: { message: message || "", kind: kind || "" } }));
|
|
}, []);
|
|
const getStatus = useCallback2((key) => statusMap[key] || { message: "", kind: "" }, [statusMap]);
|
|
const isAdminTokenExpired = useCallback2((rawToken) => {
|
|
const payload = decodeJwtPayload(rawToken || "");
|
|
const exp = Number(payload?.exp || 0);
|
|
if (!payload || !payload.role || !payload.email) return true;
|
|
if (!Number.isFinite(exp) || exp <= 0) return true;
|
|
return exp * 1e3 <= Date.now();
|
|
}, []);
|
|
const api = useAdminApi(token);
|
|
const {
|
|
requestModal,
|
|
setRequestModal,
|
|
resetRequestWorkspaceState,
|
|
updateRequestModalMessageDraft,
|
|
appendRequestModalFiles,
|
|
removeRequestModalFile,
|
|
clearRequestModalFiles,
|
|
loadRequestModalData,
|
|
refreshRequestModal,
|
|
openRequestDetails,
|
|
clearPendingStatusChangePreset,
|
|
submitRequestStatusChange,
|
|
submitRequestModalMessage,
|
|
probeRequestLive,
|
|
loadOlderRequestMessages,
|
|
setRequestTyping,
|
|
loadRequestDataTemplates,
|
|
loadRequestDataBatch,
|
|
loadRequestDataTemplateDetails,
|
|
saveRequestDataTemplate,
|
|
saveRequestDataBatch,
|
|
issueRequestInvoice
|
|
} = useRequestWorkspace({
|
|
api,
|
|
setStatus,
|
|
setActiveSection,
|
|
token,
|
|
users: dictionaries.users,
|
|
buildUniversalQuery,
|
|
resolveAdminObjectSrc
|
|
});
|
|
const getStatusOptions = useCallback2(() => {
|
|
return (dictionaries.statuses || []).filter((item) => item && item.code).map((item) => ({ value: item.code, label: String(item.name || "").trim() || humanizeKey(item.code) }));
|
|
}, [dictionaries.statuses]);
|
|
const getInvoiceStatusOptions = useCallback2(() => {
|
|
return Object.entries(INVOICE_STATUS_LABELS).map(([code, name]) => ({ value: code, label: name }));
|
|
}, []);
|
|
const getServiceRequestTypeOptions = useCallback2(() => {
|
|
return Object.entries(SERVICE_REQUEST_TYPE_LABELS).map(([code, name]) => ({ value: code, label: name }));
|
|
}, []);
|
|
const getServiceRequestStatusOptions = useCallback2(() => {
|
|
return Object.entries(SERVICE_REQUEST_STATUS_LABELS).map(([code, name]) => ({ value: code, label: name }));
|
|
}, []);
|
|
const getStatusKindOptions = useCallback2(() => {
|
|
return Object.entries(STATUS_KIND_LABELS).map(([code, name]) => ({ value: code, label: name }));
|
|
}, []);
|
|
const getTopicOptions = useCallback2(() => {
|
|
return (dictionaries.topics || []).filter((item) => item && item.code).map((item) => ({ value: item.code, label: String(item.name || "").trim() || humanizeKey(item.code) }));
|
|
}, [dictionaries.topics]);
|
|
const getLawyerOptions = useCallback2(() => {
|
|
return (dictionaries.users || []).filter((item) => item && item.id && String(item.role || "").toUpperCase() === "LAWYER").map((item) => ({
|
|
value: item.id,
|
|
label: (item.name || item.email || item.id) + (item.email ? " (" + item.email + ")" : "")
|
|
}));
|
|
}, [dictionaries.users]);
|
|
const getLawyerAndAdminOptions = useCallback2(() => {
|
|
return (dictionaries.users || []).filter((item) => item && item.id && ["LAWYER", "ADMIN"].includes(String(item.role || "").toUpperCase())).map((item) => ({
|
|
value: item.id,
|
|
label: (item.name || item.email || item.id) + (item.email ? " (" + item.email + ")" : "")
|
|
}));
|
|
}, [dictionaries.users]);
|
|
const getFormFieldTypeOptions = useCallback2(() => {
|
|
return (dictionaries.formFieldTypes || []).filter(Boolean).map((item) => ({ value: item, label: item }));
|
|
}, [dictionaries.formFieldTypes]);
|
|
const getRequestDataValueTypeOptions = useCallback2(() => {
|
|
return [
|
|
{ value: "string", label: "\u0421\u0442\u0440\u043E\u043A\u0430" },
|
|
{ value: "date", label: "\u0414\u0430\u0442\u0430" },
|
|
{ value: "number", label: "\u0427\u0438\u0441\u043B\u043E" },
|
|
{ value: "file", label: "\u0424\u0430\u0439\u043B" },
|
|
{ value: "text", label: "\u0422\u0435\u043A\u0441\u0442" }
|
|
];
|
|
}, []);
|
|
const getFormFieldKeyOptions = useCallback2(() => {
|
|
return (dictionaries.formFieldKeys || []).filter((item) => item && item.key).map((item) => ({ value: item.key, label: String(item.label || "").trim() || humanizeKey(item.key) }));
|
|
}, [dictionaries.formFieldKeys]);
|
|
const getRoleOptions = useCallback2(() => {
|
|
return Object.entries(ROLE_LABELS).map(([code, label]) => ({ value: code, label }));
|
|
}, []);
|
|
const tableCatalogMap = useMemo2(() => {
|
|
const map = {};
|
|
(tableCatalog || []).forEach((item) => {
|
|
if (!item || !item.key) return;
|
|
map[item.key] = item;
|
|
});
|
|
return map;
|
|
}, [tableCatalog]);
|
|
const getReferenceOptions = useCallback2(
|
|
(rawReference) => {
|
|
const reference = normalizeReferenceMeta(rawReference);
|
|
if (!reference) return [];
|
|
const rows = referenceRowsMap[reference.table] || [];
|
|
const map = /* @__PURE__ */ new Map();
|
|
rows.forEach((row) => {
|
|
if (!row || typeof row !== "object") return;
|
|
const rawValue = row[reference.value_field];
|
|
if (rawValue == null || rawValue === "") return;
|
|
const value = String(rawValue);
|
|
const labelRaw = row[reference.label_field];
|
|
let label;
|
|
if (reference.table === "admin_users") {
|
|
const name = String(labelRaw || "").trim();
|
|
const email2 = String(row.email || "").trim();
|
|
if (name && email2) label = name + " (" + email2 + ")";
|
|
else if (name) label = name;
|
|
else if (email2) label = email2;
|
|
else label = value;
|
|
} else {
|
|
label = String(labelRaw == null || labelRaw === "" ? rawValue : labelRaw);
|
|
}
|
|
if (!map.has(value)) map.set(value, label);
|
|
});
|
|
return Array.from(map.entries()).map(([value, label]) => ({ value, label })).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru"));
|
|
},
|
|
[referenceRowsMap]
|
|
);
|
|
const resolveReferenceLabel = useCallback2(
|
|
(rawReference, rawValue) => {
|
|
if (rawValue == null || rawValue === "") return "-";
|
|
const value = String(rawValue);
|
|
const options = getReferenceOptions(rawReference);
|
|
const found = options.find((item) => String(item.value) === value);
|
|
return found ? found.label : value;
|
|
},
|
|
[getReferenceOptions]
|
|
);
|
|
const getStatusGroupOptions = useCallback2(() => {
|
|
return getReferenceOptions({ table: "status_groups", value_field: "id", label_field: "name" });
|
|
}, [getReferenceOptions]);
|
|
const getClientOptions = useCallback2(() => {
|
|
return getReferenceOptions({ table: "clients", value_field: "id", label_field: "full_name" });
|
|
}, [getReferenceOptions]);
|
|
const getInvoiceRequestRows = useCallback2(() => {
|
|
const fromReferences = Array.isArray(referenceRowsMap.requests) ? referenceRowsMap.requests : [];
|
|
const fromTable = Array.isArray(tables.requests?.rows) ? tables.requests.rows : [];
|
|
const byTrack = /* @__PURE__ */ new Map();
|
|
[...fromReferences, ...fromTable].forEach((row) => {
|
|
const track = String(row?.track_number || "").trim().toUpperCase();
|
|
if (!track) return;
|
|
if (!byTrack.has(track)) byTrack.set(track, row);
|
|
});
|
|
return Array.from(byTrack.values());
|
|
}, [referenceRowsMap.requests, tables.requests?.rows]);
|
|
const getInvoiceRequestTrackOptions = useCallback2(() => {
|
|
const rows = getInvoiceRequestRows();
|
|
return rows.map((row) => {
|
|
const track = String(row?.track_number || "").trim().toUpperCase();
|
|
if (!track) return null;
|
|
const clientName = String(row?.client_name || "").trim();
|
|
const clientPhone = String(row?.client_phone || "").trim();
|
|
const parts = [track];
|
|
if (clientName) parts.push(clientName);
|
|
if (clientPhone) parts.push(clientPhone);
|
|
return { value: track, label: parts.join(" \u2022 ") };
|
|
}).filter(Boolean).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru"));
|
|
}, [getInvoiceRequestRows]);
|
|
const getInvoicePayerOptions = useCallback2((formOrTrack) => {
|
|
const map = /* @__PURE__ */ new Map();
|
|
const addPayer = (nameRaw, phoneRaw) => {
|
|
const name = String(nameRaw || "").trim();
|
|
if (!name) return;
|
|
const phone = String(phoneRaw || "").trim();
|
|
if (map.has(name)) return;
|
|
map.set(name, phone ? `${name} (${phone})` : name);
|
|
};
|
|
const rows = getInvoiceRequestRows();
|
|
const trackFromInput = typeof formOrTrack === "string" ? formOrTrack : String(formOrTrack?.request_track_number || "").trim();
|
|
const requestIdFromInput = typeof formOrTrack === "string" ? "" : String(formOrTrack?.request_id || "").trim();
|
|
const normalizedTrack = String(trackFromInput || "").trim().toUpperCase();
|
|
const selectedRequest = rows.find((row) => {
|
|
const rowTrack = String(row?.track_number || "").trim().toUpperCase();
|
|
const rowId = String(row?.id || "").trim();
|
|
return normalizedTrack && rowTrack === normalizedTrack || requestIdFromInput && rowId === requestIdFromInput;
|
|
});
|
|
if (selectedRequest) {
|
|
addPayer(selectedRequest?.client_name, selectedRequest?.client_phone);
|
|
} else {
|
|
const clientRows = Array.isArray(referenceRowsMap.clients) ? referenceRowsMap.clients : [];
|
|
clientRows.forEach((row) => addPayer(row?.full_name || row?.client_name, row?.phone || row?.client_phone));
|
|
rows.forEach((row) => addPayer(row?.client_name, row?.client_phone));
|
|
}
|
|
return Array.from(map.entries()).map(([value, label]) => ({ value, label })).sort((a, b) => String(a.label).localeCompare(String(b.label), "ru"));
|
|
}, [getInvoiceRequestRows, referenceRowsMap.clients]);
|
|
const dictionaryTableItems = useMemo2(() => {
|
|
return (tableCatalog || []).filter(
|
|
(item) => item && item.section === "dictionary" && Array.isArray(item.actions) && item.actions.includes("query") && !LEGACY_HIDDEN_DICTIONARY_TABLES.has(String(item.key || ""))
|
|
).sort((a, b) => String(a.label || a.key).localeCompare(String(b.label || b.key), "ru"));
|
|
}, [tableCatalog]);
|
|
const resolveTableConfig = useCallback2(
|
|
(tableKey) => {
|
|
if (TABLE_SERVER_CONFIG[tableKey]) return TABLE_SERVER_CONFIG[tableKey];
|
|
const meta = tableCatalogMap[tableKey];
|
|
if (!meta || !meta.table) return null;
|
|
const tableName = String(meta.table || tableKey);
|
|
return {
|
|
table: tableName,
|
|
endpoint: String(meta.query_endpoint || "/api/admin/crud/" + tableName + "/query"),
|
|
sort: Array.isArray(meta.default_sort) && meta.default_sort.length ? meta.default_sort : [{ field: "created_at", dir: "desc" }]
|
|
};
|
|
},
|
|
[tableCatalogMap]
|
|
);
|
|
const resolveMutationConfig = useCallback2(
|
|
(tableKey) => {
|
|
if (TABLE_MUTATION_CONFIG[tableKey]) return TABLE_MUTATION_CONFIG[tableKey];
|
|
const meta = tableCatalogMap[tableKey];
|
|
if (!meta || !meta.table) return null;
|
|
const tableName = String(meta.table || tableKey);
|
|
return {
|
|
create: String(meta.create_endpoint || "/api/admin/crud/" + tableName),
|
|
update: (id) => String(meta.update_endpoint_template || "/api/admin/crud/" + tableName + "/{id}").replace("{id}", String(id)),
|
|
delete: (id) => String(meta.delete_endpoint_template || "/api/admin/crud/" + tableName + "/{id}").replace("{id}", String(id))
|
|
};
|
|
},
|
|
[tableCatalogMap]
|
|
);
|
|
const getFilterFields = useCallback2(
|
|
(tableKey) => {
|
|
if (tableKey === "kanban") {
|
|
return [
|
|
{ field: "assigned_lawyer_id", label: "\u042E\u0440\u0438\u0441\u0442", type: "reference", options: getLawyerOptions },
|
|
{ field: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442", type: "text" },
|
|
{ field: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "reference", options: getStatusOptions },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430", type: "date" },
|
|
{ field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions },
|
|
{ field: "has_unread_updates", label: "\u0415\u0441\u0442\u044C \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F", type: "boolean" },
|
|
{ field: "deadline_alert", label: "\u0413\u043E\u0440\u044F\u0449\u0438\u0435 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u044B", type: "boolean" },
|
|
{ field: "overdue", label: "\u041F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D", type: "boolean" }
|
|
];
|
|
}
|
|
if (tableKey === "requests") {
|
|
return [
|
|
{ field: "track_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438", type: "text" },
|
|
{ field: "client_name", label: "\u041A\u043B\u0438\u0435\u043D\u0442", type: "text" },
|
|
{ field: "client_phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", type: "text" },
|
|
{ field: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "reference", options: getStatusOptions },
|
|
{ field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions },
|
|
{ field: "important_date_at", label: "\u0412\u0430\u0436\u043D\u0430\u044F \u0434\u0430\u0442\u0430", type: "date" },
|
|
{ field: "has_unread_updates", label: "\u0415\u0441\u0442\u044C \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F", type: "boolean" },
|
|
{ field: "deadline_alert", label: "\u0413\u043E\u0440\u044F\u0449\u0438\u0435 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u044B", type: "boolean" },
|
|
{ field: "client_has_unread_updates", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u043C", type: "boolean" },
|
|
{ field: "lawyer_has_unread_updates", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u044E\u0440\u0438\u0441\u0442\u043E\u043C", type: "boolean" },
|
|
{ field: "invoice_amount", label: "\u0421\u0443\u043C\u043C\u0430 \u0441\u0447\u0435\u0442\u0430", type: "number" },
|
|
{ field: "effective_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430", type: "number" },
|
|
{ field: "paid_at", label: "\u041E\u043F\u043B\u0430\u0447\u0435\u043D\u043E", type: "date" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
if (tableKey === "serviceRequests") {
|
|
return [
|
|
{ field: "type", label: "\u0422\u0438\u043F", type: "enum", options: getServiceRequestTypeOptions },
|
|
{ field: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "enum", options: getServiceRequestStatusOptions },
|
|
{ field: "request_id", label: "ID \u0437\u0430\u044F\u0432\u043A\u0438", type: "text" },
|
|
{ field: "client_id", label: "ID \u043A\u043B\u0438\u0435\u043D\u0442\u0430", type: "text" },
|
|
{ field: "assigned_lawyer_id", label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0439 \u044E\u0440\u0438\u0441\u0442", type: "reference", options: getLawyerOptions },
|
|
{ field: "admin_unread", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u043E\u043C", type: "boolean" },
|
|
{ field: "lawyer_unread", label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u044E\u0440\u0438\u0441\u0442\u043E\u043C", type: "boolean" },
|
|
{ field: "resolved_at", label: "\u0414\u0430\u0442\u0430 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0438", type: "date" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
if (tableKey === "invoices") {
|
|
return [
|
|
{ field: "invoice_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0441\u0447\u0435\u0442\u0430", type: "text" },
|
|
{ field: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "enum", options: getInvoiceStatusOptions },
|
|
{ field: "amount", label: "\u0421\u0443\u043C\u043C\u0430", type: "number" },
|
|
{ field: "currency", label: "\u0412\u0430\u043B\u044E\u0442\u0430", type: "text" },
|
|
{ field: "payer_display_name", label: "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A", type: "text" },
|
|
{ field: "request_id", label: "ID \u0437\u0430\u044F\u0432\u043A\u0438", type: "text" },
|
|
{ field: "issued_by_admin_user_id", label: "ID \u0441\u043E\u0442\u0440\u0443\u0434\u043D\u0438\u043A\u0430", type: "text" },
|
|
{ field: "issued_at", label: "\u0414\u0430\u0442\u0430 \u0444\u043E\u0440\u043C\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F", type: "date" },
|
|
{ field: "paid_at", label: "\u0414\u0430\u0442\u0430 \u043E\u043F\u043B\u0430\u0442\u044B", type: "date" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
if (tableKey === "quotes") {
|
|
return [
|
|
{ field: "author", label: "\u0410\u0432\u0442\u043E\u0440", type: "text" },
|
|
{ field: "text", label: "\u0422\u0435\u043A\u0441\u0442", type: "text" },
|
|
{ field: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", type: "text" },
|
|
{ field: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
if (tableKey === "topics") {
|
|
return [
|
|
{ field: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text" },
|
|
{ field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }
|
|
];
|
|
}
|
|
if (tableKey === "statuses") {
|
|
return [
|
|
{ field: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text" },
|
|
{ field: "status_group_id", label: "\u0413\u0440\u0443\u043F\u043F\u0430", type: "reference", options: getStatusGroupOptions },
|
|
{ field: "kind", label: "\u0422\u0438\u043F", type: "enum", options: getStatusKindOptions },
|
|
{ field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" },
|
|
{ field: "is_terminal", label: "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439", type: "boolean" }
|
|
];
|
|
}
|
|
if (tableKey === "formFields") {
|
|
return [
|
|
{ field: "key", label: "\u041A\u043B\u044E\u0447", type: "text" },
|
|
{ field: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text" },
|
|
{ field: "type", label: "\u0422\u0438\u043F", type: "enum", options: getFormFieldTypeOptions },
|
|
{ field: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean" },
|
|
{ field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }
|
|
];
|
|
}
|
|
if (tableKey === "topicRequiredFields") {
|
|
return [
|
|
{ field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions },
|
|
{ field: "field_key", label: "\u041F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", type: "reference", options: getFormFieldKeyOptions },
|
|
{ field: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean" },
|
|
{ field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }
|
|
];
|
|
}
|
|
if (tableKey === "topicDataTemplates") {
|
|
return [
|
|
{ field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions },
|
|
{ field: "key", label: "\u041A\u043B\u044E\u0447", type: "text" },
|
|
{ field: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text" },
|
|
{ field: "value_type", label: "\u0422\u0438\u043F \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F", type: "enum", options: getRequestDataValueTypeOptions },
|
|
{ field: "document_name", label: "\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442", type: "text" },
|
|
{ field: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean" },
|
|
{ field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
if (tableKey === "statusTransitions") {
|
|
return [
|
|
{ field: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", options: getTopicOptions },
|
|
{ field: "from_status", label: "\u0418\u0437 \u0441\u0442\u0430\u0442\u0443\u0441\u0430", type: "reference", options: getStatusOptions },
|
|
{ field: "to_status", label: "\u0412 \u0441\u0442\u0430\u0442\u0443\u0441", type: "reference", options: getStatusOptions },
|
|
{ field: "sla_hours", label: "SLA (\u0447\u0430\u0441\u044B)", type: "number" },
|
|
{ field: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean" },
|
|
{ field: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number" }
|
|
];
|
|
}
|
|
if (tableKey === "users") {
|
|
return [
|
|
{ field: "name", label: "\u0418\u043C\u044F", type: "text" },
|
|
{ field: "email", label: "Email", type: "text" },
|
|
{ field: "phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", type: "text" },
|
|
{ field: "role", label: "\u0420\u043E\u043B\u044C", type: "enum", options: getRoleOptions },
|
|
{ field: "primary_topic_code", label: "\u041F\u0440\u043E\u0444\u0438\u043B\u044C (\u0442\u0435\u043C\u0430)", type: "reference", options: getTopicOptions },
|
|
{ field: "default_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E", type: "number" },
|
|
{ field: "salary_percent", label: "\u041F\u0440\u043E\u0446\u0435\u043D\u0442 \u0437\u0430\u0440\u043F\u043B\u0430\u0442\u044B", type: "number" },
|
|
{ field: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean" },
|
|
{ field: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", type: "text" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
if (tableKey === "userTopics") {
|
|
return [
|
|
{ field: "admin_user_id", label: "\u042E\u0440\u0438\u0441\u0442", type: "reference", options: getLawyerOptions },
|
|
{ field: "topic_code", label: "\u0414\u043E\u043F. \u0442\u0435\u043C\u0430", type: "reference", options: getTopicOptions },
|
|
{ field: "responsible", label: "\u041E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439", type: "text" },
|
|
{ field: "created_at", label: "\u0414\u0430\u0442\u0430 \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u044F", type: "date" }
|
|
];
|
|
}
|
|
const meta = tableCatalogMap[tableKey];
|
|
if (!meta || !Array.isArray(meta.columns)) return [];
|
|
return (meta.columns || []).filter((column) => column && column.name && column.filterable !== false && String(column.name) !== "id").map((column) => {
|
|
const name = String(column.name);
|
|
const label = String(column.label || humanizeKey(name));
|
|
if (name === "topic_code") return { field: name, label, type: "reference", options: getTopicOptions };
|
|
if (name === "status_code" || name === "from_status" || name === "to_status") {
|
|
return { field: name, label, type: "reference", options: getStatusOptions };
|
|
}
|
|
if (name === "field_key") return { field: name, label, type: "reference", options: getFormFieldKeyOptions };
|
|
const reference = normalizeReferenceMeta(column.reference);
|
|
if (reference) {
|
|
return { field: name, label, type: "reference", options: () => getReferenceOptions(reference) };
|
|
}
|
|
return { field: name, label, type: metaKindToFilterType(column.kind) };
|
|
});
|
|
},
|
|
[
|
|
getReferenceOptions,
|
|
tableCatalogMap,
|
|
getFormFieldKeyOptions,
|
|
getFormFieldTypeOptions,
|
|
getInvoiceStatusOptions,
|
|
getLawyerOptions,
|
|
getRoleOptions,
|
|
getServiceRequestStatusOptions,
|
|
getServiceRequestTypeOptions,
|
|
role,
|
|
getStatusGroupOptions,
|
|
getStatusKindOptions,
|
|
getStatusOptions,
|
|
getTopicOptions
|
|
]
|
|
);
|
|
const getTableLabel = useCallback2((tableKey) => {
|
|
if (tableKey === "kanban") return "\u041A\u0430\u043D\u0431\u0430\u043D";
|
|
if (tableKey === "requests") return "\u0417\u0430\u044F\u0432\u043A\u0438";
|
|
if (tableKey === "serviceRequests") return "\u0417\u0430\u043F\u0440\u043E\u0441\u044B";
|
|
if (tableKey === "invoices") return "\u0421\u0447\u0435\u0442\u0430";
|
|
if (tableKey === "quotes") return "\u0426\u0438\u0442\u0430\u0442\u044B";
|
|
if (tableKey === "topics") return "\u0422\u0435\u043C\u044B";
|
|
if (tableKey === "statuses") return "\u0421\u0442\u0430\u0442\u0443\u0441\u044B";
|
|
if (tableKey === "statusGroups") return "\u0413\u0440\u0443\u043F\u043F\u044B \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432";
|
|
if (tableKey === "formFields") return "\u041F\u043E\u043B\u044F \u0444\u043E\u0440\u043C\u044B";
|
|
if (tableKey === "topicRequiredFields") return "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u043E\u043B\u044F \u043F\u043E \u0442\u0435\u043C\u0430\u043C";
|
|
if (tableKey === "topicDataTemplates") return "\u0428\u0430\u0431\u043B\u043E\u043D\u044B \u0434\u043E\u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u043F\u043E \u0442\u0435\u043C\u0430\u043C";
|
|
if (tableKey === "statusTransitions") return "\u041F\u0435\u0440\u0435\u0445\u043E\u0434\u044B \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u0432";
|
|
if (tableKey === "users") return "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438";
|
|
if (tableKey === "userTopics") return "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0442\u0435\u043C\u044B \u044E\u0440\u0438\u0441\u0442\u043E\u0432";
|
|
const meta = tableCatalogMap[tableKey];
|
|
if (meta && meta.label) return String(meta.label);
|
|
const raw = TABLE_UNALIASES[tableKey] || tableKey;
|
|
return humanizeKey(raw);
|
|
}, [tableCatalogMap]);
|
|
const statusDesignerRows = useMemo2(() => {
|
|
const activeTopic = String(statusDesignerTopicCode || "").trim();
|
|
const rows = tables.statusTransitions.rows || [];
|
|
if (!activeTopic) return rows;
|
|
return rows.filter((row) => String(row.topic_code || "") === activeTopic);
|
|
}, [statusDesignerTopicCode, tables.statusTransitions.rows]);
|
|
const statusDesignerCards = useMemo2(() => {
|
|
const rows = statusDesignerRows || [];
|
|
if (!rows.length) return [];
|
|
const orderMap = /* @__PURE__ */ new Map();
|
|
(tables.statuses.rows || []).forEach((row, index) => {
|
|
const code = String(row?.code || "").trim();
|
|
if (!code) return;
|
|
const sortOrder = Number(row?.sort_order);
|
|
orderMap.set(code, Number.isFinite(sortOrder) ? sortOrder : index);
|
|
});
|
|
const statusMetaMap = /* @__PURE__ */ new Map();
|
|
(dictionaries.statuses || []).forEach((row, index) => {
|
|
const code = String(row?.code || "").trim();
|
|
if (!code) return;
|
|
statusMetaMap.set(code, {
|
|
name: String(row?.name || code),
|
|
isTerminal: false,
|
|
order: orderMap.get(code) ?? index
|
|
});
|
|
});
|
|
(tables.statuses.rows || []).forEach((row, index) => {
|
|
const code = String(row?.code || "").trim();
|
|
if (!code) return;
|
|
statusMetaMap.set(code, {
|
|
name: String(row?.name || code),
|
|
isTerminal: Boolean(row?.is_terminal),
|
|
order: orderMap.get(code) ?? index
|
|
});
|
|
});
|
|
const codeSet = /* @__PURE__ */ new Set();
|
|
rows.forEach((row) => {
|
|
const fromCode = String(row?.from_status || "").trim();
|
|
const toCode = String(row?.to_status || "").trim();
|
|
if (fromCode) codeSet.add(fromCode);
|
|
if (toCode) codeSet.add(toCode);
|
|
});
|
|
const codes = Array.from(codeSet.values()).sort((a, b) => {
|
|
const aOrder = statusMetaMap.get(a)?.order;
|
|
const bOrder = statusMetaMap.get(b)?.order;
|
|
if (aOrder != null && bOrder != null && aOrder !== bOrder) return aOrder - bOrder;
|
|
if (aOrder != null && bOrder == null) return -1;
|
|
if (aOrder == null && bOrder != null) return 1;
|
|
return String(a).localeCompare(String(b), "ru");
|
|
});
|
|
return codes.map((code) => {
|
|
const outgoing = rows.filter((row) => String(row?.from_status || "").trim() === code).sort((a, b) => {
|
|
const aOrder = Number(a?.sort_order || 0);
|
|
const bOrder = Number(b?.sort_order || 0);
|
|
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
return String(a?.to_status || "").localeCompare(String(b?.to_status || ""), "ru");
|
|
});
|
|
const meta = statusMetaMap.get(code) || { name: statusLabel(code), isTerminal: false };
|
|
return {
|
|
code,
|
|
name: String(meta.name || statusLabel(code)),
|
|
isTerminal: Boolean(meta.isTerminal),
|
|
outgoing
|
|
};
|
|
});
|
|
}, [dictionaries.statuses, statusDesignerRows, tables.statuses.rows]);
|
|
const getRecordFields = useCallback2(
|
|
(tableKey) => {
|
|
if (tableKey === "requests") {
|
|
const isNewClientMode = (form) => {
|
|
const value = String(form?.client_id || "").trim();
|
|
return !value || value === NEW_REQUEST_CLIENT_OPTION;
|
|
};
|
|
const fields = [
|
|
{ key: "track_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438", type: "text", optional: true, placeholder: "\u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438" },
|
|
...role !== "LAWYER" ? [
|
|
{
|
|
key: "client_id",
|
|
label: "\u041A\u043B\u0438\u0435\u043D\u0442",
|
|
type: "reference",
|
|
defaultValue: NEW_REQUEST_CLIENT_OPTION,
|
|
options: getClientOptions,
|
|
extraOptions: [{ value: NEW_REQUEST_CLIENT_OPTION, label: "\u041D\u043E\u0432\u044B\u0439 \u043A\u043B\u0438\u0435\u043D\u0442" }],
|
|
fullRow: true
|
|
}
|
|
] : [],
|
|
{
|
|
key: "client_name",
|
|
label: role !== "LAWYER" ? "\u0424\u0418\u041E \u043D\u043E\u0432\u043E\u0433\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430" : "\u041A\u043B\u0438\u0435\u043D\u0442",
|
|
type: "text",
|
|
required: true,
|
|
visibleWhen: role === "LAWYER" ? void 0 : isNewClientMode
|
|
},
|
|
{
|
|
key: "client_phone",
|
|
label: role !== "LAWYER" ? "\u0422\u0435\u043B\u0435\u0444\u043E\u043D \u043D\u043E\u0432\u043E\u0433\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430" : "\u0422\u0435\u043B\u0435\u0444\u043E\u043D",
|
|
type: "text",
|
|
required: true,
|
|
visibleWhen: role === "LAWYER" ? void 0 : isNewClientMode
|
|
},
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", optional: true, options: getTopicOptions },
|
|
{ key: "status_code", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "reference", required: true, options: getStatusOptions },
|
|
{ key: "description", label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", type: "textarea", optional: true },
|
|
{ key: "request_cost", label: "\u0421\u0442\u043E\u0438\u043C\u043E\u0441\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438", type: "number", optional: true }
|
|
];
|
|
if (role !== "LAWYER") {
|
|
fields.push({ key: "assigned_lawyer_id", label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0439 \u044E\u0440\u0438\u0441\u0442", type: "reference", optional: true, options: getLawyerOptions });
|
|
fields.push({ key: "effective_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430 (\u0444\u0438\u043A\u0441.)", type: "number", optional: true });
|
|
}
|
|
return fields;
|
|
}
|
|
if (tableKey === "invoices") {
|
|
return [
|
|
{ key: "request_track_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0437\u0430\u044F\u0432\u043A\u0438", type: "reference", required: true, createOnly: true, options: getInvoiceRequestTrackOptions },
|
|
{ key: "invoice_number", label: "\u041D\u043E\u043C\u0435\u0440 \u0441\u0447\u0435\u0442\u0430", type: "text", optional: true, placeholder: "\u041E\u0441\u0442\u0430\u0432\u044C\u0442\u0435 \u043F\u0443\u0441\u0442\u044B\u043C \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438" },
|
|
{ key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "enum", required: true, options: getInvoiceStatusOptions, defaultValue: "WAITING_PAYMENT" },
|
|
{ key: "amount", label: "\u0421\u0443\u043C\u043C\u0430", type: "number", required: true },
|
|
{ key: "currency", label: "\u0412\u0430\u043B\u044E\u0442\u0430", type: "text", optional: true, defaultValue: "RUB" },
|
|
{
|
|
key: "payer_display_name",
|
|
label: "\u041F\u043B\u0430\u0442\u0435\u043B\u044C\u0449\u0438\u043A (\u0424\u0418\u041E / \u043A\u043E\u043C\u043F\u0430\u043D\u0438\u044F)",
|
|
type: "reference",
|
|
required: true,
|
|
options: (form) => getInvoicePayerOptions(form)
|
|
}
|
|
];
|
|
}
|
|
if (tableKey === "serviceRequests") {
|
|
return [
|
|
{ key: "type", label: "\u0422\u0438\u043F", type: "enum", required: true, options: getServiceRequestTypeOptions },
|
|
{ key: "status", label: "\u0421\u0442\u0430\u0442\u0443\u0441", type: "enum", required: true, options: getServiceRequestStatusOptions },
|
|
{ key: "body", label: "\u041E\u0431\u0440\u0430\u0449\u0435\u043D\u0438\u0435", type: "textarea", required: true, fullRow: true },
|
|
{ key: "request_id", label: "ID \u0437\u0430\u044F\u0432\u043A\u0438", type: "text", required: true },
|
|
{ key: "client_id", label: "ID \u043A\u043B\u0438\u0435\u043D\u0442\u0430", type: "text", optional: true },
|
|
{ key: "assigned_lawyer_id", label: "ID \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u043E\u0433\u043E \u044E\u0440\u0438\u0441\u0442\u0430", type: "text", optional: true }
|
|
];
|
|
}
|
|
if (tableKey === "quotes") {
|
|
return [
|
|
{ key: "author", label: "\u0410\u0432\u0442\u043E\u0440", type: "text", required: true },
|
|
{ key: "text", label: "\u0422\u0435\u043A\u0441\u0442", type: "textarea", required: true },
|
|
{ key: "source", label: "\u0418\u0441\u0442\u043E\u0447\u043D\u0438\u043A", type: "text", optional: true },
|
|
{ key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }
|
|
];
|
|
}
|
|
if (tableKey === "topics") {
|
|
return [
|
|
{ key: "code", label: "\u041A\u043E\u0434", type: "text", required: true, autoCreate: true },
|
|
{ key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text", required: true },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u0430", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }
|
|
];
|
|
}
|
|
if (tableKey === "statuses") {
|
|
return [
|
|
{ key: "code", label: "\u041A\u043E\u0434", type: "text", required: true },
|
|
{ key: "name", label: "\u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435", type: "text", required: true },
|
|
{ key: "status_group_id", label: "\u0413\u0440\u0443\u043F\u043F\u0430", type: "reference", optional: true, options: getStatusGroupOptions },
|
|
{ key: "kind", label: "\u0422\u0438\u043F", type: "enum", required: true, options: getStatusKindOptions, defaultValue: "DEFAULT" },
|
|
{ key: "invoice_template", label: "\u0428\u0430\u0431\u043B\u043E\u043D \u0441\u0447\u0435\u0442\u0430", type: "textarea", optional: true, placeholder: "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043F\u043E\u043B\u044F: {track_number}, {client_name}, {topic_code}, {amount}" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" },
|
|
{ key: "is_terminal", label: "\u0422\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439", type: "boolean", defaultValue: "false" }
|
|
];
|
|
}
|
|
if (tableKey === "formFields") {
|
|
return [
|
|
{ key: "key", label: "\u041A\u043B\u044E\u0447", type: "text", required: true },
|
|
{ key: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text", required: true },
|
|
{ key: "type", label: "\u0422\u0438\u043F", type: "enum", required: true, options: getFormFieldTypeOptions },
|
|
{ key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean", defaultValue: "false" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" },
|
|
{ key: "options", label: "\u041E\u043F\u0446\u0438\u0438 (JSON)", type: "json", optional: true }
|
|
];
|
|
}
|
|
if (tableKey === "topicRequiredFields") {
|
|
return [
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions },
|
|
{ key: "field_key", label: "\u041F\u043E\u043B\u0435 \u0444\u043E\u0440\u043C\u044B", type: "reference", required: true, options: getFormFieldKeyOptions },
|
|
{ key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean", defaultValue: "true" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }
|
|
];
|
|
}
|
|
if (tableKey === "topicDataTemplates") {
|
|
return [
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions },
|
|
{ key: "key", label: "\u041A\u043B\u044E\u0447", type: "text", required: true },
|
|
{ key: "label", label: "\u041C\u0435\u0442\u043A\u0430", type: "text", required: true },
|
|
{ key: "value_type", label: "\u0422\u0438\u043F \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F", type: "enum", required: true, options: getRequestDataValueTypeOptions, defaultValue: "string" },
|
|
{ key: "document_name", label: "\u0414\u043E\u043A\u0443\u043C\u0435\u043D\u0442", type: "text", optional: true, placeholder: "\u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \u0414\u043E\u0433\u043E\u0432\u043E\u0440 / \u041F\u0430\u0441\u043F\u043E\u0440\u0442" },
|
|
{ key: "description", label: "\u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435", type: "textarea", optional: true },
|
|
{ key: "required", label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435", type: "boolean", defaultValue: "true" },
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u043D\u043E", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }
|
|
];
|
|
}
|
|
if (tableKey === "statusTransitions") {
|
|
return [
|
|
{ key: "topic_code", label: "\u0422\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions },
|
|
{ key: "from_status", label: "\u0418\u0437 \u0441\u0442\u0430\u0442\u0443\u0441\u0430", type: "reference", required: true, options: getStatusOptions },
|
|
{ key: "to_status", label: "\u0412 \u0441\u0442\u0430\u0442\u0443\u0441", type: "reference", required: true, options: getStatusOptions },
|
|
{ key: "sla_hours", label: "SLA (\u0447\u0430\u0441\u044B)", type: "number", optional: true },
|
|
{
|
|
key: "required_data_keys",
|
|
label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u043A\u043B\u044E\u0447\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 (JSON-\u043C\u0430\u0441\u0441\u0438\u0432)",
|
|
type: "json",
|
|
optional: true,
|
|
defaultValue: "[]",
|
|
placeholder: '["passport_scan", "client_address"]'
|
|
},
|
|
{
|
|
key: "required_mime_types",
|
|
label: "\u041E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435 MIME-\u0442\u0438\u043F\u044B \u0444\u0430\u0439\u043B\u043E\u0432 (JSON-\u043C\u0430\u0441\u0441\u0438\u0432)",
|
|
type: "json",
|
|
optional: true,
|
|
defaultValue: "[]",
|
|
placeholder: '["application/pdf", "image/*"]'
|
|
},
|
|
{ key: "enabled", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean", defaultValue: "true" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" }
|
|
];
|
|
}
|
|
if (tableKey === "users") {
|
|
return [
|
|
{ key: "name", label: "\u0418\u043C\u044F", type: "text", required: true },
|
|
{ key: "email", label: "Email", type: "text", required: true },
|
|
{ key: "phone", label: "\u0422\u0435\u043B\u0435\u0444\u043E\u043D", type: "text", optional: true, placeholder: "+7..." },
|
|
{ key: "role", label: "\u0420\u043E\u043B\u044C", type: "enum", required: true, options: getRoleOptions, defaultValue: "LAWYER" },
|
|
{
|
|
key: "avatar_url",
|
|
label: "URL \u0430\u0432\u0430\u0442\u0430\u0440\u0430",
|
|
type: "text",
|
|
optional: true,
|
|
placeholder: "https://... \u0438\u043B\u0438 s3://...",
|
|
uploadScope: "USER_AVATAR",
|
|
accept: "image/*"
|
|
},
|
|
{ key: "avatar_original_key", label: "\u041E\u0440\u0438\u0433\u0438\u043D\u0430\u043B \u0430\u0432\u0430\u0442\u0430\u0440\u0430", type: "text", optional: true, hidden: true },
|
|
{ key: "avatar_crop_json", label: "\u041A\u0440\u043E\u043F \u0430\u0432\u0430\u0442\u0430\u0440\u0430", type: "text", optional: true, hidden: true },
|
|
{ key: "primary_topic_code", label: "\u041F\u0440\u043E\u0444\u0438\u043B\u044C (\u0442\u0435\u043C\u0430)", type: "reference", optional: true, options: getTopicOptions },
|
|
{ key: "default_rate", label: "\u0421\u0442\u0430\u0432\u043A\u0430 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E", type: "number", optional: true },
|
|
{ key: "salary_percent", label: "\u041F\u0440\u043E\u0446\u0435\u043D\u0442 \u0437\u0430\u0440\u043F\u043B\u0430\u0442\u044B", type: "number", optional: true },
|
|
{ key: "is_active", label: "\u0410\u043A\u0442\u0438\u0432\u0435\u043D", type: "boolean", defaultValue: "true" },
|
|
{ key: "password", label: "\u041F\u0430\u0440\u043E\u043B\u044C", type: "password", requiredOnCreate: true, optional: true, omitIfEmpty: true, placeholder: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C" }
|
|
];
|
|
}
|
|
if (tableKey === "userTopics") {
|
|
return [
|
|
{ key: "admin_user_id", label: "\u042E\u0440\u0438\u0441\u0442", type: "reference", required: true, options: getLawyerOptions },
|
|
{ key: "topic_code", label: "\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u0442\u0435\u043C\u0430", type: "reference", required: true, options: getTopicOptions }
|
|
];
|
|
}
|
|
if (tableKey === "landing_featured_staff") {
|
|
return [
|
|
{ key: "admin_user_id", label: "\u0421\u043E\u0442\u0440\u0443\u0434\u043D\u0438\u043A", type: "reference", required: true, options: getLawyerAndAdminOptions },
|
|
{ key: "caption", label: "\u041F\u043E\u0434\u043F\u0438\u0441\u044C", type: "textarea", optional: true, placeholder: "\u041A\u0440\u0430\u0442\u043A\u043E\u0435 \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u0438\u0441\u0442\u0430" },
|
|
{ key: "sort_order", label: "\u041F\u043E\u0440\u044F\u0434\u043E\u043A", type: "number", defaultValue: "0" },
|
|
{ key: "pinned", label: "\u0417\u0430\u043A\u0440\u0435\u043F\u043B\u0451\u043D", type: "boolean", defaultValue: "false" },
|
|
{ key: "enabled", label: "\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C", type: "boolean", defaultValue: "true" }
|
|
];
|
|
}
|
|
const meta = tableCatalogMap[tableKey];
|
|
if (!meta || !Array.isArray(meta.columns)) return [];
|
|
return (meta.columns || []).filter((column) => column && column.name && column.editable).map((column) => {
|
|
const key = String(column.name || "");
|
|
const requiredOnCreate = Boolean(column.required_on_create);
|
|
const reference = normalizeReferenceMeta(column.reference);
|
|
return {
|
|
key,
|
|
label: String(column.label || humanizeKey(key)),
|
|
type: reference ? "reference" : metaKindToRecordType(column.kind),
|
|
options: reference ? () => getReferenceOptions(reference) : void 0,
|
|
requiredOnCreate,
|
|
optional: !requiredOnCreate
|
|
};
|
|
});
|
|
},
|
|
[
|
|
getReferenceOptions,
|
|
tableCatalogMap,
|
|
getFormFieldKeyOptions,
|
|
getFormFieldTypeOptions,
|
|
getInvoiceStatusOptions,
|
|
getInvoicePayerOptions,
|
|
getInvoiceRequestTrackOptions,
|
|
getClientOptions,
|
|
getLawyerOptions,
|
|
getRoleOptions,
|
|
getServiceRequestStatusOptions,
|
|
getServiceRequestTypeOptions,
|
|
getStatusGroupOptions,
|
|
getStatusKindOptions,
|
|
getStatusOptions,
|
|
getTopicOptions
|
|
]
|
|
);
|
|
const getFieldDef = useCallback2(
|
|
(tableKey, fieldName) => {
|
|
return getFilterFields(tableKey).find((field) => field.field === fieldName) || null;
|
|
},
|
|
[getFilterFields]
|
|
);
|
|
const getFieldOptions = useCallback2((fieldDef) => {
|
|
if (!fieldDef) return [];
|
|
if (typeof fieldDef.options === "function") return fieldDef.options() || [];
|
|
return [];
|
|
}, []);
|
|
const getFilterValuePreview = useCallback2(
|
|
(tableKey, clause) => {
|
|
const fieldDef = getFieldDef(tableKey, clause.field);
|
|
if (!fieldDef) return String(clause.value ?? "");
|
|
if (fieldDef.type === "boolean") return boolFilterLabel(Boolean(clause.value));
|
|
if (fieldDef.type === "reference" || fieldDef.type === "enum") {
|
|
const options = getFieldOptions(fieldDef);
|
|
const found = options.find((option) => String(option.value) === String(clause.value));
|
|
return found ? found.label : String(clause.value ?? "");
|
|
}
|
|
return String(clause.value ?? "");
|
|
},
|
|
[getFieldDef, getFieldOptions]
|
|
);
|
|
const {
|
|
kanbanData,
|
|
kanbanLoading,
|
|
kanbanSortModal,
|
|
kanbanSortApplied,
|
|
loadKanban,
|
|
openKanbanSortModal,
|
|
closeKanbanSortModal,
|
|
updateKanbanSortMode,
|
|
submitKanbanSortModal,
|
|
resetKanbanState
|
|
} = useKanban({
|
|
api,
|
|
setStatus,
|
|
setTableState,
|
|
tablesRef
|
|
});
|
|
const { loadTable, loadPrevPage, loadNextPage, loadAllRows, toggleTableSort } = useTableActions({
|
|
api,
|
|
setStatus,
|
|
resolveTableConfig,
|
|
tablesRef,
|
|
setTableState,
|
|
setDictionaries,
|
|
buildUniversalQuery
|
|
});
|
|
const { loadAvailableTables, loadReferenceRows } = useAdminCatalogLoaders({
|
|
api,
|
|
setStatus,
|
|
setTableState,
|
|
setReferenceRowsMap,
|
|
buildUniversalQuery
|
|
});
|
|
const loadCurrentConfigTable = useCallback2(
|
|
async (resetOffset, tokenOverride, keyOverride) => {
|
|
const currentKey = keyOverride || configActiveKey;
|
|
if (!currentKey) {
|
|
return false;
|
|
}
|
|
return loadTable(currentKey, { resetOffset: Boolean(resetOffset) }, tokenOverride);
|
|
},
|
|
[configActiveKey, loadTable]
|
|
);
|
|
const loadStatusDesignerTopic = useCallback2(
|
|
async (topicCode) => {
|
|
const code = String(topicCode || "").trim();
|
|
setStatusDesignerTopicCode(code);
|
|
statusDesignerLoadedTopicRef.current = code;
|
|
if (!code) {
|
|
await loadTable("statusTransitions", { resetOffset: true, filtersOverride: [] });
|
|
return;
|
|
}
|
|
await loadTable("statusTransitions", {
|
|
resetOffset: true,
|
|
filtersOverride: [{ field: "topic_code", op: "=", value: code }]
|
|
});
|
|
},
|
|
[loadTable]
|
|
);
|
|
useEffect4(() => {
|
|
if (configActiveKey !== "statusTransitions") {
|
|
statusDesignerLoadedTopicRef.current = "";
|
|
return;
|
|
}
|
|
const topics = dictionaries.topics || [];
|
|
if (!topics.length) {
|
|
setStatusDesignerTopicCode("");
|
|
return;
|
|
}
|
|
const hasSelected = topics.some((item) => String(item?.code || "") === String(statusDesignerTopicCode || ""));
|
|
const nextTopic = String(hasSelected ? statusDesignerTopicCode : topics[0]?.code || "").trim();
|
|
if (!nextTopic) return;
|
|
if (nextTopic !== statusDesignerTopicCode) {
|
|
setStatusDesignerTopicCode(nextTopic);
|
|
return;
|
|
}
|
|
if (statusDesignerLoadedTopicRef.current === nextTopic) return;
|
|
statusDesignerLoadedTopicRef.current = nextTopic;
|
|
loadTable("statusTransitions", {
|
|
resetOffset: true,
|
|
filtersOverride: [{ field: "topic_code", op: "=", value: nextTopic }]
|
|
});
|
|
}, [configActiveKey, dictionaries.topics, loadTable, statusDesignerTopicCode]);
|
|
const loadDashboard = useCallback2(
|
|
async (tokenOverride) => {
|
|
const loadId = Date.now();
|
|
dashboardLoadRef.current = loadId;
|
|
setStatus("dashboard", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", "");
|
|
try {
|
|
const buildDashboardCards = (scope2, payload) => scope2 === "LAWYER" ? [
|
|
{ label: "\u041C\u043E\u0438 \u0437\u0430\u044F\u0432\u043A\u0438", value: payload.assigned_total ?? 0 },
|
|
{ label: "\u041C\u043E\u0438 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435", value: payload.active_assigned_total ?? 0 },
|
|
{ label: "\u041D\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0435", value: payload.unassigned_total ?? 0 },
|
|
{ label: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435", value: payload.my_unread_notifications_total ?? payload.my_unread_updates ?? 0 },
|
|
{ label: "\u041F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E SLA", value: payload.sla_overdue ?? 0 }
|
|
] : [
|
|
{ label: "\u041D\u043E\u0432\u044B\u0435", value: payload.new ?? 0 },
|
|
{ label: "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0435", value: payload.assigned_total ?? 0 },
|
|
{ label: "\u041D\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044B\u0435", value: payload.unassigned_total ?? 0 },
|
|
{ label: "\u041F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043E SLA", value: payload.sla_overdue ?? 0 },
|
|
{ label: "\u041C\u043E\u0438 \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435", value: payload.my_unread_notifications_total ?? payload.my_unread_updates ?? 0 },
|
|
{ label: "\u0412\u044B\u0440\u0443\u0447\u043A\u0430 (\u043C\u0435\u0441.)", value: Number(payload.month_revenue ?? 0).toFixed(2) },
|
|
{ label: "\u0420\u0430\u0441\u0445\u043E\u0434\u044B (\u043C\u0435\u0441.)", value: Number(payload.month_expenses ?? 0).toFixed(2) },
|
|
{ label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u044E\u0440\u0438\u0441\u0442\u0430\u043C\u0438", value: payload.unread_for_lawyers ?? 0 },
|
|
{ label: "\u041D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043E \u043A\u043B\u0438\u0435\u043D\u0442\u0430\u043C\u0438", value: payload.unread_for_clients ?? 0 }
|
|
];
|
|
const data = await api("/api/admin/metrics/overview?include_sla=false", {}, tokenOverride);
|
|
const scope = String(data.scope || role || "");
|
|
const localized = {};
|
|
Object.entries(data.by_status || {}).forEach(([code, count]) => {
|
|
localized[statusLabel(code)] = count;
|
|
});
|
|
setDashboardData({
|
|
scope,
|
|
cards: buildDashboardCards(scope, data),
|
|
byStatus: localized,
|
|
lawyerLoads: data.lawyer_loads || [],
|
|
myUnreadByEvent: data.my_unread_by_event || {},
|
|
myUnreadTotal: Number(data.my_unread_updates || 0),
|
|
myUnreadNotificationsTotal: Number(data.my_unread_notifications_total || data.my_unread_updates || 0),
|
|
unreadForClients: Number(data.unread_for_clients_notifications_total || data.unread_for_clients || 0),
|
|
unreadForLawyers: Number(data.unread_for_lawyers_notifications_total || data.unread_for_lawyers || 0),
|
|
serviceRequestUnreadTotal: Number(data.service_request_unread_total || 0),
|
|
deadlineAlertTotal: Number(data.deadline_alert_total || 0),
|
|
monthRevenue: Number(data.month_revenue || 0),
|
|
monthExpenses: Number(data.month_expenses || 0)
|
|
});
|
|
setStatus("dashboard", "\u0414\u0430\u043D\u043D\u044B\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u044B", "ok");
|
|
void (async () => {
|
|
try {
|
|
const slaData = await api("/api/admin/metrics/overview-sla", {}, tokenOverride);
|
|
if (dashboardLoadRef.current !== loadId) return;
|
|
setDashboardData((prev) => ({
|
|
...prev,
|
|
cards: buildDashboardCards(String(prev.scope || scope || ""), { ...data, ...slaData })
|
|
}));
|
|
} catch (_) {
|
|
}
|
|
})();
|
|
} catch (error) {
|
|
setStatus("dashboard", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, role, setStatus]
|
|
);
|
|
const loadMeta = useCallback2(
|
|
async (tokenOverride) => {
|
|
const entity = (metaEntity || "quotes").trim() || "quotes";
|
|
setStatus("meta", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...", "");
|
|
try {
|
|
const data = await api("/api/admin/meta/" + encodeURIComponent(entity), {}, tokenOverride);
|
|
setMetaJson(JSON.stringify(localizeMeta(data), null, 2));
|
|
setStatus("meta", "\u041C\u0435\u0442\u0430\u0434\u0430\u043D\u043D\u044B\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u044B", "ok");
|
|
} catch (error) {
|
|
setStatus("meta", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, metaEntity, setStatus]
|
|
);
|
|
const loadSmsProviderHealth = useCallback2(
|
|
async (tokenOverride, options) => {
|
|
const opts = options || {};
|
|
const silent = Boolean(opts.silent);
|
|
const currentRole = String(role || "").toUpperCase();
|
|
const authToken = tokenOverride !== void 0 ? tokenOverride : token;
|
|
if (!authToken || currentRole !== "ADMIN") {
|
|
setSmsProviderHealth(null);
|
|
return null;
|
|
}
|
|
if (!silent) setStatus("smsProviderHealth", "\u041E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u043C \u0431\u0430\u043B\u0430\u043D\u0441 SMS Aero...", "");
|
|
try {
|
|
const payload = await api("/api/admin/system/sms-provider-health", {}, tokenOverride);
|
|
const enriched = { ...payload || {}, loaded_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
setSmsProviderHealth(enriched);
|
|
if (!silent) setStatus("smsProviderHealth", "\u0411\u0430\u043B\u0430\u043D\u0441 SMS Aero \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok");
|
|
return enriched;
|
|
} catch (error) {
|
|
const fallback = {
|
|
provider: "smsaero",
|
|
status: "error",
|
|
mode: "real",
|
|
can_send: false,
|
|
balance_available: false,
|
|
balance_amount: null,
|
|
balance_currency: "RUB",
|
|
issues: [error.message],
|
|
loaded_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
};
|
|
setSmsProviderHealth(fallback);
|
|
if (!silent) setStatus("smsProviderHealth", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
return null;
|
|
}
|
|
},
|
|
[api, role, setStatus, token]
|
|
);
|
|
const refreshSection = useCallback2(
|
|
async (section, tokenOverride) => {
|
|
if (!(tokenOverride !== void 0 ? tokenOverride : token)) return;
|
|
if (section === "dashboard") return loadDashboard(tokenOverride);
|
|
if (section === "kanban") return loadKanban(tokenOverride);
|
|
if (section === "requests" && canAccessSection(role, "requests")) return loadTable("requests", {}, tokenOverride);
|
|
if (section === "serviceRequests" && canAccessSection(role, "serviceRequests")) return loadTable("serviceRequests", {}, tokenOverride);
|
|
if (section === "invoices" && canAccessSection(role, "invoices")) return loadTable("invoices", {}, tokenOverride);
|
|
if (section === "quotes" && canAccessSection(role, "quotes")) return loadTable("quotes", {}, tokenOverride);
|
|
if (section === "config" && canAccessSection(role, "config")) return loadCurrentConfigTable(false, tokenOverride);
|
|
if (section === "availableTables" && canAccessSection(role, "availableTables")) return loadAvailableTables(tokenOverride);
|
|
if (section === "meta") return loadMeta(tokenOverride);
|
|
},
|
|
[loadAvailableTables, loadCurrentConfigTable, loadDashboard, loadKanban, loadMeta, loadTable, role, token]
|
|
);
|
|
const bootstrapReferenceData = useCallback2(
|
|
async (tokenOverride, roleOverride) => {
|
|
setDictionaries((prev) => ({
|
|
...prev,
|
|
statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name }))
|
|
}));
|
|
if (roleOverride !== "ADMIN") return;
|
|
try {
|
|
const body = buildUniversalQuery([], [{ field: "sort_order", dir: "asc" }], 500, 0);
|
|
const usersBody = buildUniversalQuery([], [{ field: "created_at", dir: "desc" }], 500, 0);
|
|
const [catalogData, topicsData, statusesData, fieldsData, usersData] = await Promise.all([
|
|
api("/api/admin/crud/meta/tables", {}, tokenOverride),
|
|
api("/api/admin/crud/topics/query", { method: "POST", body }, tokenOverride),
|
|
api("/api/admin/crud/statuses/query", { method: "POST", body }, tokenOverride),
|
|
api("/api/admin/crud/form_fields/query", { method: "POST", body }, tokenOverride),
|
|
api("/api/admin/crud/admin_users/query", { method: "POST", body: usersBody }, tokenOverride)
|
|
]);
|
|
const catalogRows = (catalogData.tables || []).filter((row) => row && row.table).map((row) => {
|
|
const tableName = String(row.table || "");
|
|
const key = TABLE_KEY_ALIASES[tableName] || String(row.key || tableName);
|
|
return { ...row, key, table: tableName };
|
|
});
|
|
setTableCatalog(catalogRows);
|
|
await loadReferenceRows(catalogRows, tokenOverride);
|
|
const statusesMap = new Map(Object.entries(STATUS_LABELS).map(([code, name]) => [code, { code, name }]));
|
|
(statusesData.rows || []).forEach((row) => {
|
|
if (!row.code) return;
|
|
statusesMap.set(row.code, { code: row.code, name: row.name || statusLabel(row.code) });
|
|
});
|
|
const typeSet = new Set(DEFAULT_FORM_FIELD_TYPES);
|
|
(fieldsData.rows || []).forEach((row) => {
|
|
if (row?.type) typeSet.add(row.type);
|
|
});
|
|
const fieldKeys = (fieldsData.rows || []).filter((row) => row && row.key).map((row) => ({ key: row.key, label: row.label || row.key })).sort((a, b) => String(a.label || a.key).localeCompare(String(b.label || b.key), "ru"));
|
|
setDictionaries((prev) => ({
|
|
...prev,
|
|
topics: sortByName((topicsData.rows || []).map((row) => ({ code: row.code, name: row.name || row.code }))),
|
|
statuses: sortByName(Array.from(statusesMap.values())),
|
|
formFieldTypes: Array.from(typeSet.values()).sort((a, b) => String(a).localeCompare(String(b), "ru")),
|
|
formFieldKeys: fieldKeys,
|
|
users: (usersData.rows || []).map((row) => ({
|
|
id: row.id,
|
|
name: row.name || "",
|
|
email: row.email || "",
|
|
phone: row.phone || "",
|
|
role: row.role || "",
|
|
is_active: Boolean(row.is_active)
|
|
}))
|
|
}));
|
|
} catch (_) {
|
|
}
|
|
},
|
|
[api, loadReferenceRows]
|
|
);
|
|
const updateAvailableTableState = useCallback2(
|
|
async (tableName, isActive) => {
|
|
const name = String(tableName || "").trim();
|
|
if (!name) return;
|
|
try {
|
|
setStatus("availableTables", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", "");
|
|
await api("/api/admin/crud/meta/available-tables/" + encodeURIComponent(name), {
|
|
method: "PATCH",
|
|
body: { is_active: Boolean(isActive) }
|
|
});
|
|
await Promise.all([loadAvailableTables(), bootstrapReferenceData(token, role)]);
|
|
setStatus("availableTables", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E", "ok");
|
|
} catch (error) {
|
|
setStatus("availableTables", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, bootstrapReferenceData, loadAvailableTables, role, setStatus, token]
|
|
);
|
|
const openCreateRecordModal = useCallback2(
|
|
(tableKey) => {
|
|
const fields = getRecordFields(tableKey);
|
|
const initial = {};
|
|
fields.forEach((field) => {
|
|
if (field.defaultValue !== void 0) initial[field.key] = String(field.defaultValue);
|
|
else if (field.type === "boolean") initial[field.key] = "false";
|
|
else if (field.type === "json") initial[field.key] = field.optional ? "" : "{}";
|
|
else if ((field.type === "reference" || field.type === "enum") && !field.optional) {
|
|
const options = typeof field.options === "function" ? field.options() : [];
|
|
initial[field.key] = options.length ? String(options[0].value) : "";
|
|
} else initial[field.key] = "";
|
|
});
|
|
if (tableKey === "requests" && !initial.status_code) initial.status_code = "NEW";
|
|
if (tableKey === "invoices") {
|
|
const selectedTrack = String(initial.request_track_number || "").trim().toUpperCase();
|
|
if (selectedTrack) {
|
|
const rows = getInvoiceRequestRows();
|
|
const found = rows.find((row) => String(row?.track_number || "").trim().toUpperCase() === selectedTrack);
|
|
const autoPayer = String(found?.client_name || "").trim();
|
|
if (autoPayer) initial.payer_display_name = autoPayer;
|
|
}
|
|
}
|
|
setRecordModal({ open: true, tableKey, mode: "create", rowId: null, form: initial });
|
|
setStatus("recordForm", "", "");
|
|
},
|
|
[getInvoiceRequestRows, getRecordFields, setStatus]
|
|
);
|
|
const openCreateStatusTransitionForTopic = useCallback2(() => {
|
|
const topicCode = String(statusDesignerTopicCode || "").trim();
|
|
if (!topicCode) {
|
|
setStatus("statusTransitions", "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0435\u043C\u0443 \u0434\u043B\u044F \u043A\u043E\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u043E\u0440\u0430", "error");
|
|
return;
|
|
}
|
|
setRecordModal({
|
|
open: true,
|
|
tableKey: "statusTransitions",
|
|
mode: "create",
|
|
rowId: null,
|
|
form: {
|
|
topic_code: topicCode,
|
|
from_status: "",
|
|
to_status: "",
|
|
sla_hours: "",
|
|
required_data_keys: "[]",
|
|
required_mime_types: "[]",
|
|
enabled: "true",
|
|
sort_order: String(Math.max(1, (statusDesignerRows || []).length + 1))
|
|
}
|
|
});
|
|
setStatus("recordForm", "", "");
|
|
}, [setStatus, statusDesignerRows, statusDesignerTopicCode]);
|
|
const openEditRecordModal = useCallback2(
|
|
async (tableKey, row) => {
|
|
let sourceRow = row || {};
|
|
if (tableKey === "requests" && role === "ADMIN" && row?.id) {
|
|
try {
|
|
setStatus("requests", "\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043C \u043F\u043E\u043B\u043D\u0443\u044E \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0443 \u0437\u0430\u044F\u0432\u043A\u0438...", "");
|
|
const loaded = await api("/api/admin/requests/" + row.id);
|
|
sourceRow = { ...row || {}, ...loaded || {} };
|
|
setStatus("requests", "", "");
|
|
} catch (error) {
|
|
setStatus("requests", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0437\u0430\u044F\u0432\u043A\u0438: " + error.message, "error");
|
|
return;
|
|
}
|
|
}
|
|
const fields = getRecordFields(tableKey);
|
|
const nextForm = {};
|
|
fields.forEach((field) => {
|
|
const value = sourceRow[field.key];
|
|
if (field.type === "boolean") nextForm[field.key] = value ? "true" : "false";
|
|
else if (field.type === "json") nextForm[field.key] = value == null ? "" : JSON.stringify(value, null, 2);
|
|
else nextForm[field.key] = value == null ? "" : String(value);
|
|
});
|
|
if (tableKey === "requests" && role !== "LAWYER" && !String(nextForm.client_id || "").trim()) {
|
|
nextForm.client_id = NEW_REQUEST_CLIENT_OPTION;
|
|
}
|
|
setRecordModal({ open: true, tableKey, mode: "edit", rowId: sourceRow.id, form: nextForm });
|
|
setStatus("recordForm", "", "");
|
|
},
|
|
[api, getRecordFields, role, setStatus]
|
|
);
|
|
const closeRecordModal = useCallback2(() => {
|
|
setRecordModal({ open: false, tableKey: null, mode: "create", rowId: null, form: {} });
|
|
setStatus("recordForm", "", "");
|
|
}, [setStatus]);
|
|
const updateRecordField = useCallback2(
|
|
(field, value) => {
|
|
setRecordModal((prev) => {
|
|
const nextForm = { ...prev.form || {}, [field]: value };
|
|
if (prev.tableKey === "requests") {
|
|
if (field === "client_id") {
|
|
const selectedId = String(value || "").trim();
|
|
if (!selectedId || selectedId === NEW_REQUEST_CLIENT_OPTION) {
|
|
nextForm.client_id = NEW_REQUEST_CLIENT_OPTION;
|
|
nextForm.client_name = "";
|
|
nextForm.client_phone = "";
|
|
} else if (selectedId) {
|
|
const rows = Array.isArray(referenceRowsMap.clients) ? referenceRowsMap.clients : [];
|
|
const found = rows.find((row) => String(row?.id || "") === selectedId);
|
|
if (found) {
|
|
nextForm.client_name = String(found.full_name || nextForm.client_name || "");
|
|
nextForm.client_phone = String(found.phone || nextForm.client_phone || "");
|
|
}
|
|
}
|
|
}
|
|
if ((field === "client_name" || field === "client_phone") && String(nextForm.client_id || "").trim() && String(nextForm.client_id || "").trim() !== NEW_REQUEST_CLIENT_OPTION) {
|
|
const selectedId = String(nextForm.client_id || "").trim();
|
|
const rows = Array.isArray(referenceRowsMap.clients) ? referenceRowsMap.clients : [];
|
|
const found = rows.find((row) => String(row?.id || "") === selectedId);
|
|
if (found) {
|
|
const selectedName = String(found.full_name || "");
|
|
const selectedPhone = String(found.phone || "");
|
|
const currentName = String(field === "client_name" ? value : nextForm.client_name || "");
|
|
const currentPhone = String(field === "client_phone" ? value : nextForm.client_phone || "");
|
|
if (currentName !== selectedName || currentPhone !== selectedPhone) {
|
|
nextForm.client_id = "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (prev.tableKey === "invoices" && field === "request_track_number") {
|
|
const selectedTrack = String(value || "").trim().toUpperCase();
|
|
if (selectedTrack) {
|
|
const rows = getInvoiceRequestRows();
|
|
const found = rows.find((row) => String(row?.track_number || "").trim().toUpperCase() === selectedTrack);
|
|
if (found) {
|
|
nextForm.request_track_number = String(found.track_number || selectedTrack).trim().toUpperCase();
|
|
const autoPayer = String(found.client_name || "").trim();
|
|
if (autoPayer) nextForm.payer_display_name = autoPayer;
|
|
}
|
|
}
|
|
}
|
|
return { ...prev, form: nextForm };
|
|
});
|
|
},
|
|
[getInvoiceRequestRows, referenceRowsMap.clients]
|
|
);
|
|
const uploadRecordFieldFile = useCallback2(
|
|
async (field, file, cropJson) => {
|
|
if (!recordModal.tableKey || !field || !file) return;
|
|
if (field.uploadScope !== "USER_AVATAR") return;
|
|
if (recordModal.tableKey !== "users") return;
|
|
if (recordModal.mode !== "edit" || !recordModal.rowId) {
|
|
setStatus("recordForm", "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u0437\u0430\u0442\u0435\u043C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0430\u0432\u0430\u0442\u0430\u0440", "error");
|
|
return;
|
|
}
|
|
try {
|
|
setStatus("recordForm", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0444\u0430\u0439\u043B\u0430...", "");
|
|
const mimeType = String(file.type || "application/octet-stream");
|
|
const initPayload = {
|
|
file_name: file.name,
|
|
mime_type: mimeType,
|
|
size_bytes: file.size,
|
|
scope: "USER_AVATAR",
|
|
user_id: recordModal.rowId
|
|
};
|
|
const init = await api("/api/admin/uploads/init", { method: "POST", body: initPayload });
|
|
const putResp = await fetch(init.presigned_url, {
|
|
method: "PUT",
|
|
headers: { "Content-Type": mimeType },
|
|
body: file
|
|
});
|
|
if (!putResp.ok) {
|
|
const errorText = (await putResp.text()).trim();
|
|
throw new Error(
|
|
`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 (${putResp.status}${errorText ? `: ${errorText.slice(0, 200)}` : ""})`
|
|
);
|
|
}
|
|
const completeBody = {
|
|
key: init.key,
|
|
file_name: file.name,
|
|
mime_type: mimeType,
|
|
size_bytes: file.size,
|
|
scope: "USER_AVATAR",
|
|
user_id: recordModal.rowId
|
|
};
|
|
if (cropJson) completeBody.crop_json = JSON.stringify(cropJson);
|
|
const done = await api("/api/admin/uploads/complete", {
|
|
method: "POST",
|
|
body: completeBody
|
|
});
|
|
updateRecordField("avatar_url", String(done.avatar_url || ""));
|
|
if (done.avatar_original_key) {
|
|
updateRecordField("avatar_original_key", String(done.avatar_original_key));
|
|
}
|
|
setStatus("recordForm", "\u0410\u0432\u0430\u0442\u0430\u0440 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D", "ok");
|
|
} catch (error) {
|
|
setStatus("recordForm", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, recordModal, setStatus, updateRecordField]
|
|
);
|
|
const recropAvatar = useCallback2(
|
|
async (avatarField, form, setCropFileCallback) => {
|
|
if (!recordModal.rowId || !form) return;
|
|
if (!form.avatar_original_key) {
|
|
setStatus("recordForm", "\u041E\u0440\u0438\u0433\u0438\u043D\u0430\u043B \u0430\u0432\u0430\u0442\u0430\u0440\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D", "error");
|
|
return;
|
|
}
|
|
try {
|
|
setStatus("recordForm", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u0430...", "");
|
|
const src = `/api/admin/uploads/object/${encodeURIComponent(form.avatar_original_key)}?token=${encodeURIComponent(token)}`;
|
|
const resp = await fetch(src);
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
const blob = await resp.blob();
|
|
const file = new File([blob], "original.webp", { type: blob.type || "image/webp" });
|
|
setStatus("recordForm", "", "");
|
|
if (setCropFileCallback) setCropFileCallback(file);
|
|
} catch (err) {
|
|
setStatus("recordForm", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B: " + err.message, "error");
|
|
}
|
|
},
|
|
[recordModal.rowId, setStatus, token]
|
|
);
|
|
const applyRecrop = useCallback2(
|
|
async (cropJson) => {
|
|
if (!recordModal.rowId) return;
|
|
try {
|
|
setStatus("recordForm", "\u041F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u0438\u0435 \u043A\u0430\u0434\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F...", "");
|
|
const done = await api("/api/admin/uploads/recrop", {
|
|
method: "POST",
|
|
body: { user_id: recordModal.rowId, crop_json: JSON.stringify(cropJson) }
|
|
});
|
|
updateRecordField("avatar_url", String(done.avatar_url || "") + "?t=" + Date.now());
|
|
setStatus("recordForm", "\u041A\u0430\u0434\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E", "ok");
|
|
} catch (err) {
|
|
setStatus("recordForm", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043A\u0430\u0434\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F: " + err.message, "error");
|
|
}
|
|
},
|
|
[api, recordModal.rowId, setStatus, updateRecordField]
|
|
);
|
|
const buildRecordPayload = useCallback2(
|
|
(tableKey, form, mode) => {
|
|
const fields = getRecordFields(tableKey);
|
|
const payload = {};
|
|
const isLawyerRequestEdit = tableKey === "requests" && role === "LAWYER" && mode === "edit";
|
|
const isAdminRequestEdit = tableKey === "requests" && role === "ADMIN" && mode === "edit";
|
|
const adminRequestRestricted = /* @__PURE__ */ new Set(["client_id", "client_name", "client_phone"]);
|
|
fields.forEach((field) => {
|
|
if (isLawyerRequestEdit && field.key !== "topic_code") return;
|
|
if (isAdminRequestEdit && adminRequestRestricted.has(field.key)) return;
|
|
if (field.hidden) return;
|
|
const raw = form[field.key];
|
|
if (field.type === "boolean") {
|
|
payload[field.key] = raw === "true";
|
|
return;
|
|
}
|
|
if (field.type === "number") {
|
|
if (raw === "" || raw == null) {
|
|
if (!field.optional) payload[field.key] = 0;
|
|
return;
|
|
}
|
|
const number = Number(raw);
|
|
if (Number.isNaN(number)) throw new Error('\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u0432 \u043F\u043E\u043B\u0435 "' + field.label + '"');
|
|
payload[field.key] = number;
|
|
return;
|
|
}
|
|
if (field.type === "json") {
|
|
const text = String(raw || "").trim();
|
|
if (!text) {
|
|
if (field.omitIfEmpty) return;
|
|
if (field.optional) payload[field.key] = null;
|
|
else payload[field.key] = {};
|
|
return;
|
|
}
|
|
try {
|
|
payload[field.key] = JSON.parse(text);
|
|
} catch (_) {
|
|
throw new Error('\u041F\u043E\u043B\u0435 "' + field.label + '" \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C JSON');
|
|
}
|
|
return;
|
|
}
|
|
const value = String(raw || "").trim();
|
|
if (tableKey === "requests" && field.key === "client_id" && value === NEW_REQUEST_CLIENT_OPTION) {
|
|
payload[field.key] = null;
|
|
return;
|
|
}
|
|
if (!value) {
|
|
if (mode === "create" && field.autoCreate) return;
|
|
if (mode === "create" && field.requiredOnCreate) throw new Error('\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 "' + field.label + '"');
|
|
if (field.required) throw new Error('\u0417\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043F\u043E\u043B\u0435 "' + field.label + '"');
|
|
if (field.omitIfEmpty) return;
|
|
if (tableKey === "requests" && field.key === "track_number") return;
|
|
if (field.optional) payload[field.key] = null;
|
|
return;
|
|
}
|
|
payload[field.key] = value;
|
|
});
|
|
if (tableKey === "requests" && mode === "create" && !payload.extra_fields) payload.extra_fields = {};
|
|
if (tableKey === "invoices" && mode === "edit") delete payload.request_track_number;
|
|
return payload;
|
|
},
|
|
[getRecordFields, role]
|
|
);
|
|
const submitRecordModal = useCallback2(
|
|
async (event) => {
|
|
event.preventDefault();
|
|
const tableKey = recordModal.tableKey;
|
|
if (!tableKey) return;
|
|
const endpoints = resolveMutationConfig(tableKey);
|
|
if (!endpoints) return;
|
|
try {
|
|
setStatus("recordForm", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", "");
|
|
const payload = buildRecordPayload(tableKey, recordModal.form || {}, recordModal.mode);
|
|
if (recordModal.mode === "edit" && recordModal.rowId) {
|
|
await api(endpoints.update(recordModal.rowId), { method: "PATCH", body: payload });
|
|
} else {
|
|
await api(endpoints.create, { method: "POST", body: payload });
|
|
}
|
|
setStatus("recordForm", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E", "ok");
|
|
await loadTable(tableKey, { resetOffset: true });
|
|
await loadReferenceRows(tableCatalog, void 0);
|
|
setTimeout(() => closeRecordModal(), 250);
|
|
} catch (error) {
|
|
setStatus("recordForm", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, buildRecordPayload, closeRecordModal, loadReferenceRows, loadTable, recordModal, resolveMutationConfig, setStatus, tableCatalog]
|
|
);
|
|
const deleteRecord = useCallback2(
|
|
async (tableKey, id) => {
|
|
const endpoints = resolveMutationConfig(tableKey);
|
|
if (!endpoints) return;
|
|
if (!confirm("\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0437\u0430\u043F\u0438\u0441\u044C?")) return;
|
|
try {
|
|
await api(endpoints.delete(id), { method: "DELETE" });
|
|
setStatus(tableKey, "\u0417\u0430\u043F\u0438\u0441\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u0430", "ok");
|
|
await loadTable(tableKey, { resetOffset: true });
|
|
await loadReferenceRows(tableCatalog, void 0);
|
|
} catch (error) {
|
|
setStatus(tableKey, "\u041E\u0448\u0438\u0431\u043A\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, loadReferenceRows, loadTable, resolveMutationConfig, setStatus, tableCatalog]
|
|
);
|
|
const claimRequest = useCallback2(
|
|
async (requestId) => {
|
|
if (!requestId) return;
|
|
try {
|
|
setStatus("requests", "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043A\u0438...", "");
|
|
setStatus("kanban", "\u041D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u044F\u0432\u043A\u0438...", "");
|
|
await api("/api/admin/requests/" + requestId + "/claim", { method: "POST" });
|
|
setStatus("requests", "\u0417\u0430\u044F\u0432\u043A\u0430 \u0432\u0437\u044F\u0442\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", "ok");
|
|
setStatus("kanban", "\u0417\u0430\u044F\u0432\u043A\u0430 \u0432\u0437\u044F\u0442\u0430 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", "ok");
|
|
const refreshRequests = canAccessSection(role, "requests") ? loadTable("requests", { resetOffset: true }) : Promise.resolve();
|
|
await Promise.all([refreshRequests, loadKanban()]);
|
|
} catch (error) {
|
|
setStatus("requests", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: " + error.message, "error");
|
|
setStatus("kanban", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, loadKanban, loadTable, role, setStatus]
|
|
);
|
|
const openInvoiceRequest = useCallback2(
|
|
(row, event) => {
|
|
if (!row || !row.request_id) return;
|
|
openRequestDetails(row.request_id, event);
|
|
},
|
|
[openRequestDetails]
|
|
);
|
|
const moveRequestFromKanban = useCallback2(
|
|
async (row, targetGroup, explicitStatus) => {
|
|
const requestId = String(row?.id || "").trim();
|
|
if (!requestId) return;
|
|
const currentGroup = String(row?.status_group || fallbackStatusGroup(row?.status_code));
|
|
const groupKey = String(targetGroup || "").trim();
|
|
const targetStatusFromSelect = String(explicitStatus || "").trim();
|
|
const assignedLawyerId = String(row?.assigned_lawyer_id || "").trim();
|
|
if (role === "LAWYER" && !assignedLawyerId) {
|
|
setStatus("kanban", "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u0432\u043E\u0437\u044C\u043C\u0438\u0442\u0435 \u0437\u0430\u044F\u0432\u043A\u0443 \u0432 \u0440\u0430\u0431\u043E\u0442\u0443", "error");
|
|
return;
|
|
}
|
|
if (role === "LAWYER" && assignedLawyerId && String(assignedLawyerId) !== String(userId || "")) {
|
|
setStatus("kanban", "\u042E\u0440\u0438\u0441\u0442 \u043C\u043E\u0436\u0435\u0442 \u043C\u0435\u043D\u044F\u0442\u044C \u0441\u0442\u0430\u0442\u0443\u0441 \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u0432\u043E\u0438\u0445 \u0437\u0430\u044F\u0432\u043E\u043A", "error");
|
|
return;
|
|
}
|
|
let targetStatus = targetStatusFromSelect;
|
|
const transitions = Array.isArray(row?.available_transitions) ? row.available_transitions : [];
|
|
if (!targetStatus) {
|
|
if (!groupKey || groupKey === currentGroup) return;
|
|
const candidates = transitions.filter((item) => String(item?.target_group || "") === groupKey);
|
|
if (!candidates.length) {
|
|
setStatus("kanban", "\u0414\u043B\u044F \u044D\u0442\u043E\u0439 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0438 \u043D\u0435\u0442 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u0432 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u0443\u044E \u043A\u043E\u043B\u043E\u043D\u043A\u0443", "error");
|
|
return;
|
|
}
|
|
if (candidates.length > 1) {
|
|
await openRequestDetails(requestId, void 0, {
|
|
statusChangePreset: {
|
|
source: "kanban",
|
|
targetGroup: groupKey,
|
|
suggestedStatuses: candidates.map((item) => String(item?.to_status || "")).filter(Boolean)
|
|
}
|
|
});
|
|
setStatus("kanban", "\u041E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u043C\u043E\u0434\u0430\u043B\u044C\u043D\u043E\u0435 \u043E\u043A\u043D\u043E \u0441\u043C\u0435\u043D\u044B \u0441\u0442\u0430\u0442\u0443\u0441\u0430 \u0438 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u044B\u0439 \u0441\u0442\u0430\u0442\u0443\u0441", "ok");
|
|
return;
|
|
}
|
|
targetStatus = String(candidates[0]?.to_status || "").trim();
|
|
}
|
|
if (!targetStatus || targetStatus === String(row?.status_code || "")) return;
|
|
try {
|
|
setStatus("kanban", "\u041F\u0435\u0440\u0435\u0432\u043E\u0434\u0438\u043C \u0437\u0430\u044F\u0432\u043A\u0443...", "");
|
|
await submitRequestStatusChange({ requestId, statusCode: targetStatus });
|
|
setStatus("kanban", "\u0421\u0442\u0430\u0442\u0443\u0441 \u0437\u0430\u044F\u0432\u043A\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok");
|
|
const refreshRequests = canAccessSection(role, "requests") ? loadTable("requests", { resetOffset: true }) : Promise.resolve();
|
|
await Promise.all([loadKanban(), refreshRequests]);
|
|
} catch (error) {
|
|
setStatus("kanban", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[loadKanban, loadTable, openRequestDetails, role, setStatus, submitRequestStatusChange, userId]
|
|
);
|
|
const downloadInvoicePdf = useCallback2(
|
|
async (row, statusKey = "invoices") => {
|
|
if (!row || !row.id || !token) return;
|
|
try {
|
|
setStatus(statusKey, "\u0424\u043E\u0440\u043C\u0438\u0440\u0443\u0435\u043C PDF...", "");
|
|
const response = await fetch("/api/admin/invoices/" + row.id + "/pdf", {
|
|
headers: { Authorization: "Bearer " + token }
|
|
});
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
let payload = {};
|
|
try {
|
|
payload = text ? JSON.parse(text) : {};
|
|
} catch (_) {
|
|
payload = { raw: text };
|
|
}
|
|
const message = payload.detail || payload.error || payload.raw || "HTTP " + response.status;
|
|
throw new Error(translateApiError(String(message)));
|
|
}
|
|
const blob = await response.blob();
|
|
const fileName = (row.invoice_number || "invoice") + ".pdf";
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
link.remove();
|
|
URL.revokeObjectURL(url);
|
|
setStatus(statusKey, "PDF \u0441\u043A\u0430\u0447\u0430\u043D", "ok");
|
|
} catch (error) {
|
|
setStatus(statusKey, "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F: " + error.message, "error");
|
|
}
|
|
},
|
|
[setStatus, token]
|
|
);
|
|
const downloadRequestInvoicePdf = useCallback2(
|
|
async (row) => {
|
|
await downloadInvoicePdf(row, "requestModal");
|
|
},
|
|
[downloadInvoicePdf]
|
|
);
|
|
const resetAdminRoute = useCallback2(() => {
|
|
const nextUrl = "/admin.html";
|
|
if (window.location.pathname !== nextUrl || window.location.search) {
|
|
window.history.replaceState(null, "", nextUrl);
|
|
}
|
|
}, []);
|
|
const goBackFromRequestWorkspace = useCallback2(() => {
|
|
const targetSection = canAccessSection(role, "requests") ? "requests" : "kanban";
|
|
resetAdminRoute();
|
|
setActiveSection(targetSection);
|
|
refreshSection(targetSection);
|
|
}, [refreshSection, resetAdminRoute, role]);
|
|
const openReassignModal = useCallback2(
|
|
(row) => {
|
|
const options = getLawyerOptions();
|
|
if (!options.length) {
|
|
setStatus("reassignForm", "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u044E\u0440\u0438\u0441\u0442\u043E\u0432 \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F", "error");
|
|
return;
|
|
}
|
|
const current = String(row?.assigned_lawyer_id || "");
|
|
const hasCurrent = options.some((option) => String(option.value) === current);
|
|
const fallback = options[0] ? String(options[0].value) : "";
|
|
setReassignModal({
|
|
open: true,
|
|
requestId: row?.id || null,
|
|
trackNumber: row?.track_number || "",
|
|
lawyerId: hasCurrent ? current : fallback
|
|
});
|
|
setStatus("reassignForm", "", "");
|
|
},
|
|
[getLawyerOptions, setStatus]
|
|
);
|
|
const closeReassignModal = useCallback2(() => {
|
|
setReassignModal({ open: false, requestId: null, trackNumber: "", lawyerId: "" });
|
|
setStatus("reassignForm", "", "");
|
|
}, [setStatus]);
|
|
const updateReassignLawyer = useCallback2((event) => {
|
|
setReassignModal((prev) => ({ ...prev, lawyerId: event.target.value }));
|
|
}, []);
|
|
const submitReassignModal = useCallback2(
|
|
async (event) => {
|
|
event.preventDefault();
|
|
if (!reassignModal.requestId) return;
|
|
const lawyerId = String(reassignModal.lawyerId || "").trim();
|
|
if (!lawyerId) {
|
|
setStatus("reassignForm", "\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u044E\u0440\u0438\u0441\u0442\u0430", "error");
|
|
return;
|
|
}
|
|
try {
|
|
setStatus("reassignForm", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", "");
|
|
await api("/api/admin/requests/" + reassignModal.requestId + "/reassign", {
|
|
method: "POST",
|
|
body: { lawyer_id: lawyerId }
|
|
});
|
|
setStatus("requests", "\u0417\u0430\u044F\u0432\u043A\u0430 \u043F\u0435\u0440\u0435\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0430", "ok");
|
|
closeReassignModal();
|
|
await loadTable("requests", { resetOffset: true });
|
|
} catch (error) {
|
|
setStatus("reassignForm", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, closeReassignModal, loadTable, reassignModal.lawyerId, reassignModal.requestId, setStatus]
|
|
);
|
|
const defaultFilterValue = useCallback2(
|
|
(fieldDef) => {
|
|
if (!fieldDef) return "";
|
|
if (fieldDef.type === "boolean") return "true";
|
|
if (fieldDef.type === "reference" || fieldDef.type === "enum") {
|
|
const options = getFieldOptions(fieldDef);
|
|
return options.length ? String(options[0].value) : "";
|
|
}
|
|
return "";
|
|
},
|
|
[getFieldOptions]
|
|
);
|
|
const openFilterModal = useCallback2(
|
|
(tableKey) => {
|
|
const fields = getFilterFields(tableKey);
|
|
if (!fields.length) {
|
|
setStatus("filter", "\u0414\u043B\u044F \u0442\u0430\u0431\u043B\u0438\u0446\u044B \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u043E\u043B\u0435\u0439 \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u0438", "error");
|
|
return;
|
|
}
|
|
const firstField = fields[0];
|
|
const firstOp = getOperatorsForType(firstField.type)[0] || "=";
|
|
setFilterModal({
|
|
open: true,
|
|
tableKey,
|
|
field: firstField.field,
|
|
op: firstOp,
|
|
rawValue: defaultFilterValue(firstField),
|
|
editIndex: null
|
|
});
|
|
setStatus("filter", "", "");
|
|
},
|
|
[defaultFilterValue, getFilterFields, setStatus]
|
|
);
|
|
const openFilterEditModal = useCallback2(
|
|
(tableKey, index) => {
|
|
const tableState = tablesRef.current[tableKey] || createTableState();
|
|
const target = (tableState.filters || [])[index];
|
|
if (!target) return;
|
|
const fieldDef = getFieldDef(tableKey, target.field);
|
|
if (!fieldDef) return;
|
|
const allowedOps = getOperatorsForType(fieldDef.type);
|
|
const safeOp = allowedOps.includes(target.op) ? target.op : allowedOps[0] || "=";
|
|
const rawValue = fieldDef.type === "boolean" ? target.value ? "true" : "false" : String(target.value ?? "");
|
|
setFilterModal({
|
|
open: true,
|
|
tableKey,
|
|
field: fieldDef.field,
|
|
op: safeOp,
|
|
rawValue,
|
|
editIndex: index
|
|
});
|
|
setStatus("filter", "", "");
|
|
},
|
|
[getFieldDef, setStatus]
|
|
);
|
|
const closeFilterModal = useCallback2(() => {
|
|
setFilterModal((prev) => ({ ...prev, open: false, editIndex: null }));
|
|
setStatus("filter", "", "");
|
|
}, [setStatus]);
|
|
const updateFilterField = useCallback2(
|
|
(event) => {
|
|
const fieldName = event.target.value;
|
|
const fields = getFilterFields(filterModal.tableKey);
|
|
const fieldDef = fields.find((field) => field.field === fieldName) || null;
|
|
if (!fieldDef) return;
|
|
const defaultOp = getOperatorsForType(fieldDef.type)[0] || "=";
|
|
setFilterModal((prev) => ({
|
|
...prev,
|
|
field: fieldName,
|
|
op: defaultOp,
|
|
rawValue: defaultFilterValue(fieldDef)
|
|
}));
|
|
},
|
|
[defaultFilterValue, filterModal.tableKey, getFilterFields]
|
|
);
|
|
const updateFilterOp = useCallback2((event) => {
|
|
const op = event.target.value;
|
|
setFilterModal((prev) => ({ ...prev, op }));
|
|
}, []);
|
|
const updateFilterValue = useCallback2((event) => {
|
|
setFilterModal((prev) => ({ ...prev, rawValue: event.target.value }));
|
|
}, []);
|
|
const { applyFilterModal, clearFiltersFromModal, removeFilterChip } = useTableFilterActions({
|
|
filterModal,
|
|
closeFilterModal,
|
|
getFieldDef,
|
|
loadKanban,
|
|
loadTable,
|
|
setStatus,
|
|
setTableState,
|
|
tablesRef
|
|
});
|
|
const selectConfigNode = useCallback2(
|
|
(tableKey) => {
|
|
resetAdminRoute();
|
|
setConfigActiveKey(tableKey);
|
|
setActiveSection("config");
|
|
loadCurrentConfigTable(false, void 0, tableKey);
|
|
},
|
|
[loadCurrentConfigTable, resetAdminRoute]
|
|
);
|
|
const activateSection = useCallback2(
|
|
(section) => {
|
|
const nextSection = canAccessSection(role, section) ? section : "dashboard";
|
|
resetAdminRoute();
|
|
setActiveSection(nextSection);
|
|
refreshSection(nextSection);
|
|
},
|
|
[refreshSection, resetAdminRoute, role]
|
|
);
|
|
const applyRequestsQuickFilterPreset = useCallback2(
|
|
async (filters, statusMessage) => {
|
|
if (!canAccessSection(role, "requests")) return;
|
|
const nextFilters = Array.isArray(filters) ? filters.filter((item) => item && item.field) : [];
|
|
resetAdminRoute();
|
|
setActiveSection("requests");
|
|
const currentState = tablesRef.current.requests || createTableState();
|
|
setTableState("requests", {
|
|
...currentState,
|
|
filters: nextFilters,
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
if (statusMessage) setStatus("requests", statusMessage, "");
|
|
await loadTable("requests", { resetOffset: true, filtersOverride: nextFilters });
|
|
},
|
|
[loadTable, resetAdminRoute, role, setStatus, setTableState, tablesRef]
|
|
);
|
|
const applyKanbanQuickFilterPreset = useCallback2(
|
|
async (filters, statusMessage) => {
|
|
const nextFilters = Array.isArray(filters) ? filters.filter((item) => item && item.field) : [];
|
|
resetAdminRoute();
|
|
setActiveSection("kanban");
|
|
const currentState = tablesRef.current.kanban || createTableState();
|
|
setTableState("kanban", {
|
|
...currentState,
|
|
filters: nextFilters,
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
if (statusMessage) setStatus("kanban", statusMessage, "");
|
|
await loadKanban(void 0, { filtersOverride: nextFilters });
|
|
},
|
|
[loadKanban, resetAdminRoute, setStatus, setTableState, tablesRef]
|
|
);
|
|
const openRequestsWithUnreadAlerts = useCallback2(async () => {
|
|
await applyRequestsQuickFilterPreset([{ field: "has_unread_updates", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u043D\u043E\u0432\u044B\u043C\u0438 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F\u043C\u0438");
|
|
}, [applyRequestsQuickFilterPreset]);
|
|
const openRequestsWithDeadlineAlerts = useCallback2(async () => {
|
|
await applyRequestsQuickFilterPreset([{ field: "deadline_alert", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u0433\u043E\u0440\u044F\u0449\u0438\u043C\u0438 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u0430\u043C\u0438");
|
|
}, [applyRequestsQuickFilterPreset]);
|
|
const openKanbanWithUnreadAlerts = useCallback2(async () => {
|
|
await applyKanbanQuickFilterPreset([{ field: "has_unread_updates", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u043D\u043E\u0432\u044B\u043C\u0438 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F\u043C\u0438");
|
|
}, [applyKanbanQuickFilterPreset]);
|
|
const openKanbanWithDeadlineAlerts = useCallback2(async () => {
|
|
await applyKanbanQuickFilterPreset([{ field: "deadline_alert", op: "=", value: true }], "\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u0433\u043E\u0440\u044F\u0449\u0438\u043C\u0438 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u0430\u043C\u0438");
|
|
}, [applyKanbanQuickFilterPreset]);
|
|
const applyServiceRequestsQuickFilterPreset = useCallback2(
|
|
async (filters, statusMessage) => {
|
|
const nextFilters = Array.isArray(filters) ? filters.filter((item) => item && item.field) : [];
|
|
resetAdminRoute();
|
|
setActiveSection("serviceRequests");
|
|
const currentState = tablesRef.current.serviceRequests || createTableState();
|
|
setTableState("serviceRequests", {
|
|
...currentState,
|
|
filters: nextFilters,
|
|
offset: 0,
|
|
showAll: false
|
|
});
|
|
if (statusMessage) setStatus("serviceRequests", statusMessage, "");
|
|
await loadTable("serviceRequests", { resetOffset: true, filtersOverride: nextFilters });
|
|
},
|
|
[loadTable, resetAdminRoute, setStatus, setTableState, tablesRef]
|
|
);
|
|
const openServiceRequestsWithUnreadAlerts = useCallback2(async () => {
|
|
if (String(role || "").toUpperCase() === "LAWYER") {
|
|
await applyServiceRequestsQuickFilterPreset(
|
|
[{ field: "lawyer_unread", op: "=", value: true }],
|
|
"\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430"
|
|
);
|
|
return;
|
|
}
|
|
await applyServiceRequestsQuickFilterPreset(
|
|
[{ field: "admin_unread", op: "=", value: true }],
|
|
"\u041F\u043E\u043A\u0430\u0437\u0430\u043D\u044B \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430"
|
|
);
|
|
}, [applyServiceRequestsQuickFilterPreset, role]);
|
|
const markServiceRequestRead = useCallback2(
|
|
async (serviceRequestId) => {
|
|
const rowId = String(serviceRequestId || "").trim();
|
|
if (!rowId) return;
|
|
try {
|
|
setStatus("serviceRequests", "\u041E\u0442\u043C\u0435\u0447\u0430\u0435\u043C \u043A\u0430\u043A \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0439...", "");
|
|
await api("/api/admin/requests/service-requests/" + encodeURIComponent(rowId) + "/read", { method: "POST" });
|
|
await Promise.all([loadTable("serviceRequests", { resetOffset: true }), loadDashboard()]);
|
|
if (canAccessSection(role, "requests")) await loadTable("requests", { resetOffset: true });
|
|
setStatus("serviceRequests", "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u0447\u0435\u043D \u043A\u0430\u043A \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0439", "ok");
|
|
} catch (error) {
|
|
setStatus("serviceRequests", "\u041E\u0448\u0438\u0431\u043A\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, loadDashboard, loadTable, role, setStatus]
|
|
);
|
|
const loadTotpStatus = useCallback2(
|
|
async (tokenOverride) => {
|
|
const activeToken = tokenOverride !== void 0 ? tokenOverride : token;
|
|
if (!activeToken) return;
|
|
try {
|
|
const data = await api("/api/admin/auth/totp/status", { method: "GET" }, activeToken);
|
|
if (data && typeof data === "object") {
|
|
setTotpStatus({
|
|
mode: String(data.mode || "password_totp_optional"),
|
|
enabled: Boolean(data.enabled),
|
|
required: Boolean(data.required),
|
|
has_backup_codes: Boolean(data.has_backup_codes)
|
|
});
|
|
}
|
|
} catch (_) {
|
|
}
|
|
},
|
|
[api, token]
|
|
);
|
|
const openAccountModal = useCallback2(async () => {
|
|
if (!token || !userId) {
|
|
setStatus("account", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043F\u0440\u043E\u0444\u0438\u043B\u044C: \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F", "error");
|
|
return;
|
|
}
|
|
setAccountModal((prev) => ({
|
|
...prev,
|
|
open: true,
|
|
loading: true,
|
|
saving: false
|
|
}));
|
|
setStatus("account", "\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043F\u0440\u043E\u0444\u0438\u043B\u044F...", "");
|
|
try {
|
|
const row = await api("/api/admin/crud/admin_users/" + encodeURIComponent(String(userId)));
|
|
const nextInitial = {
|
|
name: String(row?.name || ""),
|
|
email: String(row?.email || email || ""),
|
|
phone: String(row?.phone || "")
|
|
};
|
|
setAccountModal({
|
|
open: true,
|
|
loading: false,
|
|
saving: false,
|
|
initial: nextInitial,
|
|
form: {
|
|
...nextInitial,
|
|
password: "",
|
|
passwordConfirm: ""
|
|
}
|
|
});
|
|
setStatus("account", "", "");
|
|
} catch (error) {
|
|
setAccountModal((prev) => ({ ...prev, loading: false }));
|
|
setStatus("account", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u043F\u0440\u043E\u0444\u0438\u043B\u044F: " + error.message, "error");
|
|
}
|
|
}, [api, email, setStatus, token, userId]);
|
|
const closeAccountModal = useCallback2(() => {
|
|
setAccountModal((prev) => ({
|
|
...prev,
|
|
open: false,
|
|
loading: false,
|
|
saving: false,
|
|
form: {
|
|
name: prev.initial.name,
|
|
email: prev.initial.email,
|
|
phone: prev.initial.phone,
|
|
password: "",
|
|
passwordConfirm: ""
|
|
}
|
|
}));
|
|
setStatus("account", "", "");
|
|
}, [setStatus]);
|
|
const updateAccountField = useCallback2((event) => {
|
|
const fieldName = String(event?.target?.name || "");
|
|
if (!fieldName) return;
|
|
setAccountModal((prev) => ({
|
|
...prev,
|
|
form: {
|
|
...prev.form,
|
|
[fieldName]: event.target.value
|
|
}
|
|
}));
|
|
}, []);
|
|
const submitAccountModal = useCallback2(
|
|
async (event) => {
|
|
event.preventDefault();
|
|
if (!token || !userId) return;
|
|
const form = accountModal.form || {};
|
|
const initial = accountModal.initial || {};
|
|
const nextName = String(form.name || "").trim();
|
|
const nextEmail = String(form.email || "").trim().toLowerCase();
|
|
const nextPhone = String(form.phone || "").trim();
|
|
const nextPassword = String(form.password || "");
|
|
const nextPasswordConfirm = String(form.passwordConfirm || "");
|
|
if (!nextName) {
|
|
setStatus("account", "\u0418\u043C\u044F \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u044B\u043C", "error");
|
|
return;
|
|
}
|
|
if (!nextEmail) {
|
|
setStatus("account", "\u041F\u043E\u0447\u0442\u0430 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043F\u0443\u0441\u0442\u043E\u0439", "error");
|
|
return;
|
|
}
|
|
if (nextPassword && nextPassword.length < 8) {
|
|
setStatus("account", "\u041F\u0430\u0440\u043E\u043B\u044C \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435 \u043C\u0435\u043D\u0435\u0435 8 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432", "error");
|
|
return;
|
|
}
|
|
if (nextPassword !== nextPasswordConfirm) {
|
|
setStatus("account", "\u041F\u0430\u0440\u043E\u043B\u0438 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442", "error");
|
|
return;
|
|
}
|
|
const payload = {};
|
|
if (nextName !== String(initial.name || "").trim()) payload.name = nextName;
|
|
if (nextEmail !== String(initial.email || "").trim().toLowerCase()) payload.email = nextEmail;
|
|
if (nextPhone !== String(initial.phone || "").trim()) payload.phone = nextPhone || null;
|
|
if (nextPassword) payload.password = nextPassword;
|
|
if (!Object.keys(payload).length) {
|
|
setStatus("account", "\u041D\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u0434\u043B\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F", "");
|
|
return;
|
|
}
|
|
try {
|
|
setAccountModal((prev) => ({ ...prev, saving: true }));
|
|
setStatus("account", "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u0435...", "");
|
|
const row = await api("/api/admin/crud/admin_users/" + encodeURIComponent(String(userId)), {
|
|
method: "PATCH",
|
|
body: payload
|
|
});
|
|
const nextInitial = {
|
|
name: String(row?.name || nextName),
|
|
email: String(row?.email || nextEmail),
|
|
phone: String(row?.phone || nextPhone)
|
|
};
|
|
setAccountModal((prev) => ({
|
|
...prev,
|
|
saving: false,
|
|
initial: nextInitial,
|
|
form: {
|
|
...nextInitial,
|
|
password: "",
|
|
passwordConfirm: ""
|
|
}
|
|
}));
|
|
if (nextInitial.email) setEmail(nextInitial.email);
|
|
setStatus("account", "\u041F\u0440\u043E\u0444\u0438\u043B\u044C \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D", "ok");
|
|
} catch (error) {
|
|
setAccountModal((prev) => ({ ...prev, saving: false }));
|
|
setStatus("account", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F: " + error.message, "error");
|
|
}
|
|
},
|
|
[accountModal.form, accountModal.initial, api, setStatus, token, userId]
|
|
);
|
|
const closeTotpSetupModal = useCallback2(() => {
|
|
setTotpSetupModal({
|
|
open: false,
|
|
secret: "",
|
|
uri: "",
|
|
qrDataUrl: "",
|
|
code: "",
|
|
loading: false
|
|
});
|
|
setStatus("totpSetup", "", "");
|
|
}, [setStatus]);
|
|
const updateTotpSetupCode = useCallback2((event) => {
|
|
setTotpSetupModal((prev) => ({ ...prev, code: event.target.value }));
|
|
}, []);
|
|
const copyTotpSecret = useCallback2(async () => {
|
|
const value = String(totpSetupModal.secret || "").trim();
|
|
if (!value) return;
|
|
try {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
await navigator.clipboard.writeText(value);
|
|
setStatus("totpSetup", "\u041A\u043B\u044E\u0447 \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430", "ok");
|
|
} else {
|
|
setStatus("totpSetup", "\u0411\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0432 \u044D\u0442\u043E\u043C \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435", "error");
|
|
}
|
|
} catch (_) {
|
|
setStatus("totpSetup", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043B\u044E\u0447", "error");
|
|
}
|
|
}, [setStatus, totpSetupModal.secret]);
|
|
const copyTotpUri = useCallback2(async () => {
|
|
const value = String(totpSetupModal.uri || "").trim();
|
|
if (!value) return;
|
|
try {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
await navigator.clipboard.writeText(value);
|
|
setStatus("totpSetup", "URI \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430", "ok");
|
|
} else {
|
|
setStatus("totpSetup", "\u0411\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0432 \u044D\u0442\u043E\u043C \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435", "error");
|
|
}
|
|
} catch (_) {
|
|
setStatus("totpSetup", "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C URI", "error");
|
|
}
|
|
}, [setStatus, totpSetupModal.uri]);
|
|
const setupTotp = useCallback2(async () => {
|
|
try {
|
|
const setup = await api("/api/admin/auth/totp/setup", { method: "POST", body: {} });
|
|
const secret = String(setup?.secret || "").trim();
|
|
const uri = String(setup?.otpauth_uri || "").trim();
|
|
if (!secret || !uri) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0441\u0435\u043A\u0440\u0435\u0442 TOTP");
|
|
let qrDataUrl = "";
|
|
try {
|
|
qrDataUrl = await import_qrcode.default.toDataURL(uri, {
|
|
margin: 1,
|
|
width: 240,
|
|
errorCorrectionLevel: "M"
|
|
});
|
|
} catch (_) {
|
|
qrDataUrl = "";
|
|
}
|
|
setTotpSetupModal({
|
|
open: true,
|
|
secret,
|
|
uri,
|
|
qrDataUrl,
|
|
code: "",
|
|
loading: false
|
|
});
|
|
setStatus("totpSetup", "", "");
|
|
} catch (error) {
|
|
setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 2FA: " + error.message, "error");
|
|
}
|
|
}, [api, setStatus]);
|
|
const submitTotpSetup = useCallback2(
|
|
async (event) => {
|
|
event.preventDefault();
|
|
const secret = String(totpSetupModal.secret || "").trim();
|
|
const rawCode = String(totpSetupModal.code || "").trim();
|
|
const digitsOnly = rawCode.replace(/\D+/g, "");
|
|
if (!secret) {
|
|
setStatus("totpSetup", "\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D TOTP secret. \u041F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443.", "error");
|
|
return;
|
|
}
|
|
if (digitsOnly.length !== 6) {
|
|
setStatus("totpSetup", "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u044B\u0439 6-\u0437\u043D\u0430\u0447\u043D\u044B\u0439 \u043A\u043E\u0434", "error");
|
|
return;
|
|
}
|
|
try {
|
|
setTotpSetupModal((prev) => ({ ...prev, loading: true }));
|
|
const enabled = await api("/api/admin/auth/totp/enable", { method: "POST", body: { secret, code: digitsOnly } });
|
|
closeTotpSetupModal();
|
|
setStatus("login", "2FA \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430", "ok");
|
|
const backupCodes = Array.isArray(enabled?.backup_codes) ? enabled.backup_codes : [];
|
|
window.alert(
|
|
"2FA \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430.\n\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0435 \u043A\u043E\u0434\u044B (\u043E\u0434\u043D\u043E\u043A\u0440\u0430\u0442\u043D\u043E):\n\n" + (backupCodes.length ? backupCodes.join("\n") : "-")
|
|
);
|
|
await loadTotpStatus();
|
|
} catch (error) {
|
|
setTotpSetupModal((prev) => ({ ...prev, loading: false }));
|
|
setStatus("totpSetup", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F 2FA: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, closeTotpSetupModal, loadTotpStatus, setStatus, totpSetupModal.code, totpSetupModal.secret]
|
|
);
|
|
const regenerateTotpBackupCodes = useCallback2(async () => {
|
|
try {
|
|
const code = String(window.prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 TOTP \u043A\u043E\u0434 (\u0438\u043B\u0438 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0439 \u043A\u043E\u0434) \u0434\u043B\u044F \u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438", "") || "").trim();
|
|
if (!code) return;
|
|
const payload = /^\d{6}$/.test(code) ? { code } : { backup_code: code };
|
|
const data = await api("/api/admin/auth/totp/backup/regenerate", { method: "POST", body: payload });
|
|
const backupCodes = Array.isArray(data?.backup_codes) ? data.backup_codes : [];
|
|
window.alert("\u041D\u043E\u0432\u044B\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0435 \u043A\u043E\u0434\u044B:\n\n" + (backupCodes.length ? backupCodes.join("\n") : "-"));
|
|
await loadTotpStatus();
|
|
} catch (error) {
|
|
setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 backup-\u043A\u043E\u0434\u043E\u0432: " + error.message, "error");
|
|
}
|
|
}, [api, loadTotpStatus, setStatus]);
|
|
const disableTotp = useCallback2(async () => {
|
|
try {
|
|
const code = String(window.prompt("\u0412\u0432\u0435\u0434\u0438\u0442\u0435 TOTP \u043A\u043E\u0434 (\u0438\u043B\u0438 \u0440\u0435\u0437\u0435\u0440\u0432\u043D\u044B\u0439 \u043A\u043E\u0434) \u0434\u043B\u044F \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F 2FA", "") || "").trim();
|
|
if (!code) return;
|
|
const payload = /^\d{6}$/.test(code) ? { code } : { backup_code: code };
|
|
await api("/api/admin/auth/totp/disable", { method: "POST", body: payload });
|
|
setStatus("login", "2FA \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430", "ok");
|
|
await loadTotpStatus();
|
|
} catch (error) {
|
|
setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F 2FA: " + error.message, "error");
|
|
}
|
|
}, [api, loadTotpStatus, setStatus]);
|
|
const logout = useCallback2(() => {
|
|
localStorage.removeItem(LS_TOKEN);
|
|
setToken("");
|
|
setRole("");
|
|
setEmail("");
|
|
setUserId("");
|
|
setRecordModal({ open: false, tableKey: null, mode: "create", rowId: null, form: {} });
|
|
resetRequestWorkspaceState();
|
|
setFilterModal({ open: false, tableKey: null, field: "", op: "=", rawValue: "", editIndex: null });
|
|
resetKanbanState();
|
|
setReassignModal({ open: false, requestId: null, trackNumber: "", lawyerId: "" });
|
|
setDashboardData({
|
|
scope: "",
|
|
cards: [],
|
|
byStatus: {},
|
|
lawyerLoads: [],
|
|
myUnreadByEvent: {},
|
|
myUnreadTotal: 0,
|
|
myUnreadNotificationsTotal: 0,
|
|
unreadForClients: 0,
|
|
unreadForLawyers: 0,
|
|
serviceRequestUnreadTotal: 0,
|
|
deadlineAlertTotal: 0,
|
|
monthRevenue: 0,
|
|
monthExpenses: 0
|
|
});
|
|
setMetaJson("");
|
|
setConfigActiveKey("");
|
|
setReferencesExpanded(true);
|
|
resetTablesState();
|
|
setDictionaries({
|
|
topics: [],
|
|
statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })),
|
|
formFieldTypes: [...DEFAULT_FORM_FIELD_TYPES],
|
|
formFieldKeys: [],
|
|
users: []
|
|
});
|
|
setStatusMap({});
|
|
setSmsProviderHealth(null);
|
|
setTotpStatus({
|
|
mode: "password_totp_optional",
|
|
enabled: false,
|
|
required: false,
|
|
has_backup_codes: false
|
|
});
|
|
setTotpSetupModal({
|
|
open: false,
|
|
secret: "",
|
|
uri: "",
|
|
qrDataUrl: "",
|
|
code: "",
|
|
loading: false
|
|
});
|
|
setAccountModal({
|
|
open: false,
|
|
loading: false,
|
|
saving: false,
|
|
initial: { name: "", email: "", phone: "" },
|
|
form: { name: "", email: "", phone: "", password: "", passwordConfirm: "" }
|
|
});
|
|
setActiveSection("dashboard");
|
|
}, [resetKanbanState, resetRequestWorkspaceState, resetTablesState]);
|
|
const login = useCallback2(
|
|
async (emailInput, passwordInput, totpCodeInput) => {
|
|
try {
|
|
setStatus("login", "\u0412\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u043C \u0432\u0445\u043E\u0434...", "");
|
|
const rawTotp = String(totpCodeInput || "").trim();
|
|
const digitsOnly = rawTotp.replace(/\D+/g, "");
|
|
const loginBody = {
|
|
email: String(emailInput || "").trim(),
|
|
password: passwordInput || "",
|
|
...rawTotp ? digitsOnly.length === 6 ? { totp_code: digitsOnly } : { backup_code: rawTotp } : {}
|
|
};
|
|
const data = await api(
|
|
"/api/admin/auth/login",
|
|
{
|
|
method: "POST",
|
|
auth: false,
|
|
body: loginBody
|
|
},
|
|
""
|
|
);
|
|
const nextToken = data.access_token;
|
|
const payload = decodeJwtPayload(nextToken || "");
|
|
if (!payload || !payload.role || !payload.email) throw new Error("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u0442\u043E\u043A\u0435\u043D\u0430");
|
|
sessionStorage.removeItem(ADMIN_AUTH_REDIRECT_REASON_KEY);
|
|
localStorage.setItem(LS_TOKEN, nextToken);
|
|
setToken(nextToken);
|
|
setRole(payload.role);
|
|
setEmail(payload.email);
|
|
setUserId(String(payload.sub || ""));
|
|
setActiveSection("dashboard");
|
|
setStatus("login", "\u0423\u0441\u043F\u0435\u0448\u043D\u044B\u0439 \u0432\u0445\u043E\u0434", "ok");
|
|
} catch (error) {
|
|
setStatus("login", "\u041E\u0448\u0438\u0431\u043A\u0430 \u0432\u0445\u043E\u0434\u0430: " + error.message, "error");
|
|
}
|
|
},
|
|
[api, setStatus]
|
|
);
|
|
useEffect4(() => {
|
|
const authRedirectReason = sessionStorage.getItem(ADMIN_AUTH_REDIRECT_REASON_KEY) || "";
|
|
if (authRedirectReason === "expired") {
|
|
setStatus("login", "\u0421\u0435\u0441\u0441\u0438\u044F \u0438\u0441\u0442\u0435\u043A\u043B\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", "error");
|
|
sessionStorage.removeItem(ADMIN_AUTH_REDIRECT_REASON_KEY);
|
|
}
|
|
const saved = localStorage.getItem(LS_TOKEN) || "";
|
|
if (!saved) return;
|
|
if (isAdminTokenExpired(saved)) {
|
|
localStorage.removeItem(LS_TOKEN);
|
|
setStatus("login", "\u0421\u0435\u0441\u0441\u0438\u044F \u0438\u0441\u0442\u0435\u043A\u043B\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", "error");
|
|
return;
|
|
}
|
|
const payload = decodeJwtPayload(saved);
|
|
if (!payload || !payload.role || !payload.email) {
|
|
localStorage.removeItem(LS_TOKEN);
|
|
setStatus("login", "\u0421\u0435\u0441\u0441\u0438\u044F \u0438\u0441\u0442\u0435\u043A\u043B\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0441\u043D\u043E\u0432\u0430.", "error");
|
|
return;
|
|
}
|
|
setToken(saved);
|
|
setRole(payload.role);
|
|
setEmail(payload.email);
|
|
setUserId(String(payload.sub || ""));
|
|
}, [isAdminTokenExpired, setStatus]);
|
|
useEffect4(() => {
|
|
if (!token || !role) return;
|
|
let cancelled = false;
|
|
let deferredBootstrapCleanup = null;
|
|
const scheduleDeferredBootstrap = () => {
|
|
if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
|
|
const handle2 = window.requestIdleCallback(() => {
|
|
if (!cancelled) bootstrapReferenceData(token, role);
|
|
}, { timeout: 1500 });
|
|
return () => {
|
|
if (typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(handle2);
|
|
};
|
|
}
|
|
const handle = window.setTimeout(() => {
|
|
if (!cancelled) bootstrapReferenceData(token, role);
|
|
}, 250);
|
|
return () => window.clearTimeout(handle);
|
|
};
|
|
(async () => {
|
|
if (!isRequestWorkspaceRoute && !routeInfo.section) {
|
|
if (!cancelled) await loadDashboard(token);
|
|
if (!cancelled) await loadTotpStatus(token);
|
|
if (!cancelled) deferredBootstrapCleanup = scheduleDeferredBootstrap();
|
|
return;
|
|
}
|
|
bootstrapReferenceData(token, role);
|
|
if (!cancelled) await loadTotpStatus(token);
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
if (typeof deferredBootstrapCleanup === "function") deferredBootstrapCleanup();
|
|
};
|
|
}, [bootstrapReferenceData, isRequestWorkspaceRoute, loadDashboard, loadTotpStatus, role, routeInfo.section, token]);
|
|
useEffect4(() => {
|
|
if (!token || !role) return;
|
|
if (initialRouteHandledRef.current) return;
|
|
initialRouteHandledRef.current = true;
|
|
if (isRequestWorkspaceRoute && routeInfo.requestId) {
|
|
setActiveSection("requestWorkspace");
|
|
loadRequestModalData(routeInfo.requestId, { showLoading: true });
|
|
resetAdminRoute();
|
|
return;
|
|
}
|
|
if (routeInfo.section) {
|
|
if (canAccessSection(role, routeInfo.section)) {
|
|
setActiveSection(routeInfo.section);
|
|
refreshSection(routeInfo.section, token);
|
|
resetAdminRoute();
|
|
} else {
|
|
setActiveSection("dashboard");
|
|
refreshSection("dashboard", token);
|
|
resetAdminRoute();
|
|
}
|
|
}
|
|
}, [isRequestWorkspaceRoute, loadRequestModalData, refreshSection, resetAdminRoute, role, routeInfo.requestId, routeInfo.section, token]);
|
|
useEffect4(() => {
|
|
if (!token) {
|
|
setSmsProviderHealth(null);
|
|
return;
|
|
}
|
|
if (String(role || "").toUpperCase() !== "ADMIN") {
|
|
setSmsProviderHealth(null);
|
|
return;
|
|
}
|
|
if (activeSection !== "config" || configActiveKey !== "otp_sessions") return;
|
|
loadSmsProviderHealth(void 0, { silent: true });
|
|
}, [activeSection, configActiveKey, loadSmsProviderHealth, role, token]);
|
|
useEffect4(() => {
|
|
if (!dictionaryTableItems.length) {
|
|
if (configActiveKey) setConfigActiveKey("");
|
|
return;
|
|
}
|
|
const hasCurrent = dictionaryTableItems.some((item) => item.key === configActiveKey);
|
|
if (!hasCurrent) setConfigActiveKey(dictionaryTableItems[0].key);
|
|
}, [configActiveKey, dictionaryTableItems]);
|
|
useEffect4(() => {
|
|
try {
|
|
window.localStorage.setItem("law-admin-sidebar-collapsed", sidebarCollapsed ? "1" : "0");
|
|
} catch (_) {
|
|
}
|
|
}, [sidebarCollapsed]);
|
|
const updateMenuTreeScrollbar = useCallback2(() => {
|
|
const node = menuTreeRef.current;
|
|
if (!node) {
|
|
setMenuTreeScrollbar({ visible: false, top: 0, height: 0 });
|
|
return;
|
|
}
|
|
const viewport = node.clientHeight;
|
|
const full = node.scrollHeight;
|
|
if (!viewport || full <= viewport + 1) {
|
|
setMenuTreeScrollbar({ visible: false, top: 0, height: 0 });
|
|
return;
|
|
}
|
|
const trackHeight = viewport;
|
|
const thumbHeight = Math.max(42, Math.round(viewport / full * trackHeight));
|
|
const maxScroll = Math.max(1, full - viewport);
|
|
const maxThumbOffset = Math.max(0, trackHeight - thumbHeight);
|
|
const thumbTop = Math.round(node.scrollTop / maxScroll * maxThumbOffset);
|
|
setMenuTreeScrollbar({ visible: true, top: thumbTop, height: thumbHeight });
|
|
}, []);
|
|
useEffect4(() => {
|
|
if (!referencesExpanded || sidebarCollapsed) {
|
|
setMenuTreeScrollbar({ visible: false, top: 0, height: 0 });
|
|
return void 0;
|
|
}
|
|
const node = menuTreeRef.current;
|
|
if (!node) return void 0;
|
|
updateMenuTreeScrollbar();
|
|
const handleScroll = () => updateMenuTreeScrollbar();
|
|
node.addEventListener("scroll", handleScroll, { passive: true });
|
|
let observer = null;
|
|
if (typeof ResizeObserver !== "undefined") {
|
|
observer = new ResizeObserver(() => updateMenuTreeScrollbar());
|
|
observer.observe(node);
|
|
}
|
|
window.addEventListener("resize", updateMenuTreeScrollbar);
|
|
return () => {
|
|
node.removeEventListener("scroll", handleScroll);
|
|
if (observer) observer.disconnect();
|
|
window.removeEventListener("resize", updateMenuTreeScrollbar);
|
|
};
|
|
}, [referencesExpanded, sidebarCollapsed, dictionaryTableItems.length, updateMenuTreeScrollbar]);
|
|
useEffect4(() => {
|
|
const handlePointerMove = (event) => {
|
|
const drag = menuTreeDragRef.current;
|
|
const node = menuTreeRef.current;
|
|
if (!drag || !node) return;
|
|
event.preventDefault();
|
|
const delta = event.clientY - drag.startClientY;
|
|
const nextThumbTop = Math.min(drag.maxThumbTop, Math.max(0, drag.startThumbTop + delta));
|
|
const ratio = drag.maxThumbTop > 0 ? nextThumbTop / drag.maxThumbTop : 0;
|
|
node.scrollTop = ratio * drag.maxScrollTop;
|
|
};
|
|
const stopDrag = () => {
|
|
if (!menuTreeDragRef.current) return;
|
|
menuTreeDragRef.current = null;
|
|
document.body.classList.remove("menu-tree-scrollbar-dragging");
|
|
};
|
|
window.addEventListener("pointermove", handlePointerMove);
|
|
window.addEventListener("pointerup", stopDrag);
|
|
window.addEventListener("pointercancel", stopDrag);
|
|
return () => {
|
|
window.removeEventListener("pointermove", handlePointerMove);
|
|
window.removeEventListener("pointerup", stopDrag);
|
|
window.removeEventListener("pointercancel", stopDrag);
|
|
};
|
|
}, []);
|
|
const startMenuTreeScrollbarDrag = useCallback2((event) => {
|
|
const node = menuTreeRef.current;
|
|
if (!node) return;
|
|
const maxScrollTop = Math.max(0, node.scrollHeight - node.clientHeight);
|
|
const maxThumbTop = Math.max(0, node.clientHeight - menuTreeScrollbar.height);
|
|
if (!maxScrollTop || !maxThumbTop) return;
|
|
menuTreeDragRef.current = {
|
|
startClientY: event.clientY,
|
|
startThumbTop: menuTreeScrollbar.top,
|
|
maxThumbTop,
|
|
maxScrollTop
|
|
};
|
|
document.body.classList.add("menu-tree-scrollbar-dragging");
|
|
event.preventDefault();
|
|
}, [menuTreeScrollbar.height, menuTreeScrollbar.top]);
|
|
const anyOverlayOpen = recordModal.open || filterModal.open || reassignModal.open || kanbanSortModal.open || totpSetupModal.open || accountModal.open;
|
|
useEffect4(() => {
|
|
document.body.classList.toggle("modal-open", anyOverlayOpen);
|
|
return () => document.body.classList.remove("modal-open");
|
|
}, [anyOverlayOpen]);
|
|
useEffect4(() => {
|
|
const onEsc = (event) => {
|
|
if (event.key !== "Escape") return;
|
|
setRecordModal((prev) => ({ ...prev, open: false }));
|
|
setFilterModal((prev) => ({ ...prev, open: false }));
|
|
closeKanbanSortModal();
|
|
setReassignModal((prev) => ({ ...prev, open: false }));
|
|
closeTotpSetupModal();
|
|
closeAccountModal();
|
|
};
|
|
document.addEventListener("keydown", onEsc);
|
|
return () => document.removeEventListener("keydown", onEsc);
|
|
}, [closeAccountModal, closeKanbanSortModal, closeTotpSetupModal]);
|
|
useEffect4(() => {
|
|
const root2 = document.getElementById("admin-root");
|
|
if (!root2) return void 0;
|
|
const applyInputHints = () => {
|
|
root2.querySelectorAll("input, textarea, select").forEach((node) => {
|
|
const tagName = String(node.tagName || "").toLowerCase();
|
|
const inputType = tagName === "input" ? String(node.getAttribute("type") || "text").toLowerCase() : "";
|
|
node.setAttribute("autocomplete", inputType === "password" ? "new-password" : "off");
|
|
node.setAttribute("autocorrect", "off");
|
|
node.setAttribute("autocapitalize", "off");
|
|
node.setAttribute("spellcheck", "false");
|
|
node.setAttribute("data-form-type", "other");
|
|
});
|
|
};
|
|
applyInputHints();
|
|
const observer = new MutationObserver(() => applyInputHints());
|
|
observer.observe(root2, { childList: true, subtree: true });
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
const menuItems = useMemo2(() => {
|
|
const baseItems = [
|
|
{ key: "dashboard", label: "\u041E\u0431\u0437\u043E\u0440", icon: "dashboard" },
|
|
{ key: "kanban", label: "\u041A\u0430\u043D\u0431\u0430\u043D", icon: "kanban" },
|
|
{ key: "requests", label: "\u0417\u0430\u044F\u0432\u043A\u0438", icon: "requests" },
|
|
{ key: "serviceRequests", label: "\u0417\u0430\u043F\u0440\u043E\u0441\u044B", icon: "serviceRequests" },
|
|
{ key: "invoices", label: "\u0421\u0447\u0435\u0442\u0430", icon: "invoices" }
|
|
];
|
|
return baseItems.filter((item) => canAccessSection(role, item.key));
|
|
}, [role]);
|
|
const topbarUnreadCount = useMemo2(() => {
|
|
const roleCode = String(role || "").toUpperCase();
|
|
if (roleCode === "LAWYER" || roleCode === "ADMIN" || roleCode === "CURATOR") {
|
|
return Number(dashboardData.myUnreadNotificationsTotal || dashboardData.myUnreadTotal || 0);
|
|
}
|
|
return Number(dashboardData.unreadForClients || 0) + Number(dashboardData.unreadForLawyers || 0);
|
|
}, [dashboardData.myUnreadNotificationsTotal, dashboardData.myUnreadTotal, dashboardData.unreadForClients, dashboardData.unreadForLawyers, role]);
|
|
const topbarDeadlineAlertCount = useMemo2(() => Number(dashboardData.deadlineAlertTotal || 0), [dashboardData.deadlineAlertTotal]);
|
|
const topbarServiceRequestUnreadCount = useMemo2(
|
|
() => Number(dashboardData.serviceRequestUnreadTotal || 0),
|
|
[dashboardData.serviceRequestUnreadTotal]
|
|
);
|
|
const topbarRoleCode = String(role || "").toUpperCase();
|
|
const canUseRequestsAlerts = topbarRoleCode === "ADMIN";
|
|
const canUseKanbanAlerts = topbarRoleCode === "LAWYER";
|
|
const showRequestAlertIcons = canUseRequestsAlerts || canUseKanbanAlerts;
|
|
const showServiceRequestIcon = canAccessSection(role, "serviceRequests");
|
|
const activeFilterFields = useMemo2(() => {
|
|
if (!filterModal.tableKey) return [];
|
|
return getFilterFields(filterModal.tableKey);
|
|
}, [filterModal.tableKey, getFilterFields]);
|
|
const filterTableLabel = useMemo2(() => getTableLabel(filterModal.tableKey), [filterModal.tableKey, getTableLabel]);
|
|
const recordModalFields = useMemo2(() => {
|
|
const all = getRecordFields(recordModal.tableKey);
|
|
const isEdit = recordModal.mode !== "create";
|
|
const roleCode = String(role || "").toUpperCase();
|
|
const visible = isEdit ? all.filter((field) => !field.createOnly) : all.filter((field) => !field.autoCreate);
|
|
return visible.map((field) => {
|
|
const nextField = { ...field };
|
|
if (recordModal.tableKey === "requests" && isEdit) {
|
|
if (roleCode === "LAWYER" && field.key !== "topic_code") nextField.readOnly = true;
|
|
if (roleCode === "ADMIN" && (field.key === "client_id" || field.key === "client_name" || field.key === "client_phone")) {
|
|
nextField.readOnly = true;
|
|
}
|
|
}
|
|
if (recordModal.tableKey === "serviceRequests" && isEdit && (field.key === "request_id" || field.key === "client_id" || field.key === "assigned_lawyer_id")) {
|
|
nextField.readOnly = true;
|
|
}
|
|
return nextField;
|
|
});
|
|
}, [getRecordFields, recordModal.mode, recordModal.tableKey, role]);
|
|
const activeConfigTableState = useMemo2(() => {
|
|
return tables[configActiveKey] || createTableState();
|
|
}, [configActiveKey, tables]);
|
|
const activeConfigMeta = useMemo2(() => tableCatalogMap[configActiveKey] || null, [configActiveKey, tableCatalogMap]);
|
|
const activeConfigActions = useMemo2(() => {
|
|
return Array.isArray(activeConfigMeta?.actions) ? activeConfigMeta.actions : [];
|
|
}, [activeConfigMeta]);
|
|
const canCreateInConfig = activeConfigActions.includes("create");
|
|
const canUpdateInConfig = activeConfigActions.includes("update");
|
|
const canDeleteInConfig = activeConfigActions.includes("delete");
|
|
const genericConfigHeaders = useMemo2(() => {
|
|
if (!activeConfigMeta || !Array.isArray(activeConfigMeta.columns)) return [];
|
|
const headers = (activeConfigMeta.columns || []).filter((column) => column && column.name && String(column.name) !== "id").map((column) => {
|
|
const name = String(column.name);
|
|
return {
|
|
key: name,
|
|
label: String(column.label || humanizeKey(name)),
|
|
sortable: Boolean(column.sortable !== false),
|
|
field: name
|
|
};
|
|
});
|
|
if (canUpdateInConfig || canDeleteInConfig) headers.push({ key: "actions", label: "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F" });
|
|
return headers;
|
|
}, [activeConfigMeta, canDeleteInConfig, canUpdateInConfig]);
|
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("div", { className: "layout" + (sidebarCollapsed ? " sidebar-collapsed" : "") }, /* @__PURE__ */ React.createElement("aside", { className: "sidebar" }, /* @__PURE__ */ React.createElement("div", { className: "sidebar-head" }, /* @__PURE__ */ React.createElement("div", { className: "logo" }, /* @__PURE__ */ React.createElement("a", { href: "/" }, /* @__PURE__ */ React.createElement("img", { className: "brand-mark", src: "/brand-mark.svg", alt: "", width: "24", height: "24" }), /* @__PURE__ */ React.createElement("span", null, "\u041F\u0440\u0430\u0432\u043E\u0432\u043E\u0439 \u0442\u0440\u0435\u043A\u0435\u0440"))), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "icon-btn",
|
|
type: "button",
|
|
"data-tooltip": sidebarCollapsed ? "\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043C\u0435\u043D\u044E" : "\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043C\u0435\u043D\u044E",
|
|
"aria-label": sidebarCollapsed ? "\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043C\u0435\u043D\u044E" : "\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u043C\u0435\u043D\u044E",
|
|
onClick: () => setSidebarCollapsed((prev) => !prev)
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "16", height: "16", "aria-hidden": "true", focusable: "false" }, sidebarCollapsed ? /* @__PURE__ */ React.createElement("path", { d: "M9.53 4.47a1 1 0 0 1 1.41 0l6.82 6.82a1 1 0 0 1 0 1.42l-6.82 6.82a1 1 0 1 1-1.41-1.42L15.64 12 9.53 5.89a1 1 0 0 1 0-1.42Zm-4 0a1 1 0 0 1 1.41 0l6.82 6.82a1 1 0 0 1 0 1.42l-6.82 6.82a1 1 0 0 1-1.41-1.42L11.64 12 5.53 5.89a1 1 0 0 1 0-1.42Z", fill: "currentColor" }) : /* @__PURE__ */ React.createElement("path", { d: "M14.47 4.47a1 1 0 0 1 0 1.42L8.36 12l6.11 6.11a1 1 0 0 1-1.41 1.42l-6.82-6.82a1 1 0 0 1 0-1.42l6.82-6.82a1 1 0 0 1 1.41 0Zm4 0a1 1 0 0 1 0 1.42L12.36 12l6.11 6.11a1 1 0 0 1-1.41 1.42l-6.82-6.82a1 1 0 0 1 0-1.42l6.82-6.82a1 1 0 0 1 1.41 0Z", fill: "currentColor" }))
|
|
)), /* @__PURE__ */ React.createElement("nav", { className: "menu" }, menuItems.map((item) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
key: item.key,
|
|
className: activeSection === item.key ? "active" : "",
|
|
"data-section": item.key,
|
|
type: "button",
|
|
onClick: () => activateSection(item.key),
|
|
title: sidebarCollapsed ? item.label : void 0,
|
|
"aria-label": item.label
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "menu-button-content" }, /* @__PURE__ */ React.createElement("span", { className: "menu-icon" }, /* @__PURE__ */ React.createElement(SidebarNavIcon, { name: item.icon })), /* @__PURE__ */ React.createElement("span", { className: "menu-label" }, item.label))
|
|
)), role === "ADMIN" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: activeSection === "config" ? "active" : "",
|
|
type: "button",
|
|
onClick: () => {
|
|
if (sidebarCollapsed) {
|
|
setSidebarCollapsed(false);
|
|
setReferencesExpanded(true);
|
|
} else {
|
|
setReferencesExpanded((prev) => !prev);
|
|
}
|
|
activateSection("config");
|
|
},
|
|
title: sidebarCollapsed ? "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0438" : void 0,
|
|
"aria-label": "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0438"
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "menu-button-content" }, /* @__PURE__ */ React.createElement("span", { className: "menu-icon" }, /* @__PURE__ */ React.createElement(SidebarNavIcon, { name: "config" })), /* @__PURE__ */ React.createElement("span", { className: "menu-label" }, "\u0421\u043F\u0440\u0430\u0432\u043E\u0447\u043D\u0438\u043A\u0438"), /* @__PURE__ */ React.createElement("span", { className: "menu-caret", "aria-hidden": "true" }, referencesExpanded ? "\u25BE" : "\u25B8"))
|
|
), referencesExpanded && !sidebarCollapsed ? /* @__PURE__ */ React.createElement("div", { className: "menu-tree-shell" }, /* @__PURE__ */ React.createElement("div", { className: "menu-tree", ref: menuTreeRef }, dictionaryTableItems.map((item) => /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
key: item.key,
|
|
type: "button",
|
|
className: activeSection === "config" && configActiveKey === item.key ? "active" : "",
|
|
onClick: () => selectConfigNode(item.key)
|
|
},
|
|
getTableLabel(item.key)
|
|
))), menuTreeScrollbar.visible ? /* @__PURE__ */ React.createElement("div", { className: "menu-tree-scrollbar", "aria-hidden": "true" }, /* @__PURE__ */ React.createElement(
|
|
"div",
|
|
{
|
|
className: "menu-tree-scrollbar-thumb",
|
|
onPointerDown: startMenuTreeScrollbarDrag,
|
|
style: {
|
|
height: menuTreeScrollbar.height + "px",
|
|
transform: "translateY(" + menuTreeScrollbar.top + "px)"
|
|
}
|
|
}
|
|
)) : null) : null) : null)), /* @__PURE__ */ React.createElement("main", { className: "main" }, /* @__PURE__ */ React.createElement("div", { className: "topbar" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h1", null, "\u041F\u0430\u043D\u0435\u043B\u044C \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0430"), /* @__PURE__ */ React.createElement("p", { className: "muted" }, "UniversalQuery, RBAC \u0438 \u0430\u0443\u0434\u0438\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043F\u043E \u043A\u043B\u044E\u0447\u0435\u0432\u044B\u043C \u0441\u0443\u0449\u043D\u043E\u0441\u0442\u044F\u043C \u0441\u0438\u0441\u0442\u0435\u043C\u044B.")), /* @__PURE__ */ React.createElement("div", { className: "topbar-actions", "aria-label": "\u0411\u044B\u0441\u0442\u0440\u044B\u0435 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u044F \u0438 \u043F\u0440\u043E\u0444\u0438\u043B\u044C" }, showServiceRequestIcon ? /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn topbar-alert-btn" + (topbarServiceRequestUnreadCount > 0 ? " has-alert alert-danger" : ""),
|
|
"data-tooltip": topbarServiceRequestUnreadCount > 0 ? "\u041D\u043E\u0432\u044B\u0435 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B: " + String(topbarServiceRequestUnreadCount) : "\u041D\u043E\u0432\u044B\u0445 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0445 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 \u043D\u0435\u0442",
|
|
"aria-label": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0435 \u0437\u0430\u043F\u0440\u043E\u0441\u044B \u043A\u043B\u0438\u0435\u043D\u0442\u0430",
|
|
onClick: openServiceRequestsWithUnreadAlerts
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M4.5 4.5h15a1.5 1.5 0 0 1 1.5 1.5v9.8a1.5 1.5 0 0 1-1.5 1.5H9.1l-3.7 3.1c-.98.82-2.4.13-2.4-1.14V6a1.5 1.5 0 0 1 1.5-1.5zm1.7 4.2a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2zm5.8 0a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2zm5.8 0a1.1 1.1 0 1 0 0 2.2 1.1 1.1 0 0 0 0-2.2z",
|
|
fill: "currentColor"
|
|
}
|
|
)),
|
|
/* @__PURE__ */ React.createElement("span", { className: "topbar-alert-dot", "aria-hidden": "true" })
|
|
) : null, showRequestAlertIcons ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn topbar-alert-btn" + (topbarDeadlineAlertCount > 0 ? " has-alert alert-danger" : ""),
|
|
"data-tooltip": topbarDeadlineAlertCount > 0 ? "\u0413\u043E\u0440\u044F\u0449\u0438\u0435 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u044B: " + String(topbarDeadlineAlertCount) : "\u0413\u043E\u0440\u044F\u0449\u0438\u0445 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u043E\u0432 \u043D\u0435\u0442",
|
|
"aria-label": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u0433\u043E\u0440\u044F\u0449\u0438\u043C\u0438 \u0434\u0435\u0434\u043B\u0430\u0439\u043D\u0430\u043C\u0438",
|
|
onClick: canUseRequestsAlerts ? openRequestsWithDeadlineAlerts : openKanbanWithDeadlineAlerts
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M12 3a1.6 1.6 0 0 1 1.42.86l7.14 13.7A1.6 1.6 0 0 1 19.14 20H4.86a1.6 1.6 0 0 1-1.42-2.44l7.14-13.7A1.6 1.6 0 0 1 12 3zm0 4.2a1 1 0 0 0-1 1v5.2a1 1 0 1 0 2 0V8.2a1 1 0 0 0-1-1zm0 9.4a1.15 1.15 0 1 0 0 2.3 1.15 1.15 0 0 0 0-2.3z",
|
|
fill: "currentColor"
|
|
}
|
|
)),
|
|
/* @__PURE__ */ React.createElement("span", { className: "topbar-alert-dot", "aria-hidden": "true" })
|
|
), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn topbar-alert-btn" + (topbarUnreadCount > 0 ? " has-alert alert-success" : ""),
|
|
"data-tooltip": topbarUnreadCount > 0 ? "\u041D\u043E\u0432\u044B\u0435 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F \u043F\u043E \u0437\u0430\u044F\u0432\u043A\u0430\u043C: " + String(topbarUnreadCount) : "\u041D\u043E\u0432\u044B\u0445 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u0439 \u043D\u0435\u0442",
|
|
"aria-label": "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438 \u0441 \u043D\u043E\u0432\u044B\u043C\u0438 \u043E\u043F\u043E\u0432\u0435\u0449\u0435\u043D\u0438\u044F\u043C\u0438",
|
|
onClick: canUseRequestsAlerts ? openRequestsWithUnreadAlerts : openKanbanWithUnreadAlerts
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5v-11zm2 .5v.32l6 4.44 6-4.44V7a.5.5 0 0 0-.5-.5h-11A.5.5 0 0 0 6 7zm12 2.8-5.4 4a1 1 0 0 1-1.2 0L6 9.8v7.7c0 .28.22.5.5.5h11a.5.5 0 0 0 .5-.5V9.8z",
|
|
fill: "currentColor"
|
|
}
|
|
)),
|
|
/* @__PURE__ */ React.createElement("span", { className: "topbar-alert-dot", "aria-hidden": "true" })
|
|
)) : null, /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "icon-btn topbar-alert-btn",
|
|
"data-tooltip": "\u041B\u0438\u0447\u043D\u044B\u0439 \u043A\u0430\u0431\u0438\u043D\u0435\u0442",
|
|
"aria-label": "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u043B\u0438\u0447\u043D\u044B\u0439 \u043A\u0430\u0431\u0438\u043D\u0435\u0442",
|
|
onClick: openAccountModal
|
|
},
|
|
/* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", width: "17", height: "17", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement(
|
|
"path",
|
|
{
|
|
d: "M12 12.2a4.1 4.1 0 1 0-4.1-4.1 4.1 4.1 0 0 0 4.1 4.1zm0 2c-3.8 0-7 2.2-7.8 5.3-.1.4.2.8.6.8h14.4c.4 0 .7-.4.6-.8-.8-3.1-4-5.3-7.8-5.3z",
|
|
fill: "currentColor"
|
|
}
|
|
))
|
|
))), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "dashboard", id: "section-dashboard" }, /* @__PURE__ */ React.createElement(
|
|
DashboardSection,
|
|
{
|
|
dashboardData,
|
|
token,
|
|
status: getStatus("dashboard"),
|
|
apiCall: api,
|
|
onOpenRequest: openRequestDetails,
|
|
DataTableComponent: DataTable,
|
|
StatusLineComponent: StatusLine,
|
|
UserAvatarComponent: UserAvatar
|
|
}
|
|
)), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "kanban", id: "section-kanban" }, /* @__PURE__ */ React.createElement(
|
|
KanbanBoard,
|
|
{
|
|
loading: kanbanLoading,
|
|
columns: kanbanData.columns,
|
|
rows: kanbanData.rows,
|
|
role,
|
|
actorId: userId,
|
|
onRefresh: () => loadKanban(),
|
|
filters: tables.kanban.filters,
|
|
onOpenFilter: () => openFilterModal("kanban"),
|
|
onRemoveFilter: (index) => removeFilterChip("kanban", index),
|
|
onEditFilter: (index) => openFilterEditModal("kanban", index),
|
|
getFilterChipLabel: (clause) => {
|
|
const fieldDef = getFieldDef("kanban", clause.field);
|
|
return (fieldDef ? fieldDef.label : clause.field) + " " + OPERATOR_LABELS[clause.op] + " " + getFilterValuePreview("kanban", clause);
|
|
},
|
|
onOpenSort: openKanbanSortModal,
|
|
sortActive: kanbanSortApplied,
|
|
onOpenRequest: openRequestDetails,
|
|
onClaimRequest: claimRequest,
|
|
onMoveRequest: moveRequestFromKanban,
|
|
status: getStatus("kanban"),
|
|
FilterToolbarComponent: FilterToolbar,
|
|
StatusLineComponent: StatusLine
|
|
}
|
|
)), canAccessSection(role, "requests") ? /* @__PURE__ */ React.createElement(Section, { active: activeSection === "requests", id: "section-requests" }, /* @__PURE__ */ React.createElement(
|
|
RequestsSection,
|
|
{
|
|
role,
|
|
tables,
|
|
status: getStatus("requests"),
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
resolveReferenceLabel,
|
|
onRefresh: () => loadTable("requests", { resetOffset: true }),
|
|
onCreate: () => openCreateRecordModal("requests"),
|
|
onOpenFilter: () => openFilterModal("requests"),
|
|
onRemoveFilter: (index) => removeFilterChip("requests", index),
|
|
onEditFilter: (index) => openFilterEditModal("requests", index),
|
|
onSort: (field) => toggleTableSort("requests", field),
|
|
onPrev: () => loadPrevPage("requests"),
|
|
onNext: () => loadNextPage("requests"),
|
|
onLoadAll: () => loadAllRows("requests"),
|
|
onClaimRequest: claimRequest,
|
|
onOpenReassign: openReassignModal,
|
|
onOpenRequest: openRequestDetails,
|
|
onEditRecord: (row) => openEditRecordModal("requests", row),
|
|
onDeleteRecord: (id) => deleteRecord("requests", id),
|
|
FilterToolbarComponent: FilterToolbar,
|
|
DataTableComponent: DataTable,
|
|
TablePagerComponent: TablePager,
|
|
StatusLineComponent: StatusLine,
|
|
IconButtonComponent: IconButton
|
|
}
|
|
)) : null, /* @__PURE__ */ React.createElement(Section, { active: activeSection === "serviceRequests", id: "section-service-requests" }, /* @__PURE__ */ React.createElement(
|
|
ServiceRequestsSection,
|
|
{
|
|
role,
|
|
tables,
|
|
status: getStatus("serviceRequests"),
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
resolveReferenceLabel,
|
|
onRefresh: () => loadTable("serviceRequests", { resetOffset: true }),
|
|
onCreate: () => openCreateRecordModal("serviceRequests"),
|
|
onOpenFilter: () => openFilterModal("serviceRequests"),
|
|
onRemoveFilter: (index) => removeFilterChip("serviceRequests", index),
|
|
onEditFilter: (index) => openFilterEditModal("serviceRequests", index),
|
|
onSort: (field) => toggleTableSort("serviceRequests", field),
|
|
onPrev: () => loadPrevPage("serviceRequests"),
|
|
onNext: () => loadNextPage("serviceRequests"),
|
|
onLoadAll: () => loadAllRows("serviceRequests"),
|
|
onOpenRequest: openRequestDetails,
|
|
onMarkRead: markServiceRequestRead,
|
|
onEditRecord: (row) => openEditRecordModal("serviceRequests", row),
|
|
onDeleteRecord: (id) => deleteRecord("serviceRequests", id),
|
|
FilterToolbarComponent: FilterToolbar,
|
|
DataTableComponent: DataTable,
|
|
TablePagerComponent: TablePager,
|
|
StatusLineComponent: StatusLine,
|
|
IconButtonComponent: IconButton
|
|
}
|
|
)), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "requestWorkspace", id: "section-request-workspace" }, /* @__PURE__ */ React.createElement("div", { className: "section-head" }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("h2", null, requestModal.trackNumber ? "\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 \u0437\u0430\u044F\u0432\u043A\u0438 " + requestModal.trackNumber : "\u041A\u0430\u0440\u0442\u043E\u0447\u043A\u0430 \u0437\u0430\u044F\u0432\u043A\u0438")), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "0.45rem", flexWrap: "wrap" } }, /* @__PURE__ */ React.createElement("button", { className: "icon-btn workspace-head-icon", type: "button", "data-tooltip": "\u041D\u0430\u0437\u0430\u0434", "aria-label": "\u041D\u0430\u0437\u0430\u0434", onClick: goBackFromRequestWorkspace }, /* @__PURE__ */ React.createElement("span", { className: "workspace-head-icon-glyph" }, "\u21A9")), /* @__PURE__ */ React.createElement(
|
|
"button",
|
|
{
|
|
className: "icon-btn workspace-head-icon",
|
|
type: "button",
|
|
"data-tooltip": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C",
|
|
"aria-label": "\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C",
|
|
onClick: refreshRequestModal,
|
|
disabled: requestModal.loading || requestModal.fileUploading
|
|
},
|
|
/* @__PURE__ */ React.createElement("span", { className: "workspace-head-icon-glyph" }, "\u21BB")
|
|
))), /* @__PURE__ */ React.createElement(
|
|
RequestWorkspace,
|
|
{
|
|
viewerRole: role,
|
|
viewerUserId: userId,
|
|
viewerUserEmail: email,
|
|
viewerUserName: dictionaries.users?.find((item) => String(item?.id || "") === String(userId || ""))?.name || "",
|
|
loading: requestModal.loading,
|
|
trackNumber: requestModal.trackNumber,
|
|
requestData: requestModal.requestData,
|
|
financeSummary: requestModal.financeSummary,
|
|
invoices: requestModal.invoices || [],
|
|
statusRouteNodes: requestModal.statusRouteNodes,
|
|
statusHistory: requestModal.statusHistory || [],
|
|
availableStatuses: requestModal.availableStatuses || [],
|
|
currentImportantDateAt: requestModal.currentImportantDateAt || "",
|
|
pendingStatusChangePreset: requestModal.pendingStatusChangePreset,
|
|
messages: requestModal.messages || [],
|
|
messagesHasMore: Boolean(requestModal.messagesHasMore),
|
|
messagesLoadingMore: Boolean(requestModal.messagesLoadingMore),
|
|
attachments: requestModal.attachments || [],
|
|
messageDraft: requestModal.messageDraft || "",
|
|
selectedFiles: requestModal.selectedFiles || [],
|
|
fileUploading: Boolean(requestModal.fileUploading),
|
|
status: getStatus("requestModal"),
|
|
onMessageChange: updateRequestModalMessageDraft,
|
|
onSendMessage: submitRequestModalMessage,
|
|
onLoadOlderMessages: loadOlderRequestMessages,
|
|
onFilesSelect: appendRequestModalFiles,
|
|
onRemoveSelectedFile: removeRequestModalFile,
|
|
onClearSelectedFiles: clearRequestModalFiles,
|
|
onLoadRequestDataTemplates: loadRequestDataTemplates,
|
|
onLoadRequestDataBatch: loadRequestDataBatch,
|
|
onLoadRequestDataTemplateDetails: loadRequestDataTemplateDetails,
|
|
onSaveRequestDataTemplate: saveRequestDataTemplate,
|
|
onSaveRequestDataBatch: saveRequestDataBatch,
|
|
onIssueInvoice: issueRequestInvoice,
|
|
onDownloadInvoicePdf: downloadRequestInvoicePdf,
|
|
onChangeStatus: submitRequestStatusChange,
|
|
onConsumePendingStatusChangePreset: clearPendingStatusChangePreset,
|
|
onLiveProbe: probeRequestLive,
|
|
onTypingSignal: setRequestTyping,
|
|
AttachmentPreviewModalComponent: AttachmentPreviewModal,
|
|
StatusLineComponent: StatusLine
|
|
}
|
|
)), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "invoices", id: "section-invoices" }, /* @__PURE__ */ React.createElement(
|
|
InvoicesSection,
|
|
{
|
|
role,
|
|
tables,
|
|
status: getStatus("invoices"),
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
onRefresh: () => loadTable("invoices", { resetOffset: true }),
|
|
onCreate: () => openCreateRecordModal("invoices"),
|
|
onOpenFilter: () => openFilterModal("invoices"),
|
|
onRemoveFilter: (index) => removeFilterChip("invoices", index),
|
|
onEditFilter: (index) => openFilterEditModal("invoices", index),
|
|
onSort: (field) => toggleTableSort("invoices", field),
|
|
onPrev: () => loadPrevPage("invoices"),
|
|
onNext: () => loadNextPage("invoices"),
|
|
onLoadAll: () => loadAllRows("invoices"),
|
|
onOpenRequest: openInvoiceRequest,
|
|
onDownloadPdf: downloadInvoicePdf,
|
|
onEditRecord: (row) => openEditRecordModal("invoices", row),
|
|
onDeleteRecord: (id) => deleteRecord("invoices", id),
|
|
FilterToolbarComponent: FilterToolbar,
|
|
DataTableComponent: DataTable,
|
|
TablePagerComponent: TablePager,
|
|
StatusLineComponent: StatusLine,
|
|
IconButtonComponent: IconButton
|
|
}
|
|
)), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "quotes", id: "section-quotes" }, /* @__PURE__ */ React.createElement(
|
|
QuotesSection,
|
|
{
|
|
tables,
|
|
status: getStatus("quotes"),
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
onRefresh: () => loadTable("quotes", { resetOffset: true }),
|
|
onCreate: () => openCreateRecordModal("quotes"),
|
|
onOpenFilter: () => openFilterModal("quotes"),
|
|
onRemoveFilter: (index) => removeFilterChip("quotes", index),
|
|
onEditFilter: (index) => openFilterEditModal("quotes", index),
|
|
onSort: (field) => toggleTableSort("quotes", field),
|
|
onPrev: () => loadPrevPage("quotes"),
|
|
onNext: () => loadNextPage("quotes"),
|
|
onLoadAll: () => loadAllRows("quotes"),
|
|
onEditRecord: (row) => openEditRecordModal("quotes", row),
|
|
onDeleteRecord: (id) => deleteRecord("quotes", id),
|
|
FilterToolbarComponent: FilterToolbar,
|
|
DataTableComponent: DataTable,
|
|
TablePagerComponent: TablePager,
|
|
StatusLineComponent: StatusLine,
|
|
IconButtonComponent: IconButton
|
|
}
|
|
)), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "config", id: "section-config" }, /* @__PURE__ */ React.createElement(
|
|
ConfigSection,
|
|
{
|
|
token,
|
|
tables,
|
|
dictionaries,
|
|
configActiveKey,
|
|
activeConfigTableState,
|
|
activeConfigMeta,
|
|
genericConfigHeaders,
|
|
canCreateInConfig,
|
|
canUpdateInConfig,
|
|
canDeleteInConfig,
|
|
statusDesignerTopicCode,
|
|
statusDesignerCards,
|
|
getTableLabel,
|
|
getFieldDef,
|
|
getFilterValuePreview,
|
|
resolveReferenceLabel,
|
|
resolveTableConfig,
|
|
getStatus,
|
|
loadCurrentConfigTable,
|
|
onRefreshSmsProviderHealth: () => loadSmsProviderHealth(void 0, { silent: false }),
|
|
smsProviderHealth,
|
|
openCreateRecordModal,
|
|
openFilterModal,
|
|
removeFilterChip,
|
|
openFilterEditModal,
|
|
toggleTableSort,
|
|
openEditRecordModal,
|
|
deleteRecord,
|
|
loadStatusDesignerTopic,
|
|
openCreateStatusTransitionForTopic,
|
|
loadPrevPage,
|
|
loadNextPage,
|
|
loadAllRows,
|
|
FilterToolbarComponent: FilterToolbar,
|
|
DataTableComponent: DataTable,
|
|
TablePagerComponent: TablePager,
|
|
StatusLineComponent: StatusLine,
|
|
IconButtonComponent: IconButton,
|
|
UserAvatarComponent: UserAvatar
|
|
}
|
|
)), /* @__PURE__ */ React.createElement(Section, { active: activeSection === "availableTables", id: "section-available-tables" }, /* @__PURE__ */ React.createElement(
|
|
AvailableTablesSection,
|
|
{
|
|
tables,
|
|
status: getStatus("availableTables"),
|
|
onRefresh: () => loadAvailableTables(),
|
|
onToggleActive: updateAvailableTableState,
|
|
DataTableComponent: DataTable,
|
|
StatusLineComponent: StatusLine,
|
|
IconButtonComponent: IconButton
|
|
}
|
|
)))), /* @__PURE__ */ React.createElement(
|
|
RecordModal,
|
|
{
|
|
open: recordModal.open,
|
|
title: (recordModal.mode === "edit" ? "\u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u2022 " : "\u0421\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u2022 ") + getTableLabel(recordModal.tableKey),
|
|
tableKey: recordModal.tableKey,
|
|
mode: recordModal.mode,
|
|
fields: recordModalFields,
|
|
form: recordModal.form || {},
|
|
status: getStatus("recordForm"),
|
|
accessToken: token,
|
|
onClose: closeRecordModal,
|
|
onChange: updateRecordField,
|
|
onUploadField: uploadRecordFieldFile,
|
|
onUploadFieldWithCrop: uploadRecordFieldFile,
|
|
onRecropAvatar: recropAvatar,
|
|
onApplyRecrop: applyRecrop,
|
|
onSubmit: submitRecordModal,
|
|
OverlayComponent: Overlay,
|
|
IconButtonComponent: IconButton,
|
|
UserAvatarComponent: UserAvatar,
|
|
StatusLineComponent: StatusLine
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
FilterModal,
|
|
{
|
|
open: filterModal.open,
|
|
tableLabel: filterTableLabel,
|
|
fields: activeFilterFields,
|
|
draft: filterModal,
|
|
status: getStatus("filter"),
|
|
onClose: closeFilterModal,
|
|
onFieldChange: updateFilterField,
|
|
onOpChange: updateFilterOp,
|
|
onValueChange: updateFilterValue,
|
|
onSubmit: applyFilterModal,
|
|
onClear: clearFiltersFromModal,
|
|
getOperators: getOperatorsForType,
|
|
getFieldOptions
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
KanbanSortModal,
|
|
{
|
|
open: kanbanSortModal.open,
|
|
value: kanbanSortModal.value,
|
|
status: getStatus("kanbanSort"),
|
|
onChange: updateKanbanSortMode,
|
|
onClose: closeKanbanSortModal,
|
|
onSubmit: submitKanbanSortModal
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
ReassignModal,
|
|
{
|
|
open: reassignModal.open,
|
|
status: getStatus("reassignForm"),
|
|
options: getLawyerOptions(),
|
|
value: reassignModal.lawyerId,
|
|
onChange: updateReassignLawyer,
|
|
onClose: closeReassignModal,
|
|
onSubmit: submitReassignModal,
|
|
trackNumber: reassignModal.trackNumber
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
TotpSetupModal,
|
|
{
|
|
open: totpSetupModal.open,
|
|
status: getStatus("totpSetup"),
|
|
secret: totpSetupModal.secret,
|
|
uri: totpSetupModal.uri,
|
|
qrDataUrl: totpSetupModal.qrDataUrl,
|
|
code: totpSetupModal.code,
|
|
loading: totpSetupModal.loading,
|
|
onCodeChange: updateTotpSetupCode,
|
|
onClose: closeTotpSetupModal,
|
|
onSubmit: submitTotpSetup,
|
|
onCopySecret: copyTotpSecret,
|
|
onCopyUri: copyTotpUri
|
|
}
|
|
), /* @__PURE__ */ React.createElement(
|
|
AccountModal,
|
|
{
|
|
open: accountModal.open,
|
|
status: getStatus("account"),
|
|
profileLoading: accountModal.loading,
|
|
saveLoading: accountModal.saving,
|
|
form: accountModal.form,
|
|
currentEmail: email,
|
|
currentRoleLabel: roleLabel(role),
|
|
totpStatus,
|
|
onFieldChange: updateAccountField,
|
|
onClose: closeAccountModal,
|
|
onSubmit: submitAccountModal,
|
|
onSetupTotp: setupTotp,
|
|
onRegenerateBackupCodes: regenerateTotpBackupCodes,
|
|
onDisableTotp: disableTotp,
|
|
onLogout: logout
|
|
}
|
|
), !token || !role ? /* @__PURE__ */ React.createElement(LoginScreen, { onSubmit: login, status: getStatus("login") }) : null, /* @__PURE__ */ React.createElement(GlobalTooltipLayer, null));
|
|
}
|
|
const root = ReactDOM.createRoot(document.getElementById("admin-root"));
|
|
root.render(/* @__PURE__ */ React.createElement(App, null));
|
|
})();
|
|
})();
|