@phenyxhealth/importer 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +22 -0
- package/src/controllers/doctors.js +26 -0
- package/src/controllers/global.js +84 -0
- package/src/controllers/linacs.js +34 -0
- package/src/controllers/login.js +15 -0
- package/src/index.js +47 -0
- package/src/scripts/clearPatients.js +20 -0
- package/src/scripts/importer-old.js +435 -0
- package/src/scripts/importer.js +346 -0
- package/src/services/phenyxApi.js +483 -0
- package/src/services/phenyxApiInstance.js +5 -0
- package/src/utils/ask.js +19 -0
- package/src/utils/colors.js +42 -0
- package/src/utils/dates.js +21 -0
- package/src/utils/text.js +85 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import { doesNotMatch } from 'assert';
|
|
2
|
+
import { v4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
class PhenyxApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.apiToken = null;
|
|
7
|
+
this.apiHost = null;
|
|
8
|
+
this.user = null;
|
|
9
|
+
this.config = {};
|
|
10
|
+
this.globalInfo = null;
|
|
11
|
+
this.activeTables = null;
|
|
12
|
+
this.doctors = [];
|
|
13
|
+
this.defaultDuration = {
|
|
14
|
+
firstSession: '30',
|
|
15
|
+
regularSessions: '15',
|
|
16
|
+
};
|
|
17
|
+
this.defaultCompatibleLinacs = [];
|
|
18
|
+
this.oars = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
peerReviewedStatus = {
|
|
22
|
+
'Discussion without changes': 'Discussion without changes',
|
|
23
|
+
'No changes': 'No changes',
|
|
24
|
+
'Discusión sin cambios': 'Discussion without changes',
|
|
25
|
+
'No cambios': 'No changes',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
globalTypes = {
|
|
29
|
+
humanResources: {
|
|
30
|
+
type: 'Global',
|
|
31
|
+
name: 'Human Resources Types',
|
|
32
|
+
values: {
|
|
33
|
+
RadOnc: 'RadOnc',
|
|
34
|
+
MedPhys: 'MedPhys',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
sendingDepartment: {
|
|
38
|
+
type: 'Global',
|
|
39
|
+
name: 'Sending Department',
|
|
40
|
+
},
|
|
41
|
+
cei9: {
|
|
42
|
+
type: 'Global',
|
|
43
|
+
name: 'CEI9',
|
|
44
|
+
},
|
|
45
|
+
treatmentClass: {
|
|
46
|
+
type: 'Global',
|
|
47
|
+
name: 'Treatment Class',
|
|
48
|
+
},
|
|
49
|
+
treatmentSubClass: {
|
|
50
|
+
type: 'Global',
|
|
51
|
+
name: 'Treatment SubClass',
|
|
52
|
+
},
|
|
53
|
+
treatmentTechnique: {
|
|
54
|
+
type: 'Global',
|
|
55
|
+
name: 'Treatment Technique',
|
|
56
|
+
},
|
|
57
|
+
treatmentType: {
|
|
58
|
+
type: 'Global',
|
|
59
|
+
name: 'Treatment Type',
|
|
60
|
+
},
|
|
61
|
+
treatmentGoal: {
|
|
62
|
+
type: 'Global',
|
|
63
|
+
name: 'Treatment Goal',
|
|
64
|
+
},
|
|
65
|
+
treatmentParticles: {
|
|
66
|
+
type: 'Global',
|
|
67
|
+
name: 'Treatment Particle',
|
|
68
|
+
},
|
|
69
|
+
noTreatmentReasons: {
|
|
70
|
+
type: 'Global',
|
|
71
|
+
name: 'No Treatment Reasons',
|
|
72
|
+
},
|
|
73
|
+
ctSimProtocols: {
|
|
74
|
+
type: 'Global',
|
|
75
|
+
name: 'CT Sim Protocols',
|
|
76
|
+
},
|
|
77
|
+
linacsAllocationCriteria: {
|
|
78
|
+
type: 'Global',
|
|
79
|
+
name: 'LINACs Allocation Criteria',
|
|
80
|
+
values: {
|
|
81
|
+
default: 'Default',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
sessionDurationCriteria: {
|
|
85
|
+
type: 'Global',
|
|
86
|
+
name: 'Session Duration Criteria',
|
|
87
|
+
values: {
|
|
88
|
+
default: 'Default',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
contouringTools: {
|
|
92
|
+
type: 'Global',
|
|
93
|
+
name: 'Contouring Tools',
|
|
94
|
+
},
|
|
95
|
+
fractionationTypes: {
|
|
96
|
+
type: 'Global',
|
|
97
|
+
name: 'Fractionation Types',
|
|
98
|
+
},
|
|
99
|
+
dosimetryCalculationMethods: {
|
|
100
|
+
type: 'Global',
|
|
101
|
+
name: 'Dosimetry Calculation Methods',
|
|
102
|
+
},
|
|
103
|
+
dosimetrySecondaryCalculationMethods: {
|
|
104
|
+
type: 'Global',
|
|
105
|
+
name: 'Dosimetry 2nd Calculation Methods',
|
|
106
|
+
},
|
|
107
|
+
tps: {
|
|
108
|
+
type: 'Global',
|
|
109
|
+
name: 'TPS available',
|
|
110
|
+
},
|
|
111
|
+
treatmentVerificationProtocols: {
|
|
112
|
+
type: 'Global',
|
|
113
|
+
name: 'Treatment Verification Protocols',
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
apiCall = async ({ method, url, data }) => {
|
|
118
|
+
const fullUrl = `${this.apiHost}${url}`;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const params = {
|
|
122
|
+
method,
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
...(this.apiToken ? { Authorization: `Bearer ${this.apiToken}` } : {})
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (data && method.toUpperCase() !== 'GET') params.body = JSON.stringify(data);
|
|
130
|
+
|
|
131
|
+
if (this.config.showLogs) console.log(`Calling to: ${method.toUpperCase()} ${fullUrl}\n`, params);
|
|
132
|
+
|
|
133
|
+
const response = await fetch(fullUrl, params);
|
|
134
|
+
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const errorText = await response.text();
|
|
137
|
+
console.error(`Error: ${response.status} - ${errorText}`);
|
|
138
|
+
console.log(`Calling to: ${method.toUpperCase()} ${fullUrl}\n`, params);
|
|
139
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const output = await response.json();
|
|
143
|
+
|
|
144
|
+
return output;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error('Request failed', error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
showLogs = (showLogs) => this.config.showLogs = showLogs;
|
|
152
|
+
|
|
153
|
+
login = async ({ apiHost, user, password }) => {
|
|
154
|
+
if (!apiHost) {
|
|
155
|
+
throw new Error('Var apiHost is required');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!user) {
|
|
159
|
+
throw new Error('Var user is required');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!password) {
|
|
163
|
+
throw new Error('Var password is required');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
this.apiHost = apiHost.endsWith('/') ? apiHost : `${apiHost}/`;
|
|
167
|
+
|
|
168
|
+
const response = await this.apiCall({
|
|
169
|
+
method: 'POST',
|
|
170
|
+
url: `users/login`,
|
|
171
|
+
data: { user, password },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
this.apiToken = response.token;
|
|
175
|
+
this.user = response.user;
|
|
176
|
+
|
|
177
|
+
await this.retrieveGlobalInfo();
|
|
178
|
+
|
|
179
|
+
await this.retrieveActiveTables();
|
|
180
|
+
|
|
181
|
+
await this.retrieveDefaults();
|
|
182
|
+
|
|
183
|
+
await this.retrieveOars();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getToken = () => {
|
|
187
|
+
return this.apiToken;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
retrieveGlobalInfo = async () => {
|
|
191
|
+
const globalInfo = await this.apiCall({
|
|
192
|
+
method: 'GET',
|
|
193
|
+
url: `global-info`,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.globalInfo = globalInfo;
|
|
197
|
+
|
|
198
|
+
return globalInfo;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
retrieveOars = async () => {
|
|
202
|
+
const response = await this.apiCall({
|
|
203
|
+
method: 'GET',
|
|
204
|
+
url: `tables/Oar`,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
this.oars = response;
|
|
208
|
+
|
|
209
|
+
return response;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
retrieveActiveTables = async () => {
|
|
213
|
+
const activeTables = await this.apiCall({
|
|
214
|
+
method: 'GET',
|
|
215
|
+
url: `tables/actives`,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
this.activeTables = activeTables;
|
|
219
|
+
|
|
220
|
+
return activeTables;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
retrieveDefaults = async () => {
|
|
224
|
+
try {
|
|
225
|
+
this.defaultCompatibleLinacs = this
|
|
226
|
+
.activeTables
|
|
227
|
+
.find(t => t.type.name === 'LINACs Allocation Table')
|
|
228
|
+
.data
|
|
229
|
+
.items
|
|
230
|
+
.find(d => d.criteriaElementName === 'Default')
|
|
231
|
+
.resources
|
|
232
|
+
.filter(r => !!r.isActive)
|
|
233
|
+
.map(r => ({
|
|
234
|
+
id: r.resourceId,
|
|
235
|
+
name: r.resourceName,
|
|
236
|
+
}));
|
|
237
|
+
} catch (err) {
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
this.defaultDuration = this
|
|
242
|
+
.activeTables
|
|
243
|
+
.find(t => t.type.name === 'Session Duration Table')
|
|
244
|
+
.data
|
|
245
|
+
.items
|
|
246
|
+
.find(d => d.sessionDurationCriteria === 'Default');
|
|
247
|
+
} catch (err) {
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
defaultDuration: this.defaultDuration,
|
|
252
|
+
defaultCompatibleLinacs: this.defaultCompatibleLinacs,
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
updateGlobalInfo = async (globalTypeId, data) => {
|
|
257
|
+
await this.apiCall({
|
|
258
|
+
method: 'PUT',
|
|
259
|
+
url: `global-info/${globalTypeId}`,
|
|
260
|
+
data,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await this.retrieveGlobalInfo();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
getGlobalInfo = (globalType) => {
|
|
267
|
+
return this.globalInfo
|
|
268
|
+
?.find(item => item.typeName === globalType.type)
|
|
269
|
+
?.globalInfos
|
|
270
|
+
?.find(item => item.name === globalType.name);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
getGlobalInfoElement = (globalType, elementName) => {
|
|
274
|
+
return this.getGlobalInfo(globalType)
|
|
275
|
+
?.elements
|
|
276
|
+
?.find(item => item.name.toLowerCase() === elementName.toLowerCase());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
getGlobalInfoElementId = (globalType, elementName) => {
|
|
280
|
+
if (!elementName) return null;
|
|
281
|
+
|
|
282
|
+
return this.getGlobalInfoElement(globalType, elementName)?.id;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
getHumanResources = async () => {
|
|
286
|
+
const response= await this.apiCall({
|
|
287
|
+
method: 'GET',
|
|
288
|
+
url: `human-resources`,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
this.doctors = response;
|
|
292
|
+
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
createHumanResource = async ({ name, typeId, agenda = [] }) => {
|
|
297
|
+
return this.apiCall({
|
|
298
|
+
method: 'POST',
|
|
299
|
+
url: `human-resources`,
|
|
300
|
+
data: {
|
|
301
|
+
name,
|
|
302
|
+
typeId,
|
|
303
|
+
agenda,
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getResourcesTypes = async () => {
|
|
309
|
+
return this.apiCall({
|
|
310
|
+
method: 'GET',
|
|
311
|
+
url: `resource-types`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
getResources = async () => {
|
|
316
|
+
const response = await this.apiCall({
|
|
317
|
+
method: 'GET',
|
|
318
|
+
url: `resources`,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return response;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
createResource = async ({ name, author = 'ADMIN', typeId, agenda = [] }) => {
|
|
325
|
+
return this.apiCall({
|
|
326
|
+
method: 'POST',
|
|
327
|
+
url: `resources`,
|
|
328
|
+
data: {
|
|
329
|
+
name,
|
|
330
|
+
author,
|
|
331
|
+
typeId,
|
|
332
|
+
agenda,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getSendingDepartmentIdByName = (name) => {
|
|
338
|
+
const sendingDepartment = this.getGlobalInfoElement(this.globalTypes.sendingDepartment, name);
|
|
339
|
+
if (!sendingDepartment) {
|
|
340
|
+
throw new Error(`Sending Department ${name} not found`);
|
|
341
|
+
}
|
|
342
|
+
return sendingDepartment.id;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
getCei9IdByName = (name) => {
|
|
346
|
+
const cei9 = this.getGlobalInfoElement(this.globalTypes.cei9, name);
|
|
347
|
+
if (!cei9) {
|
|
348
|
+
throw new Error(`CEI9 ${name} not found`);
|
|
349
|
+
}
|
|
350
|
+
return cei9.id;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
createPatient = async (data) => {
|
|
354
|
+
const response = await this.apiCall({
|
|
355
|
+
method: 'POST',
|
|
356
|
+
url: `patients`,
|
|
357
|
+
data,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return response.id;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
getPatients = async () => {
|
|
364
|
+
return this.apiCall({
|
|
365
|
+
method: 'GET',
|
|
366
|
+
url: `patients`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
getPatient = async (id) => {
|
|
371
|
+
return this.apiCall({
|
|
372
|
+
method: 'GET',
|
|
373
|
+
url: `patients/${id}/clinical-data`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
deletePatient = async (id) => {
|
|
378
|
+
return this.apiCall({
|
|
379
|
+
method: 'DELETE',
|
|
380
|
+
url: `patients/${id}`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
createPatientStateChanges = ({ formDataId, date = today(), humanResourceId = null, nextStateCode }) => {
|
|
385
|
+
return this.apiCall({
|
|
386
|
+
method: 'POST',
|
|
387
|
+
url: `patients/state-changes`,
|
|
388
|
+
data: {
|
|
389
|
+
date,
|
|
390
|
+
formDataId,
|
|
391
|
+
humanResourceId,
|
|
392
|
+
nextStateCode,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
getRewiewedStatus = (status) => {
|
|
398
|
+
const saveStatus = this.peerReviewedStatus[status];
|
|
399
|
+
if (!saveStatus) return [];
|
|
400
|
+
|
|
401
|
+
return [{
|
|
402
|
+
id: v4(),
|
|
403
|
+
name: saveStatus,
|
|
404
|
+
coments: '',
|
|
405
|
+
date: new Date().toLocaleDateString(),
|
|
406
|
+
hour: new Date().toLocaleTimeString(),
|
|
407
|
+
}];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
allStatus = {
|
|
411
|
+
NOTSTARTED: 'NOT_STARTED',
|
|
412
|
+
COMPLETED: 'COMPLETED',
|
|
413
|
+
INPROGRESS: 'IN_PROGRESS',
|
|
414
|
+
ONREQUEST: 'ON_REQUEST',
|
|
415
|
+
READYTOSTART: 'READY_TO_START',
|
|
416
|
+
DONE: 'DONE',
|
|
417
|
+
REVIEWED: 'REVIEWED',
|
|
418
|
+
VALIDATED: 'VALIDATED',
|
|
419
|
+
PEERREVIEWING: 'PEER_REVIEWING',
|
|
420
|
+
PEERREVIEWED: 'PEER_REVIEWED',
|
|
421
|
+
DONEMAIN: 'DONE',
|
|
422
|
+
DONESECONDCALCULATION: 'DONE_SECOND',
|
|
423
|
+
STARTPROPOSED: 'START_PROPOSED',
|
|
424
|
+
STARTCONFIRMED: 'START_CONFIRMED',
|
|
425
|
+
CANCELED: 'CANCELED',
|
|
426
|
+
CANCELLED: 'CANCELED',
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
getQaStatus = (section) => {
|
|
430
|
+
const currentStatus = section.qa.currentStatus || null;
|
|
431
|
+
return currentStatus
|
|
432
|
+
? this.allStatus[currentStatus.toUpperCase().replaceAll(' ', '').replaceAll('_', '')]
|
|
433
|
+
: null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
getDoctorIdByName = (name) => {
|
|
437
|
+
const doc = this.doctors.find(d => d.name === name);
|
|
438
|
+
return doc ? doc.id : null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
getDoctorTypeIdByName = (name) => {
|
|
442
|
+
const doc = this.doctors.find(d => d.name === name);
|
|
443
|
+
return doc ? doc.typeId : null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
getDoctorTypeNameByName = (name) => {
|
|
447
|
+
const doc = this.doctors.find(d => d.name === name);
|
|
448
|
+
return doc ? doc.typeName : null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
getSortedQa = (qa) => {
|
|
452
|
+
const qaArray = [];
|
|
453
|
+
for (const qaKey of Object.keys(qa)) {
|
|
454
|
+
if (qa[qaKey].date) qaArray.push({
|
|
455
|
+
status: this.allStatus[qaKey.toUpperCase()],
|
|
456
|
+
...qa[qaKey]
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
const sortedQa = qaArray.sort((a, b) => new Date(a.data) - new Date(b.data));
|
|
460
|
+
|
|
461
|
+
return sortedQa;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
updateSectionQaStatus = async (qa, formDataId) => {
|
|
465
|
+
const sortedQa = this.getSortedQa(qa);
|
|
466
|
+
|
|
467
|
+
for (const item of sortedQa) {
|
|
468
|
+
await this.createPatientStateChanges({
|
|
469
|
+
formDataId,
|
|
470
|
+
date: item.date,
|
|
471
|
+
humanResourceId: this.getDoctorIdByName(item.who),
|
|
472
|
+
nextStateCode: item.status,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
getElementIdByName = (type, name) => {
|
|
478
|
+
const el = this[type].find(e => e.name === name);
|
|
479
|
+
return el?.id || null;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export default PhenyxApi;
|
package/src/utils/ask.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import readline from "readline";
|
|
2
|
+
|
|
3
|
+
const ask = (question) => {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(question, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
ask,
|
|
19
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Genera un color aleatorio en formato RGB que contrasta bien tanto con fondo blanco como negro
|
|
3
|
+
* @returns {string} Color en formato "R, G, B"
|
|
4
|
+
*/
|
|
5
|
+
const getContrastingColor = () => {
|
|
6
|
+
// Colores base que contrastan bien tanto en blanco como en negro
|
|
7
|
+
const baseColors = [
|
|
8
|
+
[244, 67, 54], // Rojo
|
|
9
|
+
[233, 30, 99], // Rosa
|
|
10
|
+
[156, 39, 176], // Morado
|
|
11
|
+
[103, 58, 183], // Morado profundo
|
|
12
|
+
[63, 81, 181], // Índigo
|
|
13
|
+
[33, 150, 243], // Azul
|
|
14
|
+
[0, 188, 212], // Cian
|
|
15
|
+
[0, 150, 136], // Verde azulado
|
|
16
|
+
[76, 175, 80], // Verde
|
|
17
|
+
[139, 195, 74], // Verde lima
|
|
18
|
+
[205, 220, 57], // Lima
|
|
19
|
+
[255, 193, 7], // Ámbar
|
|
20
|
+
[255, 152, 0], // Naranja
|
|
21
|
+
[255, 87, 34], // Naranja profundo
|
|
22
|
+
[121, 85, 72], // Marrón
|
|
23
|
+
[96, 125, 139] // Azul grisáceo
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Seleccionar un color base aleatorio
|
|
27
|
+
const color = baseColors[Math.floor(Math.random() * baseColors.length)];
|
|
28
|
+
|
|
29
|
+
// Añadir una pequeña variación aleatoria para más diversidad
|
|
30
|
+
// manteniendo el contraste dentro de límites seguros
|
|
31
|
+
const variation = () => Math.floor(Math.random() * 30) - 15;
|
|
32
|
+
|
|
33
|
+
const r = Math.max(0, Math.min(255, color[0] + variation()));
|
|
34
|
+
const g = Math.max(0, Math.min(255, color[1] + variation()));
|
|
35
|
+
const b = Math.max(0, Math.min(255, color[2] + variation()));
|
|
36
|
+
|
|
37
|
+
return `${r}, ${g}, ${b}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
getContrastingColor,
|
|
42
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const formatDateString = (date) => {
|
|
2
|
+
if (!date) return '';
|
|
3
|
+
const [day, month, year] = date.split('/');
|
|
4
|
+
return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const formatDate = (date) => {
|
|
8
|
+
const year = date.getFullYear();
|
|
9
|
+
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
10
|
+
const day = date.getDate().toString().padStart(2, '0');
|
|
11
|
+
|
|
12
|
+
return `${year}-${month}-${day}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const today = () => formatDate(new Date());
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
formatDateString,
|
|
19
|
+
formatDate,
|
|
20
|
+
today,
|
|
21
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fixText = str => str
|
|
2
|
+
.split(' ')
|
|
3
|
+
.filter(Boolean)
|
|
4
|
+
.join(' ');
|
|
5
|
+
|
|
6
|
+
const fixTextList = list => {
|
|
7
|
+
const netList = list
|
|
8
|
+
.map(fixText)
|
|
9
|
+
.filter(Boolean);
|
|
10
|
+
|
|
11
|
+
const response = [];
|
|
12
|
+
const lowercaseItems = [];
|
|
13
|
+
|
|
14
|
+
for (const item of netList) {
|
|
15
|
+
if (!lowercaseItems.includes(item.toLowerCase())) {
|
|
16
|
+
response.push(item);
|
|
17
|
+
lowercaseItems.push(item.toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return response;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const getListFromField = (data, ...fields) => {
|
|
25
|
+
const list = [];
|
|
26
|
+
for (const line of data) {
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
let item = line;
|
|
29
|
+
const items = [];
|
|
30
|
+
|
|
31
|
+
for (const tree of field.split('.')) {
|
|
32
|
+
if (!item) break;
|
|
33
|
+
if (tree.startsWith('*')) {
|
|
34
|
+
const subField = tree.substring(1);
|
|
35
|
+
for (const key of Object.keys(item)) {
|
|
36
|
+
if (item[key][subField]) {
|
|
37
|
+
items.push(item[key][subField]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
item = item[tree];
|
|
42
|
+
if (typeof (item) === 'string') items.push(item);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
if (item && !list.includes(item)) list.push(item);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return fixTextList(list);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getConditionalListFromField = (data, field, conditions) => {
|
|
56
|
+
const list = [];
|
|
57
|
+
|
|
58
|
+
for (const item of data) {
|
|
59
|
+
if (typeof item !== 'object') continue;
|
|
60
|
+
|
|
61
|
+
if (!!item[field]) {
|
|
62
|
+
let isRight = true;
|
|
63
|
+
for (const [condField, condValue] of Object.entries(conditions)) {
|
|
64
|
+
if (item[condField] !== condValue) {
|
|
65
|
+
isRight = false;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isRight) list.push(item[field]);
|
|
71
|
+
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
list.push(...getConditionalListFromField(Object.values(item), field, conditions));
|
|
76
|
+
}
|
|
77
|
+
return fixTextList(list);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
fixText,
|
|
82
|
+
fixTextList,
|
|
83
|
+
getListFromField,
|
|
84
|
+
getConditionalListFromField,
|
|
85
|
+
};
|