Skip to content

Commit

Permalink
[Fix] Offline sync and permission (#8222)
Browse files Browse the repository at this point in the history
* feat: add hasAllPermissions and hasAnyPermissions utility function

* fix: toggleApiStop method adding source and refactor

* fix: added optional chaining operator to access `status.lastLog.id`

* [Fix] Activity Watch integration issue (#8223)

* feat: add timeout to upload Images method time tracker service

* fix: speed up timer start/stop
  • Loading branch information
adkif committed Sep 17, 2024
1 parent 046d167 commit eeba5f9
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 60 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class SequenceQueue extends OfflineQueue<ISequence> {
? latest
: {
...timer,
id: status.lastLog.id
id: status?.lastLog?.id
},
...timer
});
Expand Down
21 changes: 21 additions & 0 deletions packages/desktop-ui-lib/src/lib/shared/utils/permission.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Checks if the user has all the required permissions.
*
* @param permissions The list of permissions the user has.
* @param requires The permissions required to perform the action.
* @returns A boolean indicating whether the user has all the required permissions.
*/
export function hasAllPermissions<T>(permissions: T[], ...requires: T[]) {
return requires.every((permission) => permissions.includes(permission));
}

/**
* Checks if the user has any of the required permissions.
*
* @param permissions The list of permissions the user has.
* @param requires The permissions required to perform the action.
* @returns A boolean indicating whether the user has any of the required permissions.
*/
export function hasAnyPermission<T>(permissions: T[], ...requires: T[]) {
return requires.some((permission) => permissions.includes(permission));
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { NoteService } from '../shared/features/note/+state/note.service';
import { ProjectSelectorService } from '../shared/features/project-selector/+state/project-selector.service';
import { TaskSelectorService } from '../shared/features/task-selector/+state/task-selector.service';
import { TeamSelectorService } from '../shared/features/team-selector/+state/team-selector.service';
import { hasAllPermissions } from '../shared/utils/permission.util';
import { TasksComponent } from '../tasks/tasks.component';
import { TimeTrackerQuery } from './+state/time-tracker.query';
import { IgnitionState, TimeTrackerStore } from './+state/time-tracker.store';
Expand Down Expand Up @@ -118,6 +119,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
private _remoteSleepLock = false;
private _isReady = false;
private _session: moment.Moment = null;
private hasActiveTaskPermissions = false;
@ViewChild('dialogOpenBtn') btnDialogOpen: ElementRef<HTMLElement>;
public start$: BehaviorSubject<boolean> = new BehaviorSubject(false);
userData: any;
Expand Down Expand Up @@ -204,9 +206,23 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
.pipe(
filter((permissions: any[]) => permissions.length > 0),
tap((permissions: any[]) => {
this.taskSelectorService.hasPermission = permissions.includes(PermissionsEnum.ORG_TASK_ADD);
this.clientSelectorService.hasPermission = permissions.includes(PermissionsEnum.ORG_CONTACT_EDIT);
this.projectSelectorService.hasPermission = permissions.includes(PermissionsEnum.ORG_PROJECT_ADD);
this.taskSelectorService.hasPermission = hasAllPermissions(
permissions,
PermissionsEnum.ORG_TASK_ADD
);
this.clientSelectorService.hasPermission = hasAllPermissions(
permissions,
PermissionsEnum.ORG_CONTACT_EDIT
);
this.projectSelectorService.hasPermission = hasAllPermissions(
permissions,
PermissionsEnum.ORG_PROJECT_ADD
);
this.hasActiveTaskPermissions = hasAllPermissions(
permissions,
PermissionsEnum.ORG_TEAM_EDIT_ACTIVE_TASK,
PermissionsEnum.ALL_ORG_EDIT
);
}),
untilDestroyed(this)
)
Expand Down Expand Up @@ -1687,34 +1703,24 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
this.electronService.ipcRenderer.send('stop-capture-screen');

if (this._startMode === TimerStartMode.MANUAL) {
console.log('Stopping timer');
const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config);
console.log('Taking screen capture');

this.start$.next(false);
const activities = await this.electronService.ipcRenderer.invoke('TAKE_SCREEN_CAPTURE', config);

this.loading = false;

console.log('Toggling timer');
await this._toggle(timer, onClick);
console.log('Sending activities');

asyncScheduler.schedule(async () => {
console.log('Taking screen capture');
const activities = await this.electronService.ipcRenderer.invoke('TAKE_SCREEN_CAPTURE', config);
await this.sendActivities(activities);
}

console.log('Sending activities');
await this.sendActivities(activities);
}, 1000);
} else {
console.log('Stopping timer');
const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config);
console.log('Stopping timer');
const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config);

this.start$.next(false);
console.log('Toggling timer');
await this._toggle(timer, onClick);

this.loading = false;
this.start$.next(false);

console.log('Toggling timer');
await this._toggle(timer, onClick);
}
this.loading = false;

console.log('Updating Tray stop');

Expand Down Expand Up @@ -2186,27 +2192,29 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
this.screenshots$.next([...this.screenshots, this.lastScreenCapture]);
}

// upload screenshot to TimeSlot api
try {
await Promise.all(
screenshotImg.map(async (img) => {
return await this.uploadsScreenshot(arg, img, resActivities.id);
})
);
} catch (error) {
console.log('ERROR', error);
}

const timeSlotId = resActivities.id;
asapScheduler.schedule(async () => {
console.log('upload screenshot to TimeSlot api');
try {
await Promise.all(
screenshotImg.map(async (img) => {
return await this.uploadsScreenshot(arg, img, resActivities.id);
})
);
} catch (error) {
console.log('ERROR', error);
}

console.log('Get last time slot image');
await this.getLastTimeSlotImage({
...arg,
token: this.token,
apiHost: this.apiHost,
timeSlotId
console.log('Get last time slot image');
await this.getLastTimeSlotImage({
...arg,
token: this.token,
apiHost: this.apiHost,
timeSlotId
});
});

const timeSlotId = resActivities.id;

console.log('Sending create-synced-interval event...');
this.electronService.ipcRenderer.send('create-synced-interval', {
...paramActivity,
Expand Down Expand Up @@ -2426,7 +2434,7 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {

public async updateOrganizationTeamEmployee(): Promise<void> {
try {
if (!this.selectedTask || !this.selectedTeam) {
if (!this.selectedTask || !this.selectedTeam || !this.hasActiveTaskPermissions) {
return;
}
const organizationTeamId = this.teamSelectorService.selectedId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ITaskStatus,
ITaskStatusFindInput,
ITaskUpdateInput,
ITimeLog,
TimeLogSourceEnum,
TimeLogType
} from '@gauzy/contracts';
Expand Down Expand Up @@ -325,27 +326,56 @@ export class TimeTrackerService {
}

toggleApiStop(values) {
const TIMEOUT = 15000;
const API_URL = `${API_PREFIX}/timesheet/timer/stop`;

// Destructuring with defaults
const {
organizationContactId = null,
organizationTeamId = null,
manualTimeSlot = null,
description = null,
projectId = null,
version = null,
taskId = null,
organizationId,
tenantId,
startedAt,
stoppedAt
} = values;

const options = {
headers: new HttpHeaders({ timeout: `${15 * 1000}` })
headers: new HttpHeaders({ timeout: TIMEOUT.toString() })
};

const body = {
description: values.description,
description,
isBillable: true,
logType: TimeLogType.TRACKED,
projectId: values.projectId,
taskId: values.taskId,
manualTimeSlot: values.manualTimeSlot,
organizationId: values.organizationId,
tenantId: values.tenantId,
organizationContactId: values.organizationContactId,
source: TimeLogSourceEnum.DESKTOP,
projectId,
taskId,
manualTimeSlot,
organizationId,
tenantId,
organizationContactId,
isRunning: false,
version: values.version,
startedAt: moment(values.startedAt).utc().toISOString(),
stoppedAt: moment(values.stoppedAt).utc().toISOString(),
organizationTeamId: values.organizationTeamId
version,
startedAt: moment(startedAt).utc().toISOString(),
stoppedAt: moment(stoppedAt).utc().toISOString(),
organizationTeamId
};
this._loggerService.log.info(`Toggle Stop Timer Request: ${moment().format()}`, body);
return firstValueFrom(this.http.post(`${API_PREFIX}/timesheet/timer/stop`, { ...body }, options));

// Log request details
this._loggerService.info<any>(`Toggle Stop Timer Request: ${moment().format()}`, body);

// Perform the API call
try {
return firstValueFrom(this.http.post<ITimeLog>(API_URL, body, options));
} catch (error) {
this._loggerService.error<any>(`Error stopping timer: ${moment().format()}`, { error, requestBody: body });
throw error;
}
}

deleteTimeSlot(values) {
Expand Down Expand Up @@ -448,6 +478,7 @@ export class TimeTrackerService {
}

uploadImages(values, img: any) {
const TIMEOUT = 60 * 1000; // Max 60 sec to upload images
const formData = new FormData();
const contentType = 'image/png';
const b64Data = img.b64Img;
Expand All @@ -457,8 +488,13 @@ export class TimeTrackerService {
formData.append('tenantId', values.tenantId);
formData.append('organizationId', values.organizationId);
formData.append('recordedAt', moment(values.recordedAt).utc().toISOString());

const options = {
headers: new HttpHeaders({ timeout: TIMEOUT.toString() })
};

return firstValueFrom(
this.http.post(`${API_PREFIX}/timesheet/screenshot`, formData).pipe(
this.http.post(`${API_PREFIX}/timesheet/screenshot`, formData, options).pipe(
catchError((error) => {
error.error = {
...error.error,
Expand Down Expand Up @@ -593,7 +629,9 @@ export class TimeTrackerService {
organizationTeamId: values.organizationTeamId,
tenantId: values.tenantId
};
return firstValueFrom(this.http.put(`${API_PREFIX}/organization-team-employee/${employeeId}`, params));
return firstValueFrom(
this.http.put(`${API_PREFIX}/organization-team-employee/${employeeId}/active-task`, params)
);
}

public async getTeams(values?: any): Promise<IOrganizationTeam[]> {
Expand Down

0 comments on commit eeba5f9

Please sign in to comment.