@janiscommerce/app-tracking-shift 1.0.1 → 1.2.0

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.
@@ -5,21 +5,29 @@ import {setObject, getObject} from './utils/storage';
5
5
 
6
6
  class OfflineData {
7
7
  get hasData() {
8
- const offlineData = getObject(OFFLINE_DATA, {});
9
- return Object.keys(offlineData).length > 0;
8
+ const offlineData = getObject(OFFLINE_DATA, []);
9
+ return offlineData.length > 0;
10
10
  }
11
11
 
12
12
  /**
13
13
  * Saves data to the offline data storage.
14
14
  *
15
- * @param {string} storageId - The ID of the storage to save the data to.
15
+ * @param {string} id - The reference id of the data.
16
16
  * @param {Object} data - The data to save.
17
17
  */
18
18
 
19
- save(storageId, data) {
19
+ save(id, data) {
20
20
  try {
21
- const offlineData = getObject(OFFLINE_DATA, {});
22
- offlineData[storageId] = {...offlineData[storageId], ...data};
21
+ const offlineData = getObject(OFFLINE_DATA, []);
22
+ const foundIdx = offlineData.findIndex((item) => item.storageId === id);
23
+
24
+ if (foundIdx === -1) {
25
+ offlineData.push({storageId: id, ...data});
26
+ } else {
27
+ const storedData = offlineData[foundIdx];
28
+ offlineData[foundIdx] = {...storedData, ...data};
29
+ }
30
+
23
31
  setObject(OFFLINE_DATA, offlineData);
24
32
  } catch (error) {
25
33
  throw new Error(error);
@@ -29,21 +37,37 @@ class OfflineData {
29
37
  /**
30
38
  * Gets data from the offline data storage.
31
39
  *
32
- * @param {string | Array<string> | null} storageId - The ID of the storage to get the data from.
40
+ * @param {string | Array<string> | null} id - The ID of the storage to get the data from.
33
41
  * @returns {Array<Object>} The data from the storage.
34
42
  */
35
43
 
36
- get(storageId = null) {
44
+ get(id = null) {
37
45
  try {
38
- const offlineData = getObject(OFFLINE_DATA, {});
46
+ const offlineData = getObject(OFFLINE_DATA, []);
39
47
 
40
- if (!isArray(storageId)) {
41
- storageId = [storageId].filter(Boolean);
48
+ if (!isArray(id)) {
49
+ id = [id].filter(Boolean);
42
50
  }
43
51
 
44
- if (isEmptyArray(storageId)) return Object.values(offlineData);
52
+ if (isEmptyArray(id)) return offlineData;
45
53
 
46
- return storageId.map((id) => offlineData[id]);
54
+ return offlineData.filter((item) => id.includes(item.storageId));
55
+ } catch (error) {
56
+ throw new Error(error);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Gets the last record from the offline data storage.
62
+ *
63
+ * @returns {Object} The last record from the storage.
64
+ */
65
+
66
+ getLastRecord() {
67
+ try {
68
+ const offlineData = this.get();
69
+ const [lastRecord] = offlineData.slice(-1);
70
+ return lastRecord;
47
71
  } catch (error) {
48
72
  throw new Error(error);
49
73
  }
@@ -52,24 +76,22 @@ class OfflineData {
52
76
  /**
53
77
  * Deletes data from the offline data storage.
54
78
  *
55
- * @param {string | Array<string>} storageId - The ID of the storage to delete the data from.
79
+ * @param {string | Array<string>} id - The reference id of the data.
56
80
  */
57
81
 
58
- delete(storageId) {
82
+ delete(id) {
59
83
  try {
60
- const offlineData = getObject(OFFLINE_DATA, {});
84
+ const offlineData = getObject(OFFLINE_DATA, []);
61
85
 
62
- if (!isArray(storageId)) {
63
- storageId = [storageId].filter(Boolean);
86
+ if (!isArray(id)) {
87
+ id = [id].filter(Boolean);
64
88
  }
65
89
 
66
- if (isEmptyArray(storageId)) return;
90
+ if (isEmptyArray(id)) return;
67
91
 
68
- storageId.forEach((id) => {
69
- delete offlineData[id];
70
- });
92
+ const filteredData = offlineData.filter((item) => !id.includes(item.storageId));
71
93
 
72
- setObject(OFFLINE_DATA, offlineData);
94
+ setObject(OFFLINE_DATA, filteredData);
73
95
  } catch (error) {
74
96
  throw new Error(error);
75
97
  }
package/lib/Shift.js CHANGED
@@ -1,10 +1,23 @@
1
- import TimeTracker from './db/TimeTrackerService';
2
1
  import StaffService from './StaffApiServices';
3
2
  import Storage from './db/StorageService';
4
3
  import Crashlytics from './utils/crashlytics';
5
4
  import ShiftWorklogs from './ShiftWorklogs';
6
- import {generateRandomId, isEmptyArray, isEmptyObject, isObject} from './utils/helpers';
7
- import {getObject, getShiftData, setObject} from './utils/storage';
5
+ import errorParser from './utils/errorParser';
6
+ import {
7
+ generateRandomId,
8
+ isEmptyArray,
9
+ isEmptyObject,
10
+ isNumber,
11
+ isObject,
12
+ isValidObject,
13
+ } from './utils/helpers';
14
+ import {
15
+ deleteStoredWorkLog,
16
+ getObject,
17
+ getShiftData,
18
+ getStaffAuthorizationData,
19
+ setObject,
20
+ } from './utils/storage';
8
21
  import {
9
22
  SHIFT_ID,
10
23
  SHIFT_STATUS,
@@ -12,9 +25,8 @@ import {
12
25
  CURRENT_WORKLOG_DATA,
13
26
  CURRENT_WORKLOG_ID,
14
27
  EXCLUDED_WORKLOG_TYPES,
15
- ONE_HOUR_EXTENSION,
28
+ DEFAULT_REOPENING_EXTENSION_TIME,
16
29
  } from './constant';
17
- import TrackerRecords from './TrackerRecords';
18
30
  import Formatter from './Formatter';
19
31
  import OfflineData from './OfflineData';
20
32
  /**
@@ -24,10 +36,25 @@ import OfflineData from './OfflineData';
24
36
 
25
37
  class Shift {
26
38
  /**
27
- * Open a work shift in the staff MS and record this event in the time tracking database.
28
- * @param {Object} params
29
- * @throws {Error} error
30
- * @returns {Promise<string>} shiftId => ID related to the shift that has just been opened for the user
39
+ * @returns {boolean} hasStaffAuthorization => true if the user has staff MS authorization, false otherwise
40
+ */
41
+
42
+ get hasStaffAuthorize() {
43
+ const {hasStaffAuthorization} = getStaffAuthorizationData();
44
+ return hasStaffAuthorization;
45
+ }
46
+
47
+ /**
48
+ * @returns {boolean} hasPendingData => true if the user has pending data, false otherwise
49
+ */
50
+
51
+ get hasPendingData() {
52
+ return OfflineData.hasData;
53
+ }
54
+
55
+ /**
56
+ * Check if the staff MS is enabled
57
+ * @returns {Promise<boolean>} true if the staff MS is enabled, false otherwise
31
58
  */
32
59
 
33
60
  async checkStaffMSAuthorization() {
@@ -38,33 +65,42 @@ class Shift {
38
65
 
39
66
  return enabledShiftAndWorkLog;
40
67
  } catch (error) {
41
- Crashlytics.recordError(error, 'Error checking staff MS authorization');
42
- return Promise.reject(error);
68
+ const parsedError = errorParser(error);
69
+ Crashlytics.recordError(parsedError, 'Error checking staff MS authorization');
70
+ return Promise.reject(parsedError);
43
71
  }
44
72
  }
45
73
 
46
- async open(params = {}) {
74
+ /**
75
+ * Open a work shift in the staff MS and record this event in the time tracking database.
76
+ * @param {Object} params
77
+ * @throws {Error} error
78
+ * @returns {Promise<string>} shiftId => ID related to the shift that has just been opened for the user
79
+ */
80
+
81
+ async open() {
47
82
  try {
48
- Crashlytics.log('user open shift');
49
- const {date} = params;
83
+ Crashlytics.log('openShift:');
84
+
85
+ this._requireStaffAuthorization();
86
+
50
87
  const {result: shift} = await StaffService.openShift();
51
88
  const {id: shiftId = ''} = shift || {};
52
89
 
53
90
  const openShift = await this.getUserOpenShift({id: shiftId});
54
91
 
55
- await this._startTracking({
56
- id: shiftId,
57
- date: date || openShift.startDate,
58
- });
59
-
60
92
  Storage.set(SHIFT_ID, shiftId);
61
93
  Storage.set(SHIFT_STATUS, 'opened');
62
- Storage.set(SHIFT_DATA, JSON.stringify(openShift));
94
+ setObject(SHIFT_DATA, openShift);
63
95
 
64
96
  return shiftId;
65
97
  } catch (error) {
66
- Crashlytics.recordError(error, 'Error opening shift in staff service');
67
- return Promise.reject(error);
98
+ const parsedError = errorParser(error);
99
+ Crashlytics.recordError(
100
+ parsedError,
101
+ 'An error occurred while trying to open a shift for the user'
102
+ );
103
+ return Promise.reject(parsedError);
68
104
  }
69
105
  }
70
106
 
@@ -77,14 +113,17 @@ class Shift {
77
113
 
78
114
  async finish(params = {}) {
79
115
  try {
80
- Crashlytics.log('user close shift');
116
+ Crashlytics.log('closeShift:');
117
+
118
+ this._requireStaffAuthorization();
119
+
81
120
  const shiftIsExpired = this.isDateToCloseExceeded();
82
121
 
83
122
  if (shiftIsExpired) {
84
123
  await this.reOpen();
85
124
  }
86
125
 
87
- if (OfflineData.hasData) {
126
+ if (this.hasPendingData) {
88
127
  await this.sendPendingWorkLogs();
89
128
  }
90
129
 
@@ -94,20 +133,19 @@ class Shift {
94
133
  const endDate = date || new Date().toISOString();
95
134
  const shiftData = getShiftData();
96
135
 
97
- await this._finishTracking({id: shiftId, date});
98
-
99
136
  const updatedShiftData = {
100
137
  ...shiftData,
101
138
  endDate,
102
139
  };
103
140
 
104
141
  Storage.set(SHIFT_STATUS, 'closed');
105
- Storage.set(SHIFT_DATA, JSON.stringify(updatedShiftData));
142
+ setObject(SHIFT_DATA, updatedShiftData);
106
143
 
107
144
  return shiftId;
108
145
  } catch (error) {
109
- Crashlytics.recordError(error, 'Error closing shift in staff service');
110
- return Promise.reject(error);
146
+ const parsedError = errorParser(error);
147
+ Crashlytics.recordError(parsedError, 'An error occurred while trying to close user shift');
148
+ return Promise.reject(parsedError);
111
149
  }
112
150
  }
113
151
 
@@ -124,13 +162,22 @@ class Shift {
124
162
 
125
163
  async openWorkLog(workLog = {}) {
126
164
  try {
127
- Crashlytics.log('user open shift worklog', workLog);
165
+ Crashlytics.log('openWorkLog:', workLog);
166
+
167
+ this._requireStaffAuthorization();
128
168
 
129
169
  if (!isObject(workLog) || isEmptyObject(workLog)) return null;
170
+ const currentTime = new Date().toISOString();
171
+ const mustCloseLastWorkLog = this._isNeccesaryCloseLastWorkLog();
172
+
173
+ if (mustCloseLastWorkLog) {
174
+ const lastWorkLog = getObject(CURRENT_WORKLOG_DATA);
175
+
176
+ await this.finishWorkLog(lastWorkLog);
177
+ }
130
178
 
131
179
  const {referenceId, name, type, suggestedTime = 0} = workLog;
132
180
  const shiftId = Storage.getString(SHIFT_ID);
133
- const startTime = new Date().toISOString();
134
181
  const randomId = generateRandomId();
135
182
 
136
183
  const workLogId = Formatter.formatWorkLogId(referenceId, randomId);
@@ -138,34 +185,24 @@ class Shift {
138
185
  // TODO: uncomment this when resolve how to handle offline workLogs
139
186
  // await ShiftWorklogs.open({
140
187
  // referenceId,
141
- // startDate: startTime,
188
+ // startDate: currentTime,
142
189
  // });
143
190
 
144
- const dataForTimeTracker = {
145
- type,
146
- name,
147
- shiftId,
148
- referenceId,
149
- };
150
-
151
191
  OfflineData.save(workLogId, {
152
192
  referenceId,
153
- startDate: startTime,
154
- });
155
-
156
- await this._startTracking({
157
- id: workLogId,
158
- date: startTime,
159
- payload: dataForTimeTracker,
193
+ startDate: currentTime,
160
194
  });
161
195
 
162
- const suggestedFinishDate = new Date(startTime).getTime() + suggestedTime * 60 * 1000;
196
+ const suggestedFinishDate = new Date(currentTime).getTime() + suggestedTime * 60 * 1000;
163
197
 
164
198
  const dataForStorage = {
165
- ...dataForTimeTracker,
199
+ type,
200
+ name,
201
+ shiftId,
202
+ referenceId,
166
203
  suggestedFinishDate: new Date(suggestedFinishDate).toISOString(),
167
204
  suggestedTime,
168
- startDate: startTime,
205
+ startDate: currentTime,
169
206
  };
170
207
 
171
208
  if (!EXCLUDED_WORKLOG_TYPES.includes(referenceId)) {
@@ -177,8 +214,9 @@ class Shift {
177
214
 
178
215
  return workLogId;
179
216
  } catch (error) {
180
- Crashlytics.recordError(error, 'Error opening shift worklog');
181
- return Promise.reject(error);
217
+ const parsedError = errorParser(error);
218
+ Crashlytics.recordError(parsedError, 'An error occurred while trying to open user workLog');
219
+ return Promise.reject(parsedError);
182
220
  }
183
221
  }
184
222
 
@@ -186,62 +224,65 @@ class Shift {
186
224
  * Finish a work log in the staff MS and record this event in the time tracking database.
187
225
  * @param {Object} workLog
188
226
  * @param {string} workLog.referenceId => Reference ID related to the work log
189
- * @param {string} workLog.name => Name related to the work log
190
- * @param {string} workLog.type => Type related to the work log
191
227
  * @throws {Error} error
192
228
  * @returns {Promise<string>} workLogId => ID related to the work log that has just been closed for the user
193
229
  */
194
230
 
195
231
  async finishWorkLog(workLog = {}) {
196
232
  try {
197
- Crashlytics.log('user close shift worklog', workLog);
233
+ Crashlytics.log('finishWorkLog:', workLog);
234
+
235
+ this._requireStaffAuthorization();
236
+
198
237
  if (!isObject(workLog) || isEmptyObject(workLog)) return null;
199
238
 
200
- const {referenceId, name, type} = workLog;
201
- const shiftId = Storage.getString(SHIFT_ID);
239
+ const currentWorkLog = getObject(CURRENT_WORKLOG_DATA);
240
+ const {referenceId} = workLog;
241
+
242
+ if (!isValidObject(currentWorkLog)) {
243
+ throw new Error('There is no active worklog to close');
244
+ }
245
+
246
+ if (currentWorkLog?.referenceId !== referenceId) {
247
+ throw new Error(
248
+ 'The worklog you are trying to close is different from the one that is currently open.'
249
+ );
250
+ }
251
+
202
252
  const shiftStatus = Storage.getString(SHIFT_STATUS);
203
253
  const endTime = new Date().toISOString();
204
254
  const workLogId = Storage.getString(CURRENT_WORKLOG_ID);
205
-
206
255
  // TODO: uncomment this when resolve how to handle offline workLogs
207
256
  // await ShiftWorklogs.finish({
208
257
  // referenceId,
209
258
  // endDate: endTime,
210
259
  // });
211
260
 
212
- const dataForTimeTracker = {
213
- type,
214
- name,
215
- shiftId,
216
- referenceId,
217
- };
218
-
219
261
  OfflineData.save(workLogId, {
220
262
  referenceId,
221
263
  endDate: endTime,
222
264
  });
223
265
 
224
- await this._finishTracking({
225
- id: workLogId,
226
- date: endTime,
227
- payload: dataForTimeTracker,
228
- });
229
-
230
266
  if (shiftStatus === 'paused') {
231
267
  Storage.set(SHIFT_STATUS, 'opened');
232
268
  }
233
269
 
234
- this._deleteCurrentWorkLogData();
270
+ deleteStoredWorkLog();
235
271
 
236
272
  return workLogId;
237
273
  } catch (error) {
238
- Crashlytics.recordError(error, 'Error closing shift worklog');
239
- return Promise.reject(error);
274
+ const parsedError = errorParser(error);
275
+ Crashlytics.recordError(parsedError, 'An error occurred while trying to close user workLog');
276
+ return Promise.reject(parsedError);
240
277
  }
241
278
  }
242
279
 
243
280
  async sendPendingWorkLogs() {
244
281
  try {
282
+ Crashlytics.log('sendPendingWorkLogs:');
283
+
284
+ this._requireStaffAuthorization();
285
+
245
286
  const storageData = OfflineData.get();
246
287
  const formatedWorkLogs = Formatter.formatOfflineWorkLog(storageData);
247
288
 
@@ -273,7 +314,10 @@ class Shift {
273
314
 
274
315
  async getUserOpenShift(params = {}) {
275
316
  try {
276
- Crashlytics.log('user get open shift', params);
317
+ Crashlytics.log('getUserOpenShift:', params);
318
+
319
+ this._requireStaffAuthorization();
320
+
277
321
  const {userId, id, ...rest} = params;
278
322
  const {result: shifts} = await StaffService.getShiftsList({
279
323
  filters: {
@@ -287,8 +331,9 @@ class Shift {
287
331
 
288
332
  return openShift || {};
289
333
  } catch (error) {
290
- Crashlytics.recordError(error, 'Error getting open shift in staff service');
291
- return Promise.reject(error);
334
+ const parsedError = errorParser(error);
335
+ Crashlytics.recordError(parsedError, 'Error getting open shift in staff service');
336
+ return Promise.reject(parsedError);
292
337
  }
293
338
  }
294
339
 
@@ -301,7 +346,10 @@ class Shift {
301
346
 
302
347
  async getWorkLogs(shiftId) {
303
348
  try {
304
- Crashlytics.log('user get work logs');
349
+ Crashlytics.log('getWorkLogs:');
350
+
351
+ this._requireStaffAuthorization();
352
+
305
353
  const userShiftId = shiftId || Storage.getString(SHIFT_ID);
306
354
 
307
355
  if (!userShiftId) throw new Error('Shift ID not found');
@@ -310,8 +358,12 @@ class Shift {
310
358
 
311
359
  return Formatter.formatWorkLogsFromJanis(workLogs);
312
360
  } catch (error) {
313
- Crashlytics.recordError(error, 'Error getting work logs in staff service');
314
- return Promise.reject(error);
361
+ const parsedError = errorParser(error);
362
+ Crashlytics.recordError(
363
+ parsedError,
364
+ 'An error occurred while trying to get user workLogs from staff service'
365
+ );
366
+ return Promise.reject(parsedError);
315
367
  }
316
368
  }
317
369
 
@@ -323,7 +375,10 @@ class Shift {
323
375
 
324
376
  async reOpen() {
325
377
  try {
326
- Crashlytics.log('user re open shift');
378
+ Crashlytics.log('reOpenShift:');
379
+
380
+ this._requireStaffAuthorization();
381
+
327
382
  const shiftIsExpired = this.isDateMaxToCloseExceeded();
328
383
 
329
384
  if (shiftIsExpired) {
@@ -334,8 +389,9 @@ class Shift {
334
389
 
335
390
  return null;
336
391
  } catch (error) {
337
- Crashlytics.recordError(error, 'Error re opening shift');
338
- return Promise.reject(error);
392
+ const parsedError = errorParser(error);
393
+ Crashlytics.recordError(parsedError, 'An error occurred while trying to re open user shift');
394
+ return Promise.reject(parsedError);
339
395
  }
340
396
  }
341
397
 
@@ -355,83 +411,6 @@ class Shift {
355
411
  return new Date(dateMaxToClose).getTime() < currentDate.getTime();
356
412
  }
357
413
 
358
- /**
359
- * Gets a shift report based on the shift ID and its registered events.
360
- *
361
- * The report includes information about performed activities, start and end times,
362
- * total elapsed time, work time and pause time.
363
- *
364
- * @async
365
- * @function getReport
366
- * @throws {Error} If the `shiftId` is not found in storage.
367
- * @returns {Promise<{
368
- * activities: Array<{
369
- * id: string,
370
- * name: string,
371
- * type: string,
372
- * startDate: string,
373
- * endDate: string,
374
- * duration: number
375
- * }>,
376
- * startDate: string,
377
- * endDate: string,
378
- * elapsedTime: number,
379
- * workTime: number,
380
- * pauseTime: number,
381
- * isComplete: boolean,
382
- * error: string | null
383
- * }>} Object with shift details.
384
- *
385
- * @property {Array} activities - List of activities registered during the shift.
386
- * @property {string} activities[].id - Unique identifier of the activity.
387
- * @property {string} activities[].name - Name or description of the activity.
388
- * @property {string} activities[].startDate - Start date and time of the activity.
389
- * @property {string} activities[].endDate - End date and time of the activity.
390
- * @property {number} activities[].duration - Duration of the activity in milliseconds.
391
- * @property {string} startDate - Start date and time of the shift.
392
- * @property {string} endDate - End date and time of the shift (can be empty if not finished).
393
- * @property {number} elapsedTime - Total elapsed time between shift start and end.
394
- * @property {number} workTime - Effective work time (total time minus pauses).
395
- * @property {number} pauseTime - Total time of registered pauses.
396
- * @property {boolean} isComplete - Indicates if the report was obtained completely without errors.
397
- * @property {string|null} error - Error message if the report is not complete, or null if there were no errors.
398
- */
399
-
400
- async getReport() {
401
- try {
402
- Crashlytics.log('user get shift report');
403
- const shiftId = Storage.getString(SHIFT_ID);
404
-
405
- if (!shiftId) throw new Error('Shift ID is required');
406
-
407
- const startDate = await TrackerRecords.getStartDateById(shiftId);
408
- const endDate = await TrackerRecords.getEndDateById(shiftId);
409
- const workLogs = await ShiftWorklogs.getShiftTrackedWorkLogs(shiftId);
410
-
411
- const activities = Formatter.formatShiftActivities(workLogs);
412
- const elapsedTime = TimeTracker.getElapsedTime({
413
- startTime: startDate,
414
- ...(!!endDate && {endTime: endDate}),
415
- format: false,
416
- });
417
-
418
- const pauseTime = activities.reduce((acc, activity) => acc + (activity?.duration || 0), 0);
419
- const workTime = elapsedTime - pauseTime;
420
-
421
- return {
422
- activities,
423
- startDate,
424
- endDate,
425
- elapsedTime,
426
- workTime,
427
- pauseTime,
428
- };
429
- } catch (reason) {
430
- Crashlytics.recordError(reason, 'Error getting shift report');
431
- return Promise.reject(reason);
432
- }
433
- }
434
-
435
414
  /**
436
415
  * Fetch the work log types from the staff MS and prepare them for register an activity.
437
416
  * @throws {Error} error
@@ -440,13 +419,17 @@ class Shift {
440
419
 
441
420
  async fetchWorklogTypes() {
442
421
  try {
443
- Crashlytics.log('user fetch worklog types');
422
+ Crashlytics.log('fetchWorklogTypes:');
423
+
424
+ this._requireStaffAuthorization();
425
+
444
426
  const {result: workLogTypes = []} = await StaffService.getWorkLogTypes();
445
427
 
446
428
  return Formatter.formatWorkLogTypes(workLogTypes);
447
429
  } catch (error) {
448
- Crashlytics.recordError(error, 'Error fetching worklog types in staff service');
449
- return Promise.reject(error);
430
+ const parsedError = errorParser(error);
431
+ Crashlytics.recordError(parsedError, 'Error fetching worklog types from staff service');
432
+ return Promise.reject(parsedError);
450
433
  }
451
434
  }
452
435
 
@@ -458,14 +441,17 @@ class Shift {
458
441
 
459
442
  async deleteShiftRegisters() {
460
443
  try {
461
- Crashlytics.log('user delete shift registers');
444
+ Crashlytics.log('deleteShiftRegisters:');
462
445
  this._deleteShiftData();
463
- this._deleteCurrentWorkLogData();
464
- OfflineData.deleteAll();
465
- return await TimeTracker.deleteAllEvents();
446
+ deleteStoredWorkLog();
447
+ return OfflineData.deleteAll();
466
448
  } catch (error) {
467
- Crashlytics.recordError(error, 'Error deleting registers from shift tracking database');
468
- return Promise.reject(error);
449
+ const parsedError = errorParser(error);
450
+ Crashlytics.recordError(
451
+ parsedError,
452
+ 'An error occurred while trying to delete shift storage data'
453
+ );
454
+ return Promise.reject(parsedError);
469
455
  }
470
456
  }
471
457
 
@@ -475,64 +461,57 @@ class Shift {
475
461
  Storage.delete(SHIFT_DATA);
476
462
  }
477
463
 
478
- _deleteCurrentWorkLogData() {
479
- Storage.delete(CURRENT_WORKLOG_ID);
480
- Storage.delete(CURRENT_WORKLOG_DATA);
481
- }
482
-
483
464
  /**
484
465
  * @private
485
- * Start id tracking in the time tracking database.
486
- * @param {Object} params
487
- * @param {string} params.id => ID related to the shift
488
- * @param {string} params.date => Date related to the shift
489
- * @param {Object} params.payload => Payload related to the shift
466
+ * Extend the shift closing date.
467
+ * @throws {Error} error
468
+ * @returns {Promise<null>} null
490
469
  */
491
470
 
492
- async _startTracking(params) {
493
- const {id, date, payload} = params;
494
- await TimeTracker.addEvent({
495
- id,
496
- time: date || new Date().toISOString(),
497
- type: 'start',
498
- payload,
499
- }).catch(() => null);
471
+ _extendShiftClosingDate() {
472
+ const shiftData = getObject(SHIFT_DATA);
473
+ const {dateToClose, reopeningExtensionTime} = shiftData; // reopeningExtensionTime is in minutes
474
+ let extensionTime = DEFAULT_REOPENING_EXTENSION_TIME;
475
+
476
+ if (reopeningExtensionTime && isNumber(reopeningExtensionTime)) {
477
+ extensionTime = reopeningExtensionTime * 60 * 1000; // Convert minutes to milliseconds
478
+ }
479
+
480
+ const updatedClosingDate = new Date(dateToClose).getTime() + extensionTime; // Add the extension time to the closing date
481
+
482
+ shiftData.dateToClose = new Date(updatedClosingDate).toISOString();
483
+
484
+ setObject(SHIFT_DATA, shiftData);
500
485
  }
501
486
 
502
487
  /**
503
488
  * @private
504
- * Finish id tracking in the time tracking database.
505
- * @param {Object} params
506
- * @param {string} params.id => ID related to the shift
507
- * @param {string} params.date => Date related to the shift
508
- * @param {Object} params.payload => Payload related to the shift
489
+ * Validate if the user has staff MS authorization
490
+ * @throws {Error} error
491
+ * @returns {Promise<null>} null
509
492
  */
510
493
 
511
- async _finishTracking(params) {
512
- const {id, date, payload} = params;
513
- await TimeTracker.addEvent({
514
- id,
515
- time: date || new Date().toISOString(),
516
- type: 'finish',
517
- payload,
518
- }).catch(() => null);
494
+ _requireStaffAuthorization() {
495
+ if (this.hasStaffAuthorize) return;
496
+ throw new Error('Staff MS authorization is required');
519
497
  }
520
498
 
521
499
  /**
522
500
  * @private
523
- * Extend the shift closing date.
524
- * @throws {Error} error
525
- * @returns {Promise<null>} null
501
+ * Validate if is neccesary close the last work log
502
+ * @param {Object} newWorkLog
503
+ * @param {string} newWorkLog.referenceId => Reference ID related to the new work log
504
+ * @returns {boolean} true if is neccesary close the last work log, false otherwise
526
505
  */
527
506
 
528
- _extendShiftClosingDate() {
529
- const shiftData = getObject(SHIFT_DATA);
530
- const {dateToClose} = shiftData;
531
- const updatedClosingDate = new Date(dateToClose).getTime() + ONE_HOUR_EXTENSION;
507
+ _isNeccesaryCloseLastWorkLog() {
508
+ const lastWorkLog = OfflineData.getLastRecord();
509
+ if (!isValidObject(lastWorkLog)) return false;
532
510
 
533
- shiftData.dateToClose = new Date(updatedClosingDate).toISOString();
511
+ const isLastWorkLogClosed = !!lastWorkLog?.endDate;
534
512
 
535
- setObject(SHIFT_DATA, shiftData);
513
+ return !isLastWorkLogClosed;
536
514
  }
537
515
  }
516
+
538
517
  export default new Shift();
@@ -1,5 +1,4 @@
1
1
  import StaffApiServices from './StaffApiServices';
2
- import TrackerRecords from './TrackerRecords';
3
2
  import {isArray, isEmptyArray} from './utils/helpers';
4
3
 
5
4
  class ShiftWorklogs {
@@ -63,20 +62,6 @@ class ShiftWorklogs {
63
62
  }
64
63
  }
65
64
 
66
- async getShiftTrackedWorkLogs(shiftId) {
67
- try {
68
- if (!shiftId) throw new Error(`Shift ID is required, but got ${shiftId}`);
69
-
70
- const workLogEvents = await TrackerRecords.getClientShiftActivities(shiftId);
71
- const shiftWorkLogs = workLogEvents.filter((e) => e?.payload?.shiftId === shiftId);
72
- shiftWorkLogs.sort((a, b) => new Date(a?.time) - new Date(b?.time));
73
-
74
- return shiftWorkLogs;
75
- } catch (error) {
76
- return Promise.reject(error);
77
- }
78
- }
79
-
80
65
  async postPendingBatch(pendingWorkLogs = []) {
81
66
  try {
82
67
  if (!isArray(pendingWorkLogs) || isEmptyArray(pendingWorkLogs)) return null;
@@ -30,4 +30,4 @@ export const INTERNAL_WORKLOGS = {
30
30
  };
31
31
 
32
32
  // SHIFT CLOSE EXTENSION
33
- export const ONE_HOUR_EXTENSION = 60 * 60 * 1000; // 1 hour
33
+ export const DEFAULT_REOPENING_EXTENSION_TIME = 60 * 60 * 1000; // 1 hour
@@ -1,6 +1,7 @@
1
1
  import {useMMKVString} from 'react-native-mmkv';
2
2
  import {useMemo} from 'react';
3
3
  import Crashlytics from '../../utils/crashlytics';
4
+ import errorParser from '../../utils/errorParser';
4
5
 
5
6
  export const useMMKVObject = (key, defaultValue = null) => {
6
7
  const [raw] = useMMKVString(key);
@@ -10,7 +11,8 @@ export const useMMKVObject = (key, defaultValue = null) => {
10
11
  try {
11
12
  return JSON.parse(raw);
12
13
  } catch (e) {
13
- Crashlytics.recordError(e, `Invalid JSON in MMKV key: ${key}`);
14
+ const parsedError = errorParser(e);
15
+ Crashlytics.recordError(parsedError, `Invalid JSON in MMKV key: ${key}`);
14
16
  return defaultValue;
15
17
  }
16
18
  }, [raw]);
@@ -6,7 +6,6 @@ import {
6
6
  downloadWorkLogTypes,
7
7
  isAuthorizedToUseStaffMS,
8
8
  getShiftWorkLogsFromJanis,
9
- saveWorkLogTimesInDB,
10
9
  } from '../utils/provider';
11
10
  import {
12
11
  CURRENT_WORKLOG_DATA,
@@ -20,7 +19,6 @@ import {
20
19
  } from '../constant';
21
20
  import {useMMKVObject} from '../hooks/useMMKVObject';
22
21
  import {isValidObject, promiseWrapper} from '../utils/helpers';
23
- import Crashlytics from '../utils/crashlytics';
24
22
  import Storage from '../db/StorageService';
25
23
 
26
24
  const ShiftTrackingProvider = ({children, onError = null}) => {
@@ -129,7 +127,7 @@ const ShiftTrackingProvider = ({children, onError = null}) => {
129
127
  return;
130
128
  }
131
129
 
132
- const {openWorkLogs, closedWorkLogs} = workLogs;
130
+ const {openWorkLogs} = workLogs;
133
131
  const [currentWorkLog = {}] = openWorkLogs;
134
132
  const isExcludedWork = EXCLUDED_WORKLOG_TYPES.includes(currentWorkLog?.referenceId);
135
133
 
@@ -142,14 +140,6 @@ const ShiftTrackingProvider = ({children, onError = null}) => {
142
140
  Storage.set(SHIFT_STATUS, 'paused');
143
141
  }
144
142
 
145
- const promises = [currentWorkLog, ...closedWorkLogs].map((workLog) =>
146
- saveWorkLogTimesInDB(workLog).catch((workLogError) => {
147
- Crashlytics.recordError(workLogError, 'error trying to save work log times in db', workLog);
148
- })
149
- );
150
-
151
- await Promise.all(promises);
152
-
153
143
  setOpenShiftResult((prev) => ({
154
144
  ...prev,
155
145
  getWorkLogs: false,
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @description Parse the error to a readable format
3
+ * @param {Error} error - The error to parse
4
+ * @returns {Error} The parsed error
5
+ */
6
+
7
+ const errorParser = (error = {}) => {
8
+ if (error instanceof Error) return error;
9
+
10
+ const {result = {}, message} = error;
11
+ const reportedError = result?.message || message;
12
+
13
+ return new Error(reportedError);
14
+ };
15
+
16
+ export default errorParser;
@@ -4,6 +4,7 @@ import {WORKLOG_TYPES_DATA, WORKLOG_TYPES_EXPIRATION_TIME} from '../../../consta
4
4
  import Storage from '../../../db/StorageService';
5
5
  import Crashlytics from '../../crashlytics';
6
6
  import {isFunction} from '../../helpers';
7
+ import errorParser from '../../errorParser';
7
8
 
8
9
  const downloadWorkLogTypes = async (onDownloadError) => {
9
10
  try {
@@ -21,9 +22,10 @@ const downloadWorkLogTypes = async (onDownloadError) => {
21
22
  Storage.set(WORKLOG_TYPES_DATA, JSON.stringify(data));
22
23
  return null;
23
24
  } catch (error) {
24
- Crashlytics.recordError(error, 'Error downloading worklog types');
25
- if (isFunction(onDownloadError)) onDownloadError(error);
26
- return Promise.reject(error?.result || error);
25
+ const parsedError = errorParser(error);
26
+ Crashlytics.recordError(parsedError, 'Error downloading worklog types');
27
+ if (isFunction(onDownloadError)) onDownloadError(parsedError);
28
+ return Promise.reject(parsedError);
27
29
  }
28
30
  };
29
31
 
@@ -2,4 +2,3 @@ export {default as downloadWorkLogTypes} from './downloadWorkLogTypes';
2
2
  export {default as openShift} from './openShift';
3
3
  export {default as isAuthorizedToUseStaffMS} from './isAuthorizedToUseStaffMS';
4
4
  export {default as getShiftWorkLogsFromJanis} from './getShiftWorkLogsFromJanis';
5
- export {default as saveWorkLogTimesInDB} from './saveWorkLogTimesInDB';
@@ -3,6 +3,7 @@ import {STAFF_AUTH, STAFF_MS_AUTHORIZATION_EXPIRATION_TIME} from '../../../const
3
3
  import Storage from '../../../db/StorageService';
4
4
  import Shift from '../../../Shift';
5
5
  import {getStaffAuthorizationData} from '../../storage';
6
+ import errorParser from '../../errorParser';
6
7
 
7
8
  const isAuthorizedToUseStaffMS = async () => {
8
9
  try {
@@ -24,7 +25,8 @@ const isAuthorizedToUseStaffMS = async () => {
24
25
 
25
26
  return isAuthorized;
26
27
  } catch (error) {
27
- Crashlytics.recordError(error, 'Error checking staff MS authorization');
28
+ const parsedError = errorParser(error);
29
+ Crashlytics.recordError(parsedError, 'Error checking staff MS authorization');
28
30
  const data = {
29
31
  hasStaffAuthorization: false,
30
32
  expirationTime: 0,
@@ -32,7 +34,7 @@ const isAuthorizedToUseStaffMS = async () => {
32
34
  };
33
35
  Storage.set(STAFF_AUTH, JSON.stringify(data));
34
36
 
35
- return Promise.reject(error);
37
+ return Promise.reject(parsedError);
36
38
  }
37
39
  };
38
40
 
@@ -1,16 +1,11 @@
1
1
  import Storage from '../../../db/StorageService';
2
2
  import Crashlytics from '../../crashlytics';
3
- import {
4
- SHIFT_ID,
5
- SHIFT_STATUS,
6
- SHIFT_DATA,
7
- CURRENT_WORKLOG_ID,
8
- CURRENT_WORKLOG_DATA,
9
- } from '../../../constant';
3
+ import {SHIFT_ID, SHIFT_STATUS, SHIFT_DATA} from '../../../constant';
10
4
  import getUserId from '../../userInfo/getUserId';
11
5
  import Shift from '../../../Shift';
12
6
  import {isFunction} from '../../helpers';
13
- import TimeTracker from '../../../db/TimeTrackerService';
7
+ import {deleteStoredWorkLog, setObject} from '../../storage';
8
+ import errorParser from '../../errorParser';
14
9
 
15
10
  const openShift = async (onOpenShiftError) => {
16
11
  try {
@@ -43,26 +38,20 @@ const openShift = async (onOpenShiftError) => {
43
38
  getWorkLogs: false,
44
39
  };
45
40
 
46
- Storage.delete(CURRENT_WORKLOG_ID);
47
- Storage.delete(CURRENT_WORKLOG_DATA);
41
+ deleteStoredWorkLog();
48
42
  Storage.set(SHIFT_ID, currentShift.id);
49
43
  Storage.set(SHIFT_STATUS, currentShift.status);
50
- Storage.set(SHIFT_DATA, JSON.stringify(currentShift));
51
-
52
- await TimeTracker.addEvent({
53
- id: currentShift.id,
54
- time: currentShift.startDate,
55
- type: 'start',
56
- }).catch(() => null);
44
+ setObject(SHIFT_DATA, currentShift);
57
45
 
58
46
  return {
59
47
  openShiftId: currentShift.id,
60
48
  getWorkLogs: true,
61
49
  };
62
50
  } catch (error) {
63
- Crashlytics.recordError(error, 'Error opening shift in staff service');
64
- if (isFunction(onOpenShiftError)) onOpenShiftError(error);
65
- return Promise.reject(error?.result || error);
51
+ const parsedError = errorParser(error);
52
+ Crashlytics.recordError(parsedError, 'Error opening shift in staff service');
53
+ if (isFunction(onOpenShiftError)) onOpenShiftError(parsedError);
54
+ return Promise.reject(parsedError);
66
55
  }
67
56
  };
68
57
 
@@ -0,0 +1,9 @@
1
+ import {CURRENT_WORKLOG_DATA, CURRENT_WORKLOG_ID} from '../../../constant';
2
+ import Storage from '../../../db/StorageService';
3
+
4
+ const deleteStoredWorkLog = () => {
5
+ Storage.delete(CURRENT_WORKLOG_ID);
6
+ Storage.delete(CURRENT_WORKLOG_DATA);
7
+ };
8
+
9
+ export default deleteStoredWorkLog;
@@ -1,6 +1,7 @@
1
1
  import Storage from '../../../db/StorageService';
2
2
  import {STAFF_AUTH} from '../../../constant';
3
3
  import Crashlytics from '../../crashlytics';
4
+ import errorParser from '../../errorParser';
4
5
 
5
6
  const getStaffAuthorizationData = () => {
6
7
  try {
@@ -13,7 +14,8 @@ const getStaffAuthorizationData = () => {
13
14
  isExpired: expirationTime <= Date.now() || isExpired,
14
15
  };
15
16
  } catch (error) {
16
- Crashlytics.recordError(error, 'Error getting staff authorization data');
17
+ const parsedError = errorParser(error);
18
+ Crashlytics.recordError(parsedError, 'Error getting staff authorization data');
17
19
  return {
18
20
  hasStaffAuthorization: false,
19
21
  isExpired: true,
@@ -1,6 +1,7 @@
1
1
  import Storage from '../../../db/StorageService';
2
2
  import Crashlytics from '../../crashlytics';
3
3
  import {WORKLOG_TYPES_DATA} from '../../../constant';
4
+ import errorParser from '../../errorParser';
4
5
 
5
6
  /**
6
7
  * @description Get the worklog types data from the storage
@@ -25,7 +26,8 @@ const getWorkLogTypesData = () => {
25
26
  isExpired: expirationTime <= Date.now() || !workLogTypes.length,
26
27
  };
27
28
  } catch (error) {
28
- Crashlytics.recordError(error, 'Error getting worklogs data');
29
+ const parsedError = errorParser(error);
30
+ Crashlytics.recordError(parsedError, 'Error getting worklogs data');
29
31
  return {
30
32
  workLogTypes: [],
31
33
  expirationTime: 0,
@@ -3,6 +3,7 @@ import Storage from '../../db/StorageService';
3
3
  export {default as getWorkLogTypesData} from './getWorkLogTypesData';
4
4
  export {default as getShiftData} from './getShiftData';
5
5
  export {default as getStaffAuthorizationData} from './getStaffAuthorizationData';
6
+ export {default as deleteStoredWorkLog} from './deleteStoredWorkLog';
6
7
 
7
8
  export const setObject = (key, value) => {
8
9
  const jsonValue = JSON.stringify(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@janiscommerce/app-tracking-shift",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "main": "lib/index.js",
5
5
  "module": "lib/index.js",
6
6
  "exports": {
@@ -47,7 +47,6 @@
47
47
  "peerDependencies": {
48
48
  "@janiscommerce/app-crashlytics": ">=2.1.0",
49
49
  "@janiscommerce/app-request": ">=2.0.0",
50
- "@janiscommerce/app-tracking-time": ">=2.2.1",
51
50
  "react": ">=17.0.2 <19",
52
51
  "react-dom": ">=17.0.2 <19",
53
52
  "react-native": ">=0.67.5 <0.75"
@@ -55,8 +54,7 @@
55
54
  "dependencies": {
56
55
  "@janiscommerce/app-device-info": "^1.1.0",
57
56
  "@janiscommerce/oauth-native": "^1.10.2",
58
- "react-native-mmkv": "2.12.2",
59
- "realm": "11.3.0"
57
+ "react-native-mmkv": "2.12.2"
60
58
  },
61
59
  "devDependencies": {
62
60
  "@babel/core": "^7.0.0",
@@ -65,7 +63,6 @@
65
63
  "@babel/preset-react": "^7.0.0",
66
64
  "@janiscommerce/app-crashlytics": "^2.1.0",
67
65
  "@janiscommerce/app-request": "^2.6.0",
68
- "@janiscommerce/app-tracking-time": "^2.2.1",
69
66
  "@testing-library/react": "^16.3.0",
70
67
  "@testing-library/react-native": "^12.0.1",
71
68
  "babel-jest": "^29.0.0",
@@ -1,103 +0,0 @@
1
- import TimeTracker from './db/TimeTrackerService';
2
- import {reverseArray} from './utils/helpers';
3
-
4
- class TrackerRecords {
5
- async getWorkLogsFromTimeTracker(filteredId) {
6
- try {
7
- if (!filteredId) throw new Error(`Excluding ID is required, but got ${filteredId}`);
8
-
9
- const events = await this._filterEventsExcludingId(filteredId);
10
-
11
- return events;
12
- } catch (error) {
13
- return Promise.reject(error);
14
- }
15
- }
16
-
17
- async getClientShiftActivities(id) {
18
- try {
19
- const query = 'NOT (id CONTAINS[c] "picking" OR id CONTAINS[c] "delivery") AND id != $0';
20
- const clientActivities = await TimeTracker.searchEventByQuery(query, id);
21
-
22
- return clientActivities;
23
- } catch (error) {
24
- return Promise.reject(error);
25
- }
26
- }
27
-
28
- async getStartDateById(id) {
29
- try {
30
- if (!id) throw new Error(`ID is required, but got ${id}`);
31
-
32
- const startShiftEvent = await this._getStartEventById(id);
33
-
34
- return startShiftEvent?.time;
35
- } catch (error) {
36
- return Promise.reject(error);
37
- }
38
- }
39
-
40
- async getEndDateById(id) {
41
- try {
42
- if (!id) throw new Error(`ID is required, but got ${id}`);
43
-
44
- const endShiftEvent = await this._getFinishEventById(id);
45
-
46
- return endShiftEvent?.time;
47
- } catch (error) {
48
- return Promise.reject(error);
49
- }
50
- }
51
-
52
- async _getStartEventById(id) {
53
- try {
54
- const startEvent = await this._filterEventByType(id, 'start');
55
-
56
- const [firstEvent = {}] = startEvent;
57
-
58
- return firstEvent;
59
- } catch (error) {
60
- return Promise.reject(error);
61
- }
62
- }
63
-
64
- async _getFinishEventById(id) {
65
- try {
66
- const finishEvent = await this._filterEventByType(id, 'finish');
67
-
68
- const [lastEvent = {}] = reverseArray(finishEvent);
69
-
70
- return lastEvent;
71
- } catch (error) {
72
- return Promise.reject(error);
73
- }
74
- }
75
-
76
- async _filterEventByType(id, type) {
77
- try {
78
- if (!id) throw new Error('id is required');
79
- if (!type) throw new Error('type is required');
80
-
81
- const events = await TimeTracker.searchEventByQuery('id == $0 AND type == $1', id, type);
82
- console.log('events', events);
83
-
84
- return events;
85
- } catch (error) {
86
- return Promise.reject(error);
87
- }
88
- }
89
-
90
- async _filterEventsExcludingId(id) {
91
- try {
92
- if (!id) throw new Error('id is required');
93
-
94
- const events = await TimeTracker.searchEventByQuery('id!= $0', id);
95
-
96
- return events;
97
- } catch (error) {
98
- return Promise.reject(error);
99
- }
100
- }
101
- }
102
-
103
- export default new TrackerRecords();
@@ -1,5 +0,0 @@
1
- import EventTrackingService from '@janiscommerce/app-tracking-time';
2
-
3
- const TimeTrackerService = new EventTrackingService('shift-tracking');
4
-
5
- export default TimeTrackerService;
@@ -1,38 +0,0 @@
1
- import TimeTracker from '../../../db/TimeTrackerService';
2
- import {isEmptyObject, isObject} from '../../helpers';
3
-
4
- const saveWorkLogTimesInDB = async (workLog = {}) => {
5
- try {
6
- if (!isObject(workLog) || isEmptyObject(workLog)) return false;
7
-
8
- const {startDate, endDate, shiftId, id, referenceId, name} = workLog;
9
-
10
- const dataForDB = {
11
- shiftId,
12
- referenceId,
13
- name,
14
- };
15
-
16
- await TimeTracker.addEvent({
17
- id,
18
- type: 'start',
19
- time: startDate,
20
- payload: dataForDB,
21
- });
22
-
23
- if (endDate) {
24
- await TimeTracker.addEvent({
25
- id,
26
- type: 'finish',
27
- time: endDate,
28
- payload: dataForDB,
29
- });
30
- }
31
-
32
- return true;
33
- } catch (error) {
34
- return Promise.reject(error);
35
- }
36
- };
37
-
38
- export default saveWorkLogTimesInDB;