diff --git a/.cspell.json b/.cspell.json
index c6e80efd8..56963e340 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -187,6 +187,8 @@
"Kolkata",
"Kosrae",
"Koyeb",
+ "Krisp",
+ "krisp",
"labore",
"Lask",
"lastest",
@@ -271,6 +273,7 @@
"sentryclirc",
"setrole",
"Settingfilter",
+ "settingsCloseButton",
"setuptools",
"setwin",
"setwork",
diff --git a/apps/web/app/[locale]/meet/livekit/component.tsx b/apps/web/app/[locale]/meet/livekit/component.tsx
index 54a991bed..f362623f0 100644
--- a/apps/web/app/[locale]/meet/livekit/component.tsx
+++ b/apps/web/app/[locale]/meet/livekit/component.tsx
@@ -20,6 +20,7 @@ function LiveKitPage() {
const params = useSearchParams();
const onLeave = useCallback(() => {
+ window.localStorage.removeItem('current-room-live-kit');
router.push('/');
}, [router]);
@@ -27,6 +28,7 @@ function LiveKitPage() {
const room = params.get("roomName");
if (room) {
setRoomName(room);
+ window.localStorage.setItem('current-room-live-kit', room);
}
}, [params]);
@@ -36,7 +38,7 @@ function LiveKitPage() {
});
return (
- <>
+
{token && roomName && }
- >
+
);
}
diff --git a/apps/web/app/hooks/features/useManualTime.ts b/apps/web/app/hooks/features/useManualTime.ts
new file mode 100644
index 000000000..ba5127675
--- /dev/null
+++ b/apps/web/app/hooks/features/useManualTime.ts
@@ -0,0 +1,38 @@
+import { useCallback, useState } from 'react';
+import { useQuery } from '../useQuery';
+import { useAuthenticateUser } from './useAuthenticateUser';
+import { addManualTimeRequestAPI } from '@app/services/client/api/timer/manual-time';
+import { IAddManualTimeRequest, ITimeLog } from '@app/interfaces/timer/ITimerLogs';
+import { TimeLogType, TimerSource } from '@app/interfaces';
+
+export function useManualTime() {
+ const { user } = useAuthenticateUser();
+
+ const { loading: addManualTimeLoading, queryCall: queryAddManualTime } = useQuery(addManualTimeRequestAPI);
+ const [timeLog, setTimeLog] = useState();
+
+ const addManualTime = useCallback(
+ (data: Omit) => {
+ queryAddManualTime({
+ tenantId: user?.tenantId ?? '',
+ employeeId: user?.employee.id ?? '',
+ logType: TimeLogType.MANUAL,
+ source: TimerSource.BROWSER,
+ ...data
+ })
+ .then((response) => {
+ setTimeLog(response.data);
+ })
+ .catch((error) => {
+ console.log(error);
+ });
+ },
+ [queryAddManualTime, user?.employee.id, user?.tenantId]
+ );
+
+ return {
+ addManualTimeLoading,
+ addManualTime,
+ timeLog
+ };
+}
diff --git a/apps/web/app/hooks/features/useStartStopTimerHandler.ts b/apps/web/app/hooks/features/useStartStopTimerHandler.ts
index 99ee3e566..9760a68c0 100644
--- a/apps/web/app/hooks/features/useStartStopTimerHandler.ts
+++ b/apps/web/app/hooks/features/useStartStopTimerHandler.ts
@@ -62,11 +62,34 @@ export function useStartStopTimerHandler() {
window && window?.localStorage.getItem(DAILY_PLAN_ESTIMATE_HOURS_MODAL_DATE);
/**
- * Handle missing working hour for a daily plN
+ * Handle missing working hour for a daily plan
*/
const handleMissingDailyPlanWorkHour = () => {
- if (!hasWorkedHours) {
- openAddDailyPlanWorkHoursModal();
+ if (hasPlan) {
+ if (!hasWorkedHours) {
+ openAddDailyPlanWorkHoursModal();
+ } else {
+ startTimer();
+ }
+ } else {
+ startTimer();
+ }
+ };
+
+ /**
+ * Handle missing estimation hours for tasks
+ */
+ const handleMissingTasksEstimationHours = () => {
+ if (hasPlan) {
+ if (areAllTasksEstimated) {
+ if (dailyPlanEstimateHoursModalDate != currentDate) {
+ handleMissingDailyPlanWorkHour();
+ } else {
+ startTimer();
+ }
+ } else {
+ openAddTasksEstimationHoursModal();
+ }
} else {
startTimer();
}
@@ -90,20 +113,20 @@ export function useStartStopTimerHandler() {
startTimer();
} else {
if (dailyPlanSuggestionModalDate != currentDate) {
- openSuggestDailyPlanModal();
+ if (!hasPlan) {
+ openSuggestDailyPlanModal();
+ } else {
+ handleMissingTasksEstimationHours();
+ }
} else if (tasksEstimateHoursModalDate != currentDate) {
- if (areAllTasksEstimated) {
- if (dailyPlanEstimateHoursModalDate != currentDate) {
+ handleMissingTasksEstimationHours();
+ } else if (dailyPlanEstimateHoursModalDate != currentDate) {
+ if (hasPlan) {
+ if (areAllTasksEstimated) {
handleMissingDailyPlanWorkHour();
} else {
startTimer();
}
- } else {
- openAddTasksEstimationHoursModal();
- }
- } else if (dailyPlanEstimateHoursModalDate != currentDate) {
- if (areAllTasksEstimated) {
- handleMissingDailyPlanWorkHour();
} else {
startTimer();
}
@@ -116,6 +139,7 @@ export function useStartStopTimerHandler() {
}, [
areAllTasksEstimated,
canRunTimer,
+ hasPlan,
hasWorkedHours,
isActiveTaskPlaned,
openAddDailyPlanWorkHoursModal,
diff --git a/apps/web/app/interfaces/ITimer.ts b/apps/web/app/interfaces/ITimer.ts
index 996cad947..dc68def2d 100644
--- a/apps/web/app/interfaces/ITimer.ts
+++ b/apps/web/app/interfaces/ITimer.ts
@@ -33,6 +33,13 @@ export enum TimerSource {
'TEAMS' = 'TEAMS'
}
+export enum TimeLogType {
+ TRACKED = 'TRACKED',
+ MANUAL = 'MANUAL',
+ IDLE = 'IDLE',
+ RESUMED = 'RESUMED'
+}
+
export interface ITimerStatus {
duration: number;
lastLog?: ITimer;
diff --git a/apps/web/app/interfaces/timer/ITimerLogs.ts b/apps/web/app/interfaces/timer/ITimerLogs.ts
index 73e1f3a88..5437d922c 100644
--- a/apps/web/app/interfaces/timer/ITimerLogs.ts
+++ b/apps/web/app/interfaces/timer/ITimerLogs.ts
@@ -1,3 +1,9 @@
+import { IEmployee } from '../IEmployee';
+import { IOrganization } from '../IOrganization';
+import { ITeamTask } from '../ITask';
+import { TimeLogType, TimerSource } from '../ITimer';
+import { ITimerSlot } from './ITimerSlot';
+
export interface ITimerLogsDailyReportRequest {
tenantId: string;
organizationId: string;
@@ -11,3 +17,43 @@ export interface ITimerLogsDailyReport {
date: string; // '2024-07-19'
sum: number; // in seconds
}
+
+export interface IAddManualTimeRequest {
+ employeeId: string;
+ projectId?: string;
+ taskId?: string;
+ organizationContactId?: string;
+ description?: string;
+ reason?: string;
+ startedAt: Date;
+ stoppedAt: Date;
+ editedAt?: Date;
+ tags?: string[];
+ isBillable?: boolean;
+ organizationId?: string;
+ organization?: Pick;
+ tenantId?: string;
+ logType: TimeLogType;
+ source: TimerSource.BROWSER;
+}
+
+export interface ITimeLog {
+ employee: IEmployee;
+ employeeId: string;
+ timesheetId?: string;
+ task?: ITeamTask;
+ taskId?: string;
+ timeSlots?: ITimerSlot[];
+ projectId?: string;
+ startedAt?: Date;
+ stoppedAt?: Date;
+ /** Edited At* */
+ editedAt?: Date;
+ description?: string;
+ reason?: string;
+ duration: number;
+ isBillable: boolean;
+ tags?: string[];
+ isRunning?: boolean;
+ isEdited?: boolean;
+}
diff --git a/apps/web/app/services/client/api/timer/manual-time.ts b/apps/web/app/services/client/api/timer/manual-time.ts
new file mode 100644
index 000000000..9843d61f4
--- /dev/null
+++ b/apps/web/app/services/client/api/timer/manual-time.ts
@@ -0,0 +1,13 @@
+import { post } from '@app/services/client/axios';
+import { IAddManualTimeRequest, ITimeLog } from '@app/interfaces/timer/ITimerLogs';
+
+export async function addManualTimeRequestAPI(request: IAddManualTimeRequest) {
+ const { startedAt, stoppedAt, ...rest } = request;
+ const data = {
+ ...rest,
+ startedAt: startedAt.toISOString(),
+ stoppedAt: stoppedAt.toISOString()
+ };
+
+ return post(`/timesheet/time-log`, data);
+}
diff --git a/apps/web/components/shared/collaborate/index.tsx b/apps/web/components/shared/collaborate/index.tsx
index 66b9115e6..47d382d64 100644
--- a/apps/web/components/shared/collaborate/index.tsx
+++ b/apps/web/components/shared/collaborate/index.tsx
@@ -55,6 +55,15 @@ const Collaborate = () => {
[collaborativeMembers, members, setCollaborativeMembers]
);
+ const handleAction = (actionCallback: () => void) => () => {
+ closeModal();
+ actionCallback();
+ };
+
+ const handleMeetClick = handleAction(onMeetClick);
+ const handleBoardClick = handleAction(onBoardClick);
+
+
return (
@@ -197,10 +206,7 @@ const Collaborate = () => {
-
+
-
+
diff --git a/apps/web/lib/features/integrations/livekit/index.tsx b/apps/web/lib/features/integrations/livekit/index.tsx
index c0a84684d..bd9ecf963 100644
--- a/apps/web/lib/features/integrations/livekit/index.tsx
+++ b/apps/web/lib/features/integrations/livekit/index.tsx
@@ -8,6 +8,7 @@ import {
} from '@livekit/components-react';
import { RoomConnectOptions } from 'livekit-client';
import "@livekit/components-styles";
+import { SettingsMenu } from './settings-livekit';
type ActiveRoomProps = {
userChoices: LocalUserChoices;
@@ -30,7 +31,7 @@ export default function LiveKitPage({
const LiveKitRoomComponent = LiveKitRoom as React.ElementType;
return (
);
diff --git a/apps/web/lib/features/integrations/livekit/settings-livekit.tsx b/apps/web/lib/features/integrations/livekit/settings-livekit.tsx
new file mode 100644
index 000000000..7ef3f09ef
--- /dev/null
+++ b/apps/web/lib/features/integrations/livekit/settings-livekit.tsx
@@ -0,0 +1,174 @@
+'use client';
+import * as React from 'react';
+import { LocalAudioTrack, Track } from 'livekit-client';
+import {
+ useMaybeLayoutContext,
+ useLocalParticipant,
+ MediaDeviceMenu,
+ TrackToggle,
+
+} from '@livekit/components-react';
+import { Button } from '@components/ui/button';
+import { GoCopy } from "react-icons/go";
+import styles from '../../../../styles/settings.module.css'
+import { shortenLink } from 'lib/utils';
+import { BiLoaderCircle } from "react-icons/bi";
+
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SettingsMenuProps extends React.HTMLAttributes { }
+
+export function SettingsMenu(props: SettingsMenuProps) {
+ const TrackToggleComponent = TrackToggle as React.ElementType;
+
+ const layoutContext = useMaybeLayoutContext();
+ const [copied, setCopied] = React.useState(false);
+
+ const getTeamLink = React.useCallback(() => {
+ if (typeof window !== 'undefined') {
+ return `${window.location.origin}/meet/livekit?roomName=${window.localStorage.getItem('current-room-live-kit')}`;
+ }
+ return '';
+ }, []);
+
+ const settings = React.useMemo(() => {
+ return {
+ media: { camera: true, microphone: true, label: 'Media Devices', speaker: true },
+ effects: { label: 'Effects' },
+ };
+ }, []);
+
+ const tabs = React.useMemo(
+ () => Object.keys(settings) as Array,
+ [settings],
+ );
+ const { microphoneTrack } = useLocalParticipant();
+ const [activeTab, setActiveTab] = React.useState(tabs[0]);
+ const [isNoiseFilterEnabled, setIsNoiseFilterEnabled] = React.useState(true);
+ React.useEffect(() => {
+ const micPublication = microphoneTrack;
+ if (micPublication && micPublication.track instanceof LocalAudioTrack) {
+ const currentProcessor = micPublication.track.getProcessor();
+ if (currentProcessor && !isNoiseFilterEnabled) {
+ micPublication.track.stopProcessor();
+ } else if (!currentProcessor && isNoiseFilterEnabled) {
+ import('@livekit/krisp-noise-filter')
+ .then(({ KrispNoiseFilter, isKrispNoiseFilterSupported }) => {
+ if (!isKrispNoiseFilterSupported()) {
+ console.error('Enhanced noise filter is not supported for this browser');
+ setIsNoiseFilterEnabled(false);
+ return;
+ }
+ micPublication?.track
+ // @ts-ignore
+ ?.setProcessor(KrispNoiseFilter())
+ .then(() => console.log('successfully set noise filter'));
+ })
+ .catch((e) => console.error('Failed to load noise filter', e));
+ }
+ }
+ }, [isNoiseFilterEnabled, microphoneTrack]);
+
+ return (
+
+
+ {tabs.map(
+ (tab) =>
+ settings[tab] && (
+
+ ),
+ )}
+
+
+ {activeTab === 'media' && (
+ <>
+ {settings.media && settings.media.camera && (
+ <>
+
Camera
+
+ >
+ )}
+ {settings.media && settings.media.microphone && (
+ <>
+
Microphone
+
+ >
+ )}
+ {settings.media && settings.media.speaker && (
+ <>
+
Speaker & Headphones
+
+ >
+ )}
+ >
+ )}
+ {activeTab === 'effects' && (
+ <>
+
Audio
+
+ >
+ )}
+
+
+
+ You can invite your colleagues to join the meeting by sharing this link.
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/lib/features/manual-time/add-manual-time-modal.tsx b/apps/web/lib/features/manual-time/add-manual-time-modal.tsx
index 8137c7da1..cf8c1b4b9 100644
--- a/apps/web/lib/features/manual-time/add-manual-time-modal.tsx
+++ b/apps/web/lib/features/manual-time/add-manual-time-modal.tsx
@@ -1,6 +1,6 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
import '../../../styles/style.css';
import { useOrganizationTeams, useTeamTasks } from '@app/hooks';
-import api from '@app/services/client/axios';
import { clsxm } from '@app/utils';
import { DatePicker } from '@components/ui/DatePicker';
import { PencilSquareIcon } from '@heroicons/react/20/solid';
@@ -11,6 +11,9 @@ import { FaRegCalendarAlt } from 'react-icons/fa';
import { HiMiniClock } from 'react-icons/hi2';
import { manualTimeReasons } from '@app/constants';
import { useTranslations } from 'next-intl';
+import { IOrganizationTeamList } from '@app/interfaces';
+import { useManualTime } from '@app/hooks/features/useManualTime';
+import { IAddManualTimeRequest } from '@app/interfaces/timer/ITimerLogs';
interface IAddManualTimeModalProps {
isOpen: boolean;
@@ -23,17 +26,18 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
const [isBillable, setIsBillable] = useState(false);
const [description, setDescription] = useState('');
const [reason, setReason] = useState('');
- const [errorMsg, setError] = useState('');
- const [loading, setLoading] = useState(false);
+ const [errorMsg, setErrorMsg] = useState('');
const [endTime, setEndTime] = useState('');
const [date, setDate] = useState(new Date());
const [startTime, setStartTime] = useState('');
- const [teamId, setTeamId] = useState('');
+ const [team, setTeam] = useState();
const [taskId, setTaskId] = useState('');
const [timeDifference, setTimeDifference] = useState('');
- const { activeTeamTask, tasks, activeTeamId, activeTeam } = useTeamTasks();
+ const { activeTeamTask, tasks, activeTeam } = useTeamTasks();
const { teams } = useOrganizationTeams();
+ const { addManualTime, addManualTimeLoading, timeLog } = useManualTime();
+
useEffect(() => {
const now = new Date();
const currentTime = now.toTimeString().slice(0, 5);
@@ -47,40 +51,38 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
(e: FormEvent) => {
e.preventDefault();
- const timeObject = {
- date,
- isBillable,
- startTime,
- endTime,
- teamId,
+ const startedAt = new Date(date);
+ const stoppedAt = new Date(date);
+
+ // Set time for the started date
+ startedAt.setHours(parseInt(startTime.split(':')[0]));
+ startedAt.setMinutes(parseInt(startTime.split(':')[1]));
+
+ // Set time for the stopped date
+ stoppedAt.setHours(parseInt(endTime.split(':')[0]));
+ stoppedAt.setMinutes(parseInt(endTime.split(':')[1]));
+
+ const requestData: Omit = {
+ startedAt,
+ stoppedAt,
taskId,
description,
reason,
- timeDifference
+ isBillable,
+ organizationId: team?.organizationId
};
- if (date && startTime && endTime && teamId && taskId) {
- setLoading(true);
- setError('');
- const postData = async () => {
- try {
- const response = await api.post('/add_time', timeObject);
- if (response.data.message) {
- setLoading(false);
- closeModal();
- }
- } catch (err) {
- setError('Failed to post data');
- setLoading(false);
- }
- };
-
- postData();
+ if (date && startTime && endTime && team && taskId) {
+ if (endTime > startTime) {
+ addManualTime(requestData); // [TODO : api] Allow team member to add manual time as well
+ } else {
+ setErrorMsg('End time should be after than start time');
+ }
} else {
- setError(`Please complete all required fields with a ${'*'}`);
+ setErrorMsg("Please complete all required fields with a '*'");
}
},
- [closeModal, date, description, endTime, isBillable, reason, startTime, taskId, teamId, timeDifference]
+ [addManualTime, date, description, endTime, isBillable, reason, startTime, taskId, team]
);
const calculateTimeDifference = useCallback(() => {
@@ -108,10 +110,18 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
if (activeTeamTask) {
setTaskId(activeTeamTask.id);
}
- if (activeTeamId) {
- setTeamId(activeTeamId);
+ if (activeTeam) {
+ setTeam(activeTeam);
+ }
+ }, [activeTeamTask, activeTeam]);
+
+ useEffect(() => {
+ if (!addManualTimeLoading && timeLog) {
+ closeModal();
+ setDescription('');
+ setErrorMsg('');
}
- }, [activeTeamTask, activeTeamId]);
+ }, [addManualTimeLoading, closeModal, timeLog]);
return (
*
setTeamId(value ? value.id : '')}
+ onValueChange={(team) => setTeam(team)}
itemId={(team) => (team ? team.id : '')}
itemToString={(team) => (team ? team.name : '')}
triggerClassName="border-slate-100 dark:border-slate-600"
@@ -238,10 +248,10 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
Task*
setTaskId(value ? value.id : '')}
+ onValueChange={(task) => setTaskId(task ? task.id : '')}
itemId={(task) => (task ? task.id : '')}
- defaultValue={activeTeamTask}
itemToString={(task) => (task ? task.title : '')}
triggerClassName="border-slate-100 dark:border-slate-600"
/>
@@ -278,8 +288,8 @@ export function AddManualTimeModal(props: IAddManualTimeModalProps) {
View timesheet