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

+
+ Camera +
+ +
+
+ + )} + {settings.media && settings.media.microphone && ( + <> +

Microphone

+
+ Microphone +
+ +
+
+ + )} + {settings.media && settings.media.speaker && ( + <> +

Speaker & Headphones

+
+ Audio Output +
+ +
+
+ + )} + + )} + {activeTab === 'effects' && ( + <> +

Audio

+
+ + setIsNoiseFilterEnabled(ev.target.checked)} + checked={isNoiseFilterEnabled} + > +
+ + )} +
+
+
+ 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 + {showEditAndSaveButton && ( +
+ {!updateLoading ? ( + editableMode ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} -
+ + )} + + )} ); } diff --git a/apps/web/lib/features/team/user-team-card/task-estimate.tsx b/apps/web/lib/features/team/user-team-card/task-estimate.tsx index 3fee804ca..e33188e37 100644 --- a/apps/web/lib/features/team/user-team-card/task-estimate.tsx +++ b/apps/web/lib/features/team/user-team-card/task-estimate.tsx @@ -60,7 +60,12 @@ export function TaskEstimateInput({ memberInfo, edition }: Omit {task && ( <> - +