diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index 1b7659de6..47a35fd06 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -285,7 +285,7 @@ export const TASKS_ESTIMATE_HOURS_MODAL_DATE = 'tasks-estimate-hours-modal-date' export const DAILY_PLAN_ESTIMATE_HOURS_MODAL_DATE = 'daily-plan-estimate-hours-modal'; export const DEFAULT_PLANNED_TASK_ID = 'default-planned-task-id'; export const LAST_OPTION__CREATE_DAILY_PLAN_MODAL = 'last-option--create-daily-plan-modal'; -export const HAS_VISITED_OUTSTANDING_TAB = 'has-visited-outstanding-tab'; +export const HAS_VISITED_OUTSTANDING_TASKS = 'has-visited-outstanding-tasks'; // OAuth provider's keys diff --git a/apps/web/app/hooks/features/useDailyPlan.ts b/apps/web/app/hooks/features/useDailyPlan.ts index af285b25b..f7b1d4d7d 100644 --- a/apps/web/app/hooks/features/useDailyPlan.ts +++ b/apps/web/app/hooks/features/useDailyPlan.ts @@ -147,9 +147,11 @@ export function useDailyPlan() { createQueryCall, employeePlans, getMyDailyPlans, - profileDailyPlans, + profileDailyPlans.items, + profileDailyPlans.total, setEmployeePlans, setProfileDailyPlans, + user?.employee?.id, user?.tenantId ] ); diff --git a/apps/web/lib/components/accordian.tsx b/apps/web/lib/components/accordian.tsx index 47602ee1f..9cd7275dc 100644 --- a/apps/web/lib/components/accordian.tsx +++ b/apps/web/lib/components/accordian.tsx @@ -30,7 +30,7 @@ export const Accordian = ({ children, title, className, isDanger, id, defaultOpe diff --git a/apps/web/lib/components/modal.tsx b/apps/web/lib/components/modal.tsx index 33ea9eed9..84985fc01 100644 --- a/apps/web/lib/components/modal.tsx +++ b/apps/web/lib/components/modal.tsx @@ -11,6 +11,7 @@ type Props = { description?: string; isOpen: boolean; closeModal: () => void; + customCloseModal?: () => void; className?: string; alignCloseIcon?: boolean; showCloseIcon?: boolean; @@ -19,6 +20,7 @@ type Props = { export function Modal({ isOpen, closeModal, + customCloseModal, children, title, titleClass, @@ -54,7 +56,10 @@ export function Modal({ {description && {description}} {showCloseIcon && (
{ + closeModal(); + customCloseModal?.(); + }} className={`absolute ${ alignCloseIcon ? 'right-2 top-3' : 'right-3 top-3' } md:right-2 md:top-3 cursor-pointer z-50`} diff --git a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx index 1413d496d..86ce1bf6e 100644 --- a/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx +++ b/apps/web/lib/features/daily-plan/add-task-estimation-hours-modal.tsx @@ -8,7 +8,7 @@ import { useTranslations } from 'next-intl'; import { useAuthenticateUser, useDailyPlan, useModal, useTaskStatus, useTeamTasks, useTimerView } from '@app/hooks'; import { TaskNameInfoDisplay } from '../task/task-displays'; import { TaskEstimate } from '../task/task-estimate'; -import { IDailyPlan, ITeamTask } from '@app/interfaces'; +import { DailyPlanStatusEnum, IDailyPlan, ITeamTask } from '@app/interfaces'; import clsx from 'clsx'; import { AddIcon, ThreeCircleOutlineVerticalIcon } from 'assets/svg'; import { estimatedTotalTime } from '../task/daily-plan'; @@ -22,6 +22,7 @@ import { ScrollArea, ScrollBar } from '@components/ui/scroll-bar'; import { Cross2Icon } from '@radix-ui/react-icons'; import { checkPastDate } from 'lib/utils'; import { UnplanActiveTaskModal } from './unplan-active-task-modal'; +import moment from 'moment'; /** * A modal that allows user to add task estimation / planned work time, etc. @@ -32,19 +33,21 @@ import { UnplanActiveTaskModal } from './unplan-active-task-modal'; * @param {IDailyPlan} props.plan - The selected plan * @param {ITeamTask[]} props.tasks - The list of planned tasks * @param {boolean} props.isRenderedInSoftFlow - If true use the soft flow logic. + * @param {Date} props.selectedDate - A date on which the user can create the plan * * @returns {JSX.Element} The modal element */ interface IAddTasksEstimationHoursModalProps { closeModal: () => void; isOpen: boolean; - plan: IDailyPlan; + plan?: IDailyPlan; tasks: ITeamTask[]; isRenderedInSoftFlow?: boolean; + selectedDate?: Date; } export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModalProps) { - const { isOpen, closeModal, plan, tasks, isRenderedInSoftFlow = true } = props; + const { isOpen, closeModal, plan, tasks, isRenderedInSoftFlow = true, selectedDate } = props; const { isOpen: isActiveTaskHandlerModalOpen, closeModal: closeActiveTaskHandlerModal, @@ -56,25 +59,30 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa const { startTimer, timerStatus } = useTimerView(); const { activeTeam, activeTeamTask, setActiveTask } = useTeamTasks(); const [showSearchInput, setShowSearchInput] = useState(false); - const [workTimePlanned, setWorkTimePlanned] = useState(plan.workTimePlanned); + const [workTimePlanned, setWorkTimePlanned] = useState(plan?.workTimePlanned ?? 0); const currentDate = useMemo(() => new Date().toISOString().split('T')[0], []); const requirePlan = useMemo(() => activeTeam?.requirePlanToTrack, [activeTeam?.requirePlanToTrack]); - const tasksEstimationTimes = useMemo(() => estimatedTotalTime(plan.tasks).timesEstimated / 3600, [plan.tasks]); + const tasksEstimationTimes = useMemo( + () => (plan && plan.tasks ? estimatedTotalTime(plan.tasks).timesEstimated / 3600 : 0), + [plan] + ); const totalWorkedTime = useMemo( () => - plan.tasks?.reduce((acc, cur) => { - const totalWorkedTime = cur.totalWorkedTime ?? 0; + plan && plan.tasks + ? plan.tasks.reduce((acc, cur) => { + const totalWorkedTime = cur.totalWorkedTime ?? 0; - return acc + totalWorkedTime; - }, 0), + return acc + totalWorkedTime; + }, 0) + : 0, [plan] ); const [warning, setWarning] = useState(''); const [loading, setLoading] = useState(false); const [defaultTask, setDefaultTask] = useState(null); const isActiveTaskPlanned = useMemo( - () => plan.tasks?.some((task) => task.id == activeTeamTask?.id), - [activeTeamTask?.id, plan.tasks] + () => plan && plan.tasks && plan.tasks.some((task) => task.id == activeTeamTask?.id), + [activeTeamTask?.id, plan] ); const [isWorkingTimeInputFocused, setWorkingTimeInputFocused] = useState(false); const [planEditState, setPlanEditState] = useState<{ draft: boolean; saved: boolean }>({ @@ -84,11 +92,11 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa const canStartWorking = useMemo(() => { const isTodayPlan = - new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); + plan && new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); return isTodayPlan; // Can add others conditions - }, [plan.date]); + }, [plan]); const handleCloseModal = useCallback(() => { if (canStartWorking) { @@ -142,7 +150,9 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa setLoading(true); // Update the plan work time only if the user changed it - plan.workTimePlanned !== workTimePlanned && (await updateDailyPlan({ workTimePlanned }, plan.id ?? '')); + plan && + plan.workTimePlanned !== workTimePlanned && + (await updateDailyPlan({ workTimePlanned }, plan.id ?? '')); setPlanEditState({ draft: false, saved: true }); @@ -159,8 +169,7 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa setLoading(false); } }, [ - plan.workTimePlanned, - plan.id, + plan, workTimePlanned, updateDailyPlan, canStartWorking, @@ -189,17 +198,24 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa // Handle warning messages useEffect(() => { - if (!workTimePlanned || workTimePlanned <= 0) { - setWarning(t('dailyPlan.planned_tasks_popup.warning.PLANNED_TIME')); - } else if (plan.tasks?.find((task) => !task.estimate)) { + // First, check if there are tasks without estimates and show the corresponding warning + if (plan?.tasks?.find((task) => !task.estimate)) { setWarning(t('dailyPlan.planned_tasks_popup.warning.TASKS_ESTIMATION')); - } else if (Math.abs(workTimePlanned - tasksEstimationTimes) > 1) { + } + // Next, check if no work time is planned or if planned time is invalid + else if (!workTimePlanned || workTimePlanned <= 0) { + setWarning(t('dailyPlan.planned_tasks_popup.warning.PLANNED_TIME')); + } + // If the difference between planned and estimated times is significant, check further + else if (Math.abs(workTimePlanned - tasksEstimationTimes) > 1) { checkPlannedAndEstimateTimeDiff(); - } else { + } + // If all checks pass, clear the warning + else { setWarning(''); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [workTimePlanned, tasksEstimationTimes, plan.tasks, myDailyPlans]); + }, [workTimePlanned, tasksEstimationTimes, plan?.tasks, myDailyPlans]); // Put tasks without estimates at the top of the list const sortedTasks = useMemo( @@ -238,17 +254,23 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa // Update the working planned time useEffect(() => { - setWorkTimePlanned(plan.workTimePlanned); - }, [plan.id, plan.workTimePlanned]); + setWorkTimePlanned(plan?.workTimePlanned ?? 0); + }, [plan?.id, plan?.workTimePlanned]); const StartWorkingButton = ( ); + // TODO: Add onclick handler + const TimeSheetsButton = ( + + ); + const content = (
@@ -281,16 +310,17 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa setDefaultTask={setDefaultTask} setShowSearchInput={setShowSearchInput} selectedPlan={plan} + selectedDate={selectedDate} /> - ) : ( + ) : plan || selectedDate ? (
- {checkPastDate(plan.date) ? ( + {checkPastDate(plan?.date ?? selectedDate) ? ( {t('dailyPlan.PLANNED_TIME')} ) : ( @@ -299,9 +329,13 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa )}
- {checkPastDate(plan.date) ? ( + {checkPastDate(plan?.date ?? selectedDate) ? (
- {formatIntegerToHour(tasksEstimationTimes)} + {/**Create a space between hours and minutes for past plans view */} + {formatIntegerToHour(plan?.workTimePlanned ?? 0).replace( + /(\d+h)(\d+m)/, + '$1 $2' + )}
) : ( setWorkingTimeInputFocused(true)} onBlur={() => setWorkingTimeInputFocused(false)} defaultValue={ - plan.workTimePlanned ? parseInt(plan.workTimePlanned.toString()) : 0 + plan && plan.workTimePlanned ? parseInt(plan.workTimePlanned.toString()) : 0 } - readOnly={checkPastDate(plan.date)} + readOnly={checkPastDate(plan?.date ?? selectedDate)} /> )} - {!checkPastDate(plan.date) && ( + {!checkPastDate(plan?.date ?? selectedDate) && (
- {checkPastDate(plan.date) && ( + {checkPastDate(plan?.date ?? selectedDate) && (
- Tracked time + {t('common.plan.TRACKED_TIME')}
- {formatIntegerToHour(totalWorkedTime ?? 0)} + {formatIntegerToHour(totalWorkedTime ?? 0).replace(/(\d+h)(\d+m)/, '$1 $2')}
)}
- )} + ) : null} -
-
-
-
-
- {t('task.TITLE_PLURAL')} - {!checkPastDate(plan.date) && *} + {plan ? ( + <> +
+
+
+
+
+ {t('task.TITLE_PLURAL')} + {!checkPastDate(plan.date) && *} +
+
+ {checkPastDate(plan.date) ? ( + <> + {t('dailyPlan.ESTIMATED')} : + + {formatIntegerToHour(tasksEstimationTimes).replace( + /(\d+h)(\d+m)/, + '$1 $2' + )} + + + ) : ( + <> + {t('dailyPlan.TOTAL_ESTIMATED')} : + + {formatIntegerToHour(tasksEstimationTimes)} + + + )} +
+
+
+ +
    + {sortedTasks.map((task, index) => ( + + ))} +
+ +
+
-
- {checkPastDate(plan.date) ? ( - {t('dailyPlan.ESTIMATED')} : - ) : ( - {t('dailyPlan.TOTAL_ESTIMATED')} : +
+ {!checkPastDate(plan.date) && warning && ( + <> + + {warning} + )} - {formatIntegerToHour(tasksEstimationTimes)}
-
- -
    - {sortedTasks.map((task, index) => ( - - ))} -
- -
-
-
- {!checkPastDate(plan.date) && warning && ( - <> - - {warning} - +
+ + {canStartWorking && timerStatus?.running && !planEditState.draft ? ( + + {StartWorkingButton} + + ) : ( +
+ {checkPastDate(plan.date) ? TimeSheetsButton : StartWorkingButton} +
)}
-
-
-
- - {timerStatus?.running ? ( - - {StartWorkingButton} - - ) : ( -
{StartWorkingButton}
- )} -
+ + ) : null}
); @@ -465,10 +520,11 @@ export function AddTasksEstimationHoursModal(props: IAddTasksEstimationHoursModa */ interface ISearchTaskInputProps { - selectedPlan: IDailyPlan; + selectedPlan?: IDailyPlan; setShowSearchInput: Dispatch>; setDefaultTask: Dispatch>; defaultTask: ITeamTask | null; + selectedDate?: Date; } /** @@ -479,11 +535,12 @@ interface ISearchTaskInputProps { * @param {Dispatch>} props.setShowSearchInput - A setter for (showing / hiding) the input * @param {Dispatch>} props.setDefaultTask - A function that sets default planned task * @param {ITeamTask} props.defaultTask - The default planned task + * @param {Date} props.selectedDate - A date on which the user can create the plan * * @returns The Search input component */ export function SearchTaskInput(props: ISearchTaskInputProps) { - const { selectedPlan, setShowSearchInput, defaultTask, setDefaultTask } = props; + const { selectedPlan, setShowSearchInput, defaultTask, setDefaultTask, selectedDate } = props; const { tasks: teamTasks, createTask } = useTeamTasks(); const { taskStatus } = useTaskStatus(); const [taskName, setTaskName] = useState(''); @@ -497,9 +554,9 @@ export function SearchTaskInput(props: ISearchTaskInputProps) { const isTaskPlanned = useCallback( (taskId: string) => { - return selectedPlan?.tasks?.some((task) => task.id == taskId); + return selectedPlan && selectedPlan?.tasks?.some((task) => task.id == taskId); }, - [selectedPlan.tasks] + [selectedPlan] ); useEffect(() => { @@ -517,7 +574,7 @@ export function SearchTaskInput(props: ISearchTaskInputProps) { } }) ); - }, [isTaskPlanned, selectedPlan.tasks, taskName, teamTasks]); + }, [isTaskPlanned, selectedPlan?.tasks, taskName, teamTasks]); const handleCreateTask = useCallback(async () => { try { @@ -592,6 +649,7 @@ export function SearchTaskInput(props: ISearchTaskInputProps) { plan={selectedPlan} setDefaultTask={setDefaultTask} isDefaultTask={task.id == defaultTask?.id} + selectedDate={selectedDate} /> ))} @@ -625,17 +683,19 @@ interface ITaskCardProps { task: ITeamTask; setDefaultTask: Dispatch>; isDefaultTask: boolean; - plan: IDailyPlan; + plan?: IDailyPlan; viewListMode?: 'planned' | 'searched'; + selectedDate?: Date; } function TaskCard(props: ITaskCardProps) { - const { task, plan, viewListMode = 'planned', isDefaultTask, setDefaultTask } = props; + const { task, plan, viewListMode = 'planned', isDefaultTask, setDefaultTask, selectedDate } = props; const { getTaskById } = useTeamTasks(); - const { addTaskToPlan } = useDailyPlan(); + const { addTaskToPlan, createDailyPlan } = useDailyPlan(); + const { user } = useAuthenticateUser(); const [addToPlanLoading, setAddToPlanLoading] = useState(false); const isTaskRenderedInTodayPlan = - new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); + plan && new Date(Date.now()).toLocaleDateString('en') == new Date(plan.date).toLocaleDateString('en'); const { isOpen: isTaskDetailsModalOpen, closeModal: closeTaskDetailsModal, @@ -664,13 +724,38 @@ function TaskCard(props: ITaskCardProps) { try { setAddToPlanLoading(true); - if (plan.id) await addTaskToPlan({ taskId: task.id }, plan.id); + if (plan && plan.id) { + await addTaskToPlan({ taskId: task.id }, plan.id); + } else { + const planDate = plan ? plan.date : selectedDate; + + if (planDate) { + await createDailyPlan({ + workTimePlanned: 0, + taskId: task.id, + date: new Date(moment(planDate).format('YYYY-MM-DD')), + status: DailyPlanStatusEnum.OPEN, + tenantId: user?.tenantId ?? '', + employeeId: user?.employee.id, + organizationId: user?.employee.organizationId + }); + } + } } catch (error) { console.log(error); } finally { setAddToPlanLoading(false); } - }, [addTaskToPlan, plan.id, task.id]); + }, [ + addTaskToPlan, + createDailyPlan, + plan, + selectedDate, + task.id, + user?.employee.id, + user?.employee.organizationId, + user?.tenantId + ]); return ( {addToPlanLoading ? : 'Add'} - ) : ( + ) : plan ? ( <> -
+
{checkPastDate(plan.date) ? ( - )} + ) : null}
- {activeTeamTask && ( + {plan && activeTeamTask && ( void; @@ -33,15 +35,14 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) const { isOpen, closeModal } = props; const [showCalendar, setShowCalendar] = useState(false); const [showCustomPlan, setShowCustomPlan] = useState(false); - const [customDate, setCustomDate] = useState(); + const [customDate, setCustomDate] = useState(moment().toDate()); const { myDailyPlans, pastPlans } = useDailyPlan(); + const t = useTranslations(); // Utility function for checking if two dates are the same - const isSameDate = useCallback( - (date1: Date, date2: Date) => - new Date(date1).toLocaleDateString('en') === new Date(date2).toLocaleDateString('en'), - [] - ); + const isSameDate = useCallback((date1: Date | number | string, date2: Date | number | string) => { + return moment(date1).toISOString().split('T')[0] === moment(date2).toISOString().split('T')[0]; + }, []); // Memoize today, tomorrow, and future plans const todayPlan = useMemo( @@ -54,9 +55,13 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) [isSameDate, myDailyPlans.items] ); - const selectedFuturePlan = useMemo( - () => customDate && myDailyPlans.items.find((plan) => isSameDate(plan.date, customDate)), - [isSameDate, myDailyPlans.items, customDate] + const selectedPlan = useMemo( + () => + customDate && + myDailyPlans.items.find((plan) => { + return isSameDate(plan.date.toString().split('T')[0], customDate.setHours(0, 0, 0, 0)); + }), + [customDate, myDailyPlans.items, isSameDate] ); // Handle modal close @@ -72,6 +77,11 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) // Handle tab switching const handleTabClick = (tab: CalendarTab) => { + if (tab === 'Today') { + setCustomDate(moment().toDate()); + } else if (tab === 'Tomorrow') { + setCustomDate(moment().add(1, 'days').toDate()); + } setSelectedTab(tab); setShowCalendar(tab === 'Calendar'); setShowCustomPlan(false); @@ -85,39 +95,53 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) case 'Tomorrow': return tomorrowPlan; case 'Calendar': - return selectedFuturePlan; + return selectedPlan; default: return undefined; } - }, [selectedTab, todayPlan, tomorrowPlan, selectedFuturePlan]); + }, [selectedTab, todayPlan, tomorrowPlan, selectedPlan]); - const [showSearchInput, setShowSearchInput] = useState(false); - const [defaultTask, setDefaultTask] = useState(null); - const [workTimePlanned, setWorkTimePlanned] = useState(0); - const [isWorkingTimeInputFocused, setWorkingTimeInputFocused] = useState(false); - const t = useTranslations(); + const { user } = useAuthenticateUser(); + const { createDailyPlan, createDailyPlanLoading } = useDailyPlan(); // Set the related tab for today and tomorrow dates const handleCalendarSelect = useCallback(() => { if (customDate) { - if ( - new Date(customDate).toLocaleDateString('en') === new Date(moment().toDate()).toLocaleDateString('en') - ) { + if (isSameDate(customDate, moment().startOf('day').toDate())) { setSelectedTab('Today'); - } else if ( - new Date(customDate).toLocaleDateString('en') === - new Date(moment().add(1, 'days').toDate()).toLocaleDateString('en') - ) { + setCustomDate(moment().toDate()); + } else if (isSameDate(customDate, moment().add(1, 'days').startOf('day').toDate())) { setSelectedTab('Tomorrow'); + setCustomDate(moment().add(1, 'days').toDate()); } else { setShowCalendar(false); setShowCustomPlan(true); } } - }, [customDate]); + }, [customDate, isSameDate]); + + const createEmptyPlan = useCallback(async () => { + try { + await createDailyPlan({ + workTimePlanned: 0, + date: new Date(moment(customDate).format('YYYY-MM-DD')), + status: DailyPlanStatusEnum.OPEN, + tenantId: user?.tenantId ?? '', + employeeId: user?.employee.id, + organizationId: user?.employee.organizationId + }); + } catch (error) { + console.log(error); + } + }, [createDailyPlan, customDate, user?.employee.id, user?.employee.organizationId, user?.tenantId]); return ( - + null} + className={clsxm('w-[36rem]')} + >
@@ -133,17 +157,19 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) - Back + {t('common.BACK')} )} {selectedTab == 'Calendar' - ? showCustomPlan && selectedFuturePlan - ? `PLAN FOR ${new Date(selectedFuturePlan.date).toLocaleDateString('en-GB')}` - : `PLANS` - : `${selectedTab}'S PLAN`} + ? showCustomPlan && selectedPlan + ? t('common.plan.FOR_DATE', { + date: new Date(selectedPlan.date).toLocaleDateString('en-GB') + }) + : t('common.plan.PLURAL') + : `${selectedTab === 'Today' ? t('common.plan.FOR_TODAY') : selectedTab === 'Tomorrow' ? t('common.plan.FOR_TOMORROW') : ''}`}
@@ -154,25 +180,33 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) className={`flex justify-center gap-4 items-center hover:text-primary cursor-pointer ${selectedTab === tab ? 'text-primary font-medium' : ''}`} onClick={() => handleTabClick(tab)} > - {tab} + + {tab === 'Today' + ? t('common.TODAY') + : tab === 'Tomorrow' + ? t('common.TOMORROW') + : t('common.CALENDAR')} + {index + 1 < tabs.length && } ))}
-
+
{selectedTab === 'Calendar' && showCalendar ? (
-

Select a date to be able to see a plan

+

{t('common.plan.CHOOSE_DATE')}

@@ -182,90 +216,53 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal)
) : ( <> - {plan ? ( + {selectedPlan ? ( + ) : customDate ? ( + ) : ( - <> - {showSearchInput ? ( - - ) : ( -
- - {t('timer.todayPlanSettings.WORK_TIME_PLANNED')}{' '} - * - -
- { - !isNaN(parseInt(e.target.value)) - ? setWorkTimePlanned(parseInt(e.target.value)) - : setWorkTimePlanned(0); - }} - required - noWrapper - min={0} - value={ - !isNaN(workTimePlanned) && - workTimePlanned.toString() !== '0' - ? workTimePlanned.toString().replace(/^0+/, '') - : isWorkingTimeInputFocused - ? '' - : 0 - } - onFocus={() => setWorkingTimeInputFocused(true)} - onBlur={() => setWorkingTimeInputFocused(false)} - defaultValue={0} - /> - -
-
- )} -
- } text="Plan not found " /> -
- +
+ } text={t('common.plan.PLAN_NOT_FOUND')} /> +
)} )} @@ -283,25 +280,31 @@ export const AllPlansModal = memo(function AllPlansModal(props: IAllPlansModal) */ interface ICalendarProps { - setSelectedPlan: Dispatch>; + setSelectedPlan: Dispatch>; selectedPlan: Date | undefined; plans: IDailyPlan[]; pastPlans: IDailyPlan[]; + handleCalendarSelect: () => void; + createEmptyPlan: () => Promise; } /** * The component that handles the selection of a plan * * @param {Object} props - The props object - * @param {Dispatch>} props.setSelectedFuturePlan - A function that set the selected plan - * @param {IDailyPlan} props.selectedFuturePlan - The selected plan + * @param {Dispatch>} props.setSelectedPlan - A function that set the selected plan + * @param {IDailyPlan} props.selectedPlan - The selected plan * @param {IDailyPlan[]} props.plans - Available plans * @param {IDailyPlan[]} props.pastPlans - Past plans + * @param {() => void} props.handleCalendarSelect - Handle plan selection + * @param {() => Promise} props.createEmptyPlan - Create empty plan * * @returns {JSX.Element} The Calendar component. */ const FuturePlansCalendar = memo(function FuturePlansCalendar(props: ICalendarProps) { - const { selectedPlan, setSelectedPlan, plans, pastPlans } = props; + const { selectedPlan, setSelectedPlan, plans, pastPlans, createEmptyPlan, handleCalendarSelect } = props; + const clickTimeoutRef = useRef(null); + const clickCountRef = useRef(0); const sortedPlans = useMemo( () => @@ -321,29 +324,70 @@ const FuturePlansCalendar = memo(function FuturePlansCalendar(props: ICalendarPr const isDateUnplanned = useCallback( (dateToCheck: Date) => { return !plans - .map((plan) => new Date(plan.date)) - .some( - (date) => new Date(date).toLocaleDateString('en') == new Date(dateToCheck).toLocaleDateString('en') - ); + .map((plan) => { + return moment(plan.date.toString().split('T')[0]).toISOString().split('T')[0]; + }) + .some((date) => { + return date === moment(dateToCheck).toISOString().split('T')[0]; + }); }, [plans] ); + /** + * onDayClick handler - A function that handles clicks on a day (date) + * + * @param {Date} day - The clicked date + * @param {ActiveModifiers} activeModifiers - The active modifiers + * @param {React.MouseEvent} e - The event + * + * @returns {void} Nothing + */ + const handleDayClick = useCallback( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (day: Date, activeModifiers: ActiveModifiers, e: React.MouseEvent) => { + if (activeModifiers.disabled) return; + + clickCountRef.current += 1; + + if (clickCountRef.current === 1) { + clickTimeoutRef.current = setTimeout(() => { + // Single click + clickCountRef.current = 0; + }, 300); + } else if (clickCountRef.current === 2) { + if (clickTimeoutRef.current) clearTimeout(clickTimeoutRef.current); + // Double click + if (selectedPlan) { + handleCalendarSelect(); + } else { + setSelectedPlan(moment(day).toDate()); + createEmptyPlan(); + } + clickCountRef.current = 0; + } + }, + [createEmptyPlan, handleCalendarSelect, selectedPlan, setSelectedPlan] + ); + return ( { if (date) { - setSelectedPlan(date); + setSelectedPlan(moment(date).toDate()); } }} initialFocus - disabled={isDateUnplanned} + disabled={(date) => { + return checkPastDate(date) && isDateUnplanned(date); + }} modifiers={{ - booked: sortedPlans?.map((plan) => new Date(plan.date)), - pastDay: pastPlans?.map((plan) => new Date(plan.date)) + booked: sortedPlans?.map((plan) => moment.utc(plan.date.toString().split('T')[0]).toDate()), + pastDay: pastPlans?.map((plan) => moment.utc(plan.date.toString().split('T')[0]).toDate()) }} modifiersClassNames={{ booked: clsxm( diff --git a/apps/web/lib/features/team/team-outstanding-notifications.tsx b/apps/web/lib/features/team/team-outstanding-notifications.tsx index 622d84cd4..d86c24d98 100644 --- a/apps/web/lib/features/team/team-outstanding-notifications.tsx +++ b/apps/web/lib/features/team/team-outstanding-notifications.tsx @@ -5,9 +5,10 @@ import { Cross2Icon, EyeOpenIcon } from '@radix-ui/react-icons'; import { Tooltip } from 'lib/components'; import { useTranslations } from 'next-intl'; import Link from 'next/link'; -import { useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { estimatedTotalTime } from '../task/daily-plan'; -import { HAS_VISITED_OUTSTANDING_TAB } from '@app/constants'; +import { HAS_VISITED_OUTSTANDING_TASKS } from '@app/constants'; +import moment from 'moment'; interface IEmployeeWithOutstanding { employeeId: string | undefined; @@ -37,45 +38,48 @@ export function TeamOutstandingNotifications() { ); } -function UserOutstandingNotification({ outstandingPlans, user }: { outstandingPlans: IDailyPlan[]; user?: IUser }) { +const UserOutstandingNotification = memo(function UserOutstandingNotification({ + outstandingPlans, + user +}: { + outstandingPlans: IDailyPlan[]; + user?: IUser; +}) { const t = useTranslations(); - // Notification will be displayed 6 hours after the user closed it - const REAPPEAR_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours in milliseconds; - const DISMISSAL_TIMESTAMP_KEY = 'user-saw-notif'; + // Notification will be displayed by next day const name = user?.name || user?.firstName || user?.lastName || user?.username; const [visible, setVisible] = useState(false); + const outStandingTasksCount = estimatedTotalTime( outstandingPlans.map((plan) => plan.tasks?.map((task) => task)) ).totalTasks; + const lastVisited = window?.localStorage.getItem(HAS_VISITED_OUTSTANDING_TASKS); + useEffect(() => { - const checkNotification = () => { - const alreadySeen = window && parseInt(window?.localStorage.getItem(DISMISSAL_TIMESTAMP_KEY) || '0', 10); - const hasVisitedOutstandingTab = - window && JSON.parse(window?.localStorage.getItem(HAS_VISITED_OUTSTANDING_TAB) as string); - const currentTime = new Date().getTime(); - - if (hasVisitedOutstandingTab) { - setVisible(false); - } else if (!alreadySeen || currentTime - alreadySeen > REAPPEAR_INTERVAL) { - setVisible(true); + if (lastVisited == new Date(moment().format('YYYY-MM-DD')).toISOString().split('T')[0]) { + setVisible(false); + } else { + setVisible(true); + if (!lastVisited) { + window?.localStorage.setItem( + HAS_VISITED_OUTSTANDING_TASKS, + new Date(moment().subtract(1, 'days').format('YYYY-MM-DD')).toISOString().split('T')[0] + ); } - }; - - checkNotification(); - const intervalId = setInterval(function () { - window && window?.localStorage.setItem(HAS_VISITED_OUTSTANDING_TAB, JSON.stringify(false)); - checkNotification(); - }, REAPPEAR_INTERVAL); - return () => clearInterval(intervalId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onClose = () => { - window && window?.localStorage.setItem(DISMISSAL_TIMESTAMP_KEY, new Date().getTime().toString()); + window.localStorage.setItem( + HAS_VISITED_OUTSTANDING_TASKS, + new Date(moment().format('YYYY-MM-DD')).toISOString().split('T')[0] + ); setVisible(false); }; @@ -113,15 +117,17 @@ function UserOutstandingNotification({ outstandingPlans, user }: { outstandingPl )} ); -} +}); -function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstandingTasks: IDailyPlan[] }) { +const ManagerOutstandingUsersNotification = memo(function ManagerOutstandingUsersNotification({ + outstandingTasks +}: { + outstandingTasks: IDailyPlan[]; +}) { const { user } = useAuthenticateUser(); const t = useTranslations(); - // Notification will be displayed 6 hours after the user closed it - const REAPPEAR_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours in milliseconds; - const MANAGER_DISMISSAL_TIMESTAMP_KEY = 'manager-saw-outstanding-notif'; + // Notification will be displayed by next day const [visible, setVisible] = useState(false); @@ -158,32 +164,29 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding [] ); + const lastVisited = window?.localStorage.getItem(HAS_VISITED_OUTSTANDING_TASKS); + useEffect(() => { - const checkNotification = () => { - const alreadySeen = - window && parseInt(window?.localStorage.getItem(MANAGER_DISMISSAL_TIMESTAMP_KEY) || '0', 10); - const hasVisitedOutstandingTab = - window && JSON.parse(window?.localStorage.getItem(HAS_VISITED_OUTSTANDING_TAB) as string); - const currentTime = new Date().getTime(); - - if (hasVisitedOutstandingTab) { - setVisible(false); - } else if (!alreadySeen || currentTime - alreadySeen > REAPPEAR_INTERVAL) { - setVisible(true); + if (lastVisited == new Date(moment().format('YYYY-MM-DD')).toISOString().split('T')[0]) { + setVisible(false); + } else { + setVisible(true); + if (!lastVisited) { + window?.localStorage.setItem( + HAS_VISITED_OUTSTANDING_TASKS, + new Date(moment().subtract(1, 'days').format('YYYY-MM-DD')).toISOString().split('T')[0] + ); } - }; - - checkNotification(); - const intervalId = setInterval(function () { - window && window?.localStorage.setItem(HAS_VISITED_OUTSTANDING_TAB, JSON.stringify(false)); - checkNotification(); - }, REAPPEAR_INTERVAL); - return () => clearInterval(intervalId); + } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onClose = () => { - window && window?.localStorage.setItem(MANAGER_DISMISSAL_TIMESTAMP_KEY, new Date().getTime().toString()); + window.localStorage.setItem( + HAS_VISITED_OUTSTANDING_TASKS, + new Date(moment().format('YYYY-MM-DD')).toISOString().split('T')[0] + ); setVisible(false); }; return ( @@ -214,4 +217,4 @@ function ManagerOutstandingUsersNotification({ outstandingTasks }: { outstanding )} ); -} +}); diff --git a/apps/web/lib/features/team/user-team-card/task-skeleton.tsx b/apps/web/lib/features/team/user-team-card/task-skeleton.tsx index e23538c55..7212cee4e 100644 --- a/apps/web/lib/features/team/user-team-card/task-skeleton.tsx +++ b/apps/web/lib/features/team/user-team-card/task-skeleton.tsx @@ -59,7 +59,7 @@ export function UserTeamCardHeader() {
-
+
{t('dailyPlan.TASK_TIME')} diff --git a/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx b/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx index 0e331efcd..ff074df9b 100644 --- a/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx +++ b/apps/web/lib/features/team/user-team-card/user-team-card-menu.tsx @@ -194,7 +194,7 @@ function DropdownMenu({ edition, memberInfo }: Props) { 'font-normal whitespace-nowrap text-sm hover:font-semibold hover:transition-all' )} > - See Plan + {t('common.plan.SEE_PLANS')} diff --git a/apps/web/lib/features/user-profile-plans.tsx b/apps/web/lib/features/user-profile-plans.tsx index f7a3655ac..e02acb305 100644 --- a/apps/web/lib/features/user-profile-plans.tsx +++ b/apps/web/lib/features/user-profile-plans.tsx @@ -14,7 +14,7 @@ import { useAuthenticateUser, useCanSeeActivityScreen, useDailyPlan, useUserProf import { useDateRange } from '@app/hooks/useDateRange'; import { filterDailyPlan } from '@app/hooks/useFilterDateRange'; import { useLocalStorageState } from '@app/hooks/useLocalStorageState'; -import { HAS_VISITED_OUTSTANDING_TAB } from '@app/constants'; +import { HAS_VISITED_OUTSTANDING_TASKS } from '@app/constants'; import { IDailyPlan, ITeamTask } from '@app/interfaces'; import { dataDailyPlanState } from '@app/stores'; import { fullWidthState } from '@app/stores/fullWidth'; @@ -37,6 +37,7 @@ import { FutureTasks } from './task/daily-plan/future-tasks'; import ViewsHeaderTabs from './task/daily-plan/views-header-tabs'; import TaskBlockCard from './task/task-block-card'; import { TaskCard } from './task/task-card'; +import moment from 'moment'; export type FilterTabs = 'Today Tasks' | 'Future Tasks' | 'Past Tasks' | 'All Tasks' | 'Outstanding'; type FilterOutstanding = 'ALL' | 'DATE'; @@ -92,7 +93,10 @@ export function UserProfilePlans() { setCurrentDataDailyPlan(futurePlans); setFilterFuturePlanData(filterDailyPlan(date as any, futurePlans)); } else if (currentTab === 'Outstanding') { - window.localStorage.setItem(HAS_VISITED_OUTSTANDING_TAB, JSON.stringify(true)); + window.localStorage.setItem( + HAS_VISITED_OUTSTANDING_TASKS, + new Date(moment().format('YYYY-MM-DD')).toISOString().split('T')[0] + ); } }, [currentTab, setCurrentDataDailyPlan, date, currentDataDailyPlan, futurePlans, pastPlans, sortedPlans]); diff --git a/apps/web/locales/ar.json b/apps/web/locales/ar.json index 366a8a383..b00789f45 100644 --- a/apps/web/locales/ar.json +++ b/apps/web/locales/ar.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "أضف لاحقًا", "DAILYPLAN": "مخطط", "INVITATION_SENT": "تم إرسال دعوة بنجاح", - "INVITATION_SENT_TO_USER": "تم إرسال دعوة فريق إلى {email}." + "INVITATION_SENT_TO_USER": "تم إرسال دعوة فريق إلى {email}.", + "CALENDAR": "تقويم", + "SELECT": "اختر", + "SAVE_CHANGES": "حفظ التغييرات", + "plan": { + "SINGULAR": "خطة", + "PLURAL": "خطط", + "CHOOSE_DATE": "اختر تاريخًا لتتمكن من رؤية خطة", + "PLAN_NOT_FOUND": "لم يتم العثور على خطة", + "FOR_DATE": "خطة لِـ {date}", + "FOR_TODAY": "خطة اليوم", + "FOR_TOMORROW": "خطة الغد", + "EDIT_PLAN": "تعديل الخطة", + "TRACKED_TIME": "الوقت المتعقب", + "SEE_PLANS": "عرض الخطط", + "ADD_PLAN": "إضافة خطة" + }, + "timesheets": { + "SINGULAR": "ورقة الحضور", + "PLURAL": "ورقات الحضور" + } }, "hotkeys": { "HELP": "مساعدة", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "وقت العمل المخطط له اليوم", "TASKS_WITH_NO_ESTIMATIONS": "تاكس مع عدم وجود تقديرات زمنية", "START_WORKING_BUTTON": "ابدأ العمل", + "TIMER_RUNNING": "المؤقت قيد التشغيل بالفعل", "WARNING_PLAN_ESTIMATION": "رجى تصحيح ساعات العمل المخطط لها أو إعادة تقدير المهمة (المهام)" } }, diff --git a/apps/web/locales/bg.json b/apps/web/locales/bg.json index 578d64818..923b4e5e0 100644 --- a/apps/web/locales/bg.json +++ b/apps/web/locales/bg.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Добави по-късно", "DAILYPLAN": "Планирано", "INVITATION_SENT": "Покана е изпратена успешно", - "INVITATION_SENT_TO_USER": "Покана е изпратена на {email}." + "INVITATION_SENT_TO_USER": "Покана е изпратена на {email}.", + "CALENDAR": "Календар", + "SELECT": "Изберете", + "SAVE_CHANGES": "Запази промените", + "plan": { + "SINGULAR": "План", + "PLURAL": "Планове", + "CHOOSE_DATE": "Изберете дата, за да видите план", + "PLAN_NOT_FOUND": "Не е намерен план", + "FOR_DATE": "ПЛАН ЗА {date}", + "FOR_TODAY": "Днешният план", + "FOR_TOMORROW": "Утрешният план", + "EDIT_PLAN": "Редактиране на план", + "TRACKED_TIME": "Проследено време", + "SEE_PLANS": "Виж планове", + "ADD_PLAN": "Добави план" + }, + "timesheets": { + "SINGULAR": "Работен лист", + "PLURAL": "Работни листове" + } }, "hotkeys": { "HELP": "Помощ", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Планирано работно време за днес", "TASKS_WITH_NO_ESTIMATIONS": "Такси без оценки на времето", "START_WORKING_BUTTON": "Започнете работа", + "TIMER_RUNNING": "Таймерът вече работи", "WARNING_PLAN_ESTIMATION": "Моля, коригирайте планираните работни часове или преоценете задачата(ите) " } }, diff --git a/apps/web/locales/de.json b/apps/web/locales/de.json index 43e1088b2..d6786ffc5 100644 --- a/apps/web/locales/de.json +++ b/apps/web/locales/de.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Später hinzufügen", "DAILYPLAN": "Plan dzienny", "INVITATION_SENT": "Einladung erfolgreich gesendet", - "INVITATION_SENT_TO_USER": "Eine Teameinladung wurde an {email} gesendet." + "INVITATION_SENT_TO_USER": "Eine Teameinladung wurde an {email} gesendet.", + "CALENDAR": "Kalender", + "SELECT": "Auswählen", + "SAVE_CHANGES": "Änderungen speichern", + "plan": { + "SINGULAR": "Plan", + "PLURAL": "Pläne", + "CHOOSE_DATE": "Wählen Sie ein Datum, um einen Plan zu sehen", + "PLAN_NOT_FOUND": "Plan nicht gefunden", + "FOR_DATE": "PLAN FÜR {date}", + "FOR_TODAY": "Heutiger Plan", + "FOR_TOMORROW": "Morgenplan", + "EDIT_PLAN": "Plan bearbeiten", + "TRACKED_TIME": "Verfolgte Zeit", + "SEE_PLANS": "Pläne anzeigen", + "ADD_PLAN": "Plan hinzufügen" + }, + "timesheets": { + "SINGULAR": "Stundenzettel", + "PLURAL": "Stundenzettel" + } }, "hotkeys": { "HELP": "Hilfe", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Für heute geplante Arbeitszeit", "TASKS_WITH_NO_ESTIMATIONS": "Taks ohne Zeitvorgaben", "START_WORKING_BUTTON": "Mit der Arbeit beginnen", + "TIMER_RUNNING": "Der Timer läuft bereits", "WARNING_PLAN_ESTIMATION": "Bitte korrigieren Sie die geplanten Arbeitsstunden oder schätzen Sie die Aufgabe(n) neu ein." } }, diff --git a/apps/web/locales/en.json b/apps/web/locales/en.json index 2febfb13e..e326b98df 100644 --- a/apps/web/locales/en.json +++ b/apps/web/locales/en.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Add later", "DAILYPLAN": "Planned", "INVITATION_SENT": "Invitation Sent Successfully", - "INVITATION_SENT_TO_USER": "A team invitation has been sent to {email}." + "INVITATION_SENT_TO_USER": "A team invitation has been sent to {email}.", + "CALENDAR": "Calendar", + "SELECT": "Select", + "SAVE_CHANGES": "Save changes", + "plan": { + "SINGULAR": "Plan", + "PLURAL": "Plans", + "CHOOSE_DATE": "Select a date to be able to see a plan", + "PLAN_NOT_FOUND": "Plan not found", + "FOR_DATE": "PLAN FOR {date}", + "FOR_TODAY": "Today's plan", + "FOR_TOMORROW": "Tomorrow's plan", + "EDIT_PLAN": "Edit Plan", + "TRACKED_TIME": "Tracked time", + "SEE_PLANS": "See plans", + "ADD_PLAN": "Add Plan" + }, + "timesheets": { + "SINGULAR": "Timesheet", + "PLURAL": "Timesheets" + } }, "hotkeys": { "HELP": "Help", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Working time planned for today", "TASKS_WITH_NO_ESTIMATIONS": "Tasks with no time estimations", "START_WORKING_BUTTON": "Start working", + "TIMER_RUNNING": "The timer is already running", "WARNING_PLAN_ESTIMATION": "Please correct planned work hours or re-estimate task(s)" } }, diff --git a/apps/web/locales/es.json b/apps/web/locales/es.json index fa97289f2..5ff10e4d1 100644 --- a/apps/web/locales/es.json +++ b/apps/web/locales/es.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Agregar más tarde", "DAILYPLAN": "Planificado", "INVITATION_SENT": "Invitación enviada con éxito", - "INVITATION_SENT_TO_USER": "Se ha enviado una invitación de equipo a {email}." + "INVITATION_SENT_TO_USER": "Se ha enviado una invitación de equipo a {email}.", + "CALENDAR": "Calendario", + "SELECT": "Seleccionar", + "SAVE_CHANGES": "Guardar cambios", + "plan": { + "SINGULAR": "Plan", + "PLURAL": "Planes", + "CHOOSE_DATE": "Seleccione una fecha para poder ver un plan", + "PLAN_NOT_FOUND": "Plan no encontrado", + "FOR_DATE": "PLAN PARA {date}", + "FOR_TODAY": "Plan de hoy", + "EDIT_PLAN": "Editar Plan", + "ADD_PLAN": "Agregar Plan", + "TRACKED_TIME": "Tiempo registrado", + "SEE_PLANS": "Ver planes", + "FOR_TOMORROW": "Plan de mañana" + }, + "timesheets": { + "SINGULAR": "Hoja de horas", + "PLURAL": "Hojas de horas" + } }, "hotkeys": { "HELP": "Ayuda", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Tiempo de trabajo previsto para hoy", "TASKS_WITH_NO_ESTIMATIONS": "Tarea sin estimación de tiempo", "START_WORKING_BUTTON": "Empezar a trabajar", + "TIMER_RUNNING": "El temporizador ya está en ejecución", "WARNING_PLAN_ESTIMATION": "Por favor, corrija las horas de trabajo previstas o reevalúe la(s) tarea(s)" } }, diff --git a/apps/web/locales/fr.json b/apps/web/locales/fr.json index b422827f3..1b1fce02c 100644 --- a/apps/web/locales/fr.json +++ b/apps/web/locales/fr.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Ajouter plus tard", "DAILYPLAN": "Planifié", "INVITATION_SENT": "Invitation envoyée avec succès", - "INVITATION_SENT_TO_USER": "Une invitation a été envoyée à {email}." + "INVITATION_SENT_TO_USER": "Une invitation a été envoyée à {email}.", + "CALENDAR": "Calendrier", + "SELECT": "Sélectionner", + "SAVE_CHANGES": "Enregistrer les modifications", + "plan": { + "SINGULAR": "Plan", + "PLURAL": "Plans", + "CHOOSE_DATE": "Sélectionnez une date pour pouvoir voir un plan", + "PLAN_NOT_FOUND": "Plan non trouvé", + "FOR_DATE": "PLAN POUR {date}", + "FOR_TODAY": "Le plan d'aujourd'hui", + "FOR_TOMORROW": "Le plan de demain", + "EDIT_PLAN": "Modifier le plan", + "TRACKED_TIME": "Temps suivi", + "SEE_PLANS": "Voir les plans", + "ADD_PLAN": "Ajouter un plan" + }, + "timesheets": { + "SINGULAR": "Feuille de temps", + "PLURAL": "Feuilles de temps" + } }, "hotkeys": { "HELP": "Aide", @@ -520,6 +540,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Temps de travail prévu pour aujourd'hui", "TASKS_WITH_NO_ESTIMATIONS": "Tâches sans estimations de temps", "START_WORKING_BUTTON": "Commencer à travailler", + "TIMER_RUNNING": "Le minuteur fonctionne déjà", "WARNING_PLAN_ESTIMATION": "Veuillez corriger les heures de travail prévues ou réévaluer la/les tâche(s)" }, "TIME_ACTIVITY": "Activité", diff --git a/apps/web/locales/he.json b/apps/web/locales/he.json index 36c0a130c..296206f2c 100644 --- a/apps/web/locales/he.json +++ b/apps/web/locales/he.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "הוסף מאוחר יותר", "DAILYPLAN": "מְתוּכנָן", "INVITATION_SENT": "ההזמנה נשלחה בהצלחה", - "INVITATION_SENT_TO_USER": "ההזמנה נשלחה ל{email}." + "INVITATION_SENT_TO_USER": "ההזמנה נשלחה ל{email}.", + "CALENDAR": "לוח שנה", + "SELECT": "בחר", + "SAVE_CHANGES": "שמור שינויים", + "plan": { + "SINGULAR": "תכנית", + "PLURAL": "תכניות", + "CHOOSE_DATE": "בחר תאריך כדי לראות תוכנית", + "PLAN_NOT_FOUND": "התוכנית לא נמצאה", + "FOR_DATE": "תכנית ל-{date}", + "FOR_TODAY": "תכנית להיום", + "FOR_TOMORROW": "תכנית למחר", + "EDIT_PLAN": "ערוך תוכנית", + "TRACKED_TIME": "זמן מעקב", + "SEE_PLANS": "ראה תוכניות", + "ADD_PLAN": "הוסף תוכנית" + }, + "timesheets": { + "SINGULAR": "דוח שעות", + "PLURAL": "דוחות שעות" + } }, "hotkeys": { "HELP": "עזרה", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "זמן עבודה מתוכנן להיום", "TASKS_WITH_NO_ESTIMATIONS": "משימות בלי הערכת זמן", "START_WORKING_BUTTON": "התחל לעבוד", + "TIMER_RUNNING": "הטיימר כבר פועל", "WARNING_PLAN_ESTIMATION": "נא לתקן את שעות העבודה המתוכננות או להעריך מחדש את המשימות" } }, diff --git a/apps/web/locales/it.json b/apps/web/locales/it.json index 09de582a1..3fd71133b 100644 --- a/apps/web/locales/it.json +++ b/apps/web/locales/it.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Aggiungi dopo", "DAILYPLAN": "Pianificato", "INVITATION_SENT": "Invito inviato con successo", - "INVITATION_SENT_TO_USER": "Un invito alla squadra è stato inviato a {email}." + "INVITATION_SENT_TO_USER": "Un invito alla squadra è stato inviato a {email}.", + "timesheets": { + "SINGULAR": "Scheda attività", + "PLURAL": "Schede attività" + }, + "CALENDAR": "Calendario", + "SELECT": "Seleziona", + "SAVE_CHANGES": "Salva modifiche", + "plan": { + "SINGULAR": "Piano", + "PLURAL": "Piani", + "CHOOSE_DATE": "Seleziona una data per poter vedere un piano", + "PLAN_NOT_FOUND": "Piano non trovato", + "FOR_DATE": "PIANO PER {date}", + "FOR_TODAY": "Piano di oggi", + "FOR_TOMORROW": "Piano di domani", + "EDIT_PLAN": "Modifica Piano", + "TRACKED_TIME": "Tempo tracciato", + "SEE_PLANS": "Vedi piani", + "ADD_PLAN": "Aggiungi Piano" + } }, "hotkeys": { "HELP": "Aiuto", @@ -520,6 +540,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Tempo di lavoro pianificato per oggi", "TASKS_WITH_NO_ESTIMATIONS": "Compiti senza stime di tempo", "START_WORKING_BUTTON": "Inizia a lavorare", + "TIMER_RUNNING": "Il timer è già in esecuzione", "WARNING_PLAN_ESTIMATION": "Correggere le ore di lavoro previste o rivalutare il/i compito/i." }, "TIME_ACTIVITY": "Attività", diff --git a/apps/web/locales/nl.json b/apps/web/locales/nl.json index d529bc994..2049eede7 100644 --- a/apps/web/locales/nl.json +++ b/apps/web/locales/nl.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Later toevoegen", "DAILYPLAN": "Gepland", "INVITATION_SENT": "Uitnodiging verzonden", - "INVITATION_SENT_TO_USER": "Een teamuitnodiging is verzonden naar {email}." + "INVITATION_SENT_TO_USER": "Een teamuitnodiging is verzonden naar {email}.", + "timesheets": { + "SINGULAR": "Urenstaat", + "PLURAL": "Urenstaten" + }, + "CALENDAR": "Kalender", + "SELECT": "Selecteren", + "SAVE_CHANGES": "Wijzigingen opslaan", + "plan": { + "SINGULAR": "Plan", + "PLURAL": "Plannen", + "CHOOSE_DATE": "Selecteer een datum om een plan te kunnen zien", + "PLAN_NOT_FOUND": "Plan niet gevonden", + "FOR_DATE": "PLAN VOOR {date}", + "FOR_TODAY": "Vandaag's plan", + "FOR_TOMORROW": "Morgen's plan", + "EDIT_PLAN": "Plan bewerken", + "TRACKED_TIME": "Gegeneraliseerde tijd", + "SEE_PLANS": "Plannen bekijken", + "ADD_PLAN": "Plan toevoegen" + } }, "hotkeys": { "HELP": "Help", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Geplande werktijd voor vandaag", "TASKS_WITH_NO_ESTIMATIONS": "Taken zonder tijdschattingen", "START_WORKING_BUTTON": "Begin met werken", + "TIMER_RUNNING": "De timer loopt al", "WARNING_PLAN_ESTIMATION": "Corrigeer de geplande werkuren of schat de taak(en) opnieuw in" } }, diff --git a/apps/web/locales/pl.json b/apps/web/locales/pl.json index b3c753449..9d4c0a5c4 100644 --- a/apps/web/locales/pl.json +++ b/apps/web/locales/pl.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Dodaj później", "DAILYPLAN": "Planowane", "INVITATION_SENT": "Zaproszenie wysłane", - "INVITATION_SENT_TO_USER": "Zaproszenie zespołu zostało wysłane do {email}." + "INVITATION_SENT_TO_USER": "Zaproszenie zespołu zostało wysłane do {email}.", + "timesheets": { + "SINGULAR": "Karta pracy", + "PLURAL": "Karty pracy" + }, + "CALENDAR": "Kalendarz", + "SELECT": "Wybierz", + "SAVE_CHANGES": "Zapisz zmiany", + "plan": { + "SINGULAR": "Plan", + "PLURAL": "Plany", + "CHOOSE_DATE": "Wybierz datę, aby móc zobaczyć plan", + "PLAN_NOT_FOUND": "Plan nie znaleziony", + "FOR_DATE": "PLAN NA {date}", + "FOR_TODAY": "Dzienny plan", + "FOR_TOMORROW": "Jutro plan", + "EDIT_PLAN": "Edytuj plan", + "TRACKED_TIME": "Śledzony czas", + "SEE_PLANS": "Zobacz plany", + "ADD_PLAN": "Dodaj plan" + } }, "hotkeys": { "HELP": "Pomoc", @@ -520,6 +540,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Zaplanowany czas pracy na dziś", "TASKS_WITH_NO_ESTIMATIONS": "Zadania bez oszacowania czasu", "START_WORKING_BUTTON": "Rozpocznij pracę", + "TIMER_RUNNING": "Timer już działa", "WARNING_PLAN_ESTIMATION": "Popraw planowane godziny pracy lub ponownie oszacuj zadania" }, "TIME_ACTIVITY": "Aktywność", diff --git a/apps/web/locales/pt.json b/apps/web/locales/pt.json index efcbf1bc8..763c20dc7 100644 --- a/apps/web/locales/pt.json +++ b/apps/web/locales/pt.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Adicionar depois", "DAILYPLAN": "Planeado", "INVITATION_SENT": "Convite enviado com sucesso", - "INVITATION_SENT_TO_USER": "Um convite de equipe foi enviado para {email}." + "INVITATION_SENT_TO_USER": "Um convite de equipe foi enviado para {email}.", + "timesheets": { + "SINGULAR": "Folha de ponto", + "PLURAL": "Folhas de ponto" + }, + "CALENDAR": "Calendário", + "SELECT": "Selecionar", + "SAVE_CHANGES": "Salvar alterações", + "plan": { + "SINGULAR": "Plano", + "PLURAL": "Planos", + "CHOOSE_DATE": "Selecione uma data para poder ver um plano", + "PLAN_NOT_FOUND": "Plano não encontrado", + "FOR_DATE": "PLANO PARA {date}", + "FOR_TODAY": "Plano de hoje", + "FOR_TOMORROW": "Plano de amanhã", + "EDIT_PLAN": "Editar Plano", + "TRACKED_TIME": "Tempo rastreado", + "SEE_PLANS": "Ver planos", + "ADD_PLAN": "Adicionar Plano" + } }, "hotkeys": { "HELP": "Ajuda", @@ -520,6 +540,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Tempo de trabalho planejado para hoje", "TASKS_WITH_NO_ESTIMATIONS": "Tarefas sem estimativas de tempo", "START_WORKING_BUTTON": "Começar a trabalhar", + "TIMER_RUNNING": "O temporizador já está em execução", "WARNING_PLAN_ESTIMATION": "Corrigir as horas de trabalho planeadas ou fazer uma nova estimativa da(s) tarefa(s)" }, "TIME_ACTIVITY": "Atividade", diff --git a/apps/web/locales/ru.json b/apps/web/locales/ru.json index defbb101c..36ae292c6 100644 --- a/apps/web/locales/ru.json +++ b/apps/web/locales/ru.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "Добавить позже", "DAILYPLAN": "Планируется", "INVITATION_SENT": "Приглашение отправлено успешно", - "INVITATION_SENT_TO_USER": "Приглашение к команде было отправлено на {email}." + "INVITATION_SENT_TO_USER": "Приглашение к команде было отправлено на {email}.", + "timesheets": { + "SINGULAR": "Табель", + "PLURAL": "Табели" + }, + "CALENDAR": "Календарь", + "SELECT": "Выбрать", + "SAVE_CHANGES": "Сохранить изменения", + "plan": { + "SINGULAR": "План", + "PLURAL": "Планы", + "CHOOSE_DATE": "Выберите дату, чтобы увидеть план", + "PLAN_NOT_FOUND": "План не найден", + "FOR_DATE": "ПЛАН НА {date}", + "FOR_TODAY": "Сегодняшний план", + "FOR_TOMORROW": "Завтрашний план", + "EDIT_PLAN": "Редактировать план", + "TRACKED_TIME": "Отслеживаемое время", + "SEE_PLANS": "Посмотреть планы", + "ADD_PLAN": "Agregar Plan" + } }, "hotkeys": { "HELP": "Помощь", @@ -520,6 +540,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "Запланированное время работы на сегодня", "TASKS_WITH_NO_ESTIMATIONS": "Задачи без оценки времени", "START_WORKING_BUTTON": "Начать работу", + "TIMER_RUNNING": "Таймер уже запущен", "WARNING_PLAN_ESTIMATION": "Пожалуйста, исправьте запланированные часы работы или пересчитайте задание(я)" }, "TIME_ACTIVITY": "Активность", diff --git a/apps/web/locales/zh.json b/apps/web/locales/zh.json index d60241854..5feaa682c 100644 --- a/apps/web/locales/zh.json +++ b/apps/web/locales/zh.json @@ -202,7 +202,27 @@ "SKIP_ADD_LATER": "稍后添加", "DAILYPLAN": "計劃中", "INVITATION_SENT": "邀请已发送", - "INVITATION_SENT_TO_USER": "团队邀请已发送给 {email}。" + "INVITATION_SENT_TO_USER": "团队邀请已发送给 {email}。", + "timesheets": { + "SINGULAR": "时间表", + "PLURAL": "时间表" + }, + "CALENDAR": "日历", + "SELECT": "Select", + "SAVE_CHANGES": "保存更改", + "plan": { + "SINGULAR": "计划", + "PLURAL": "计划", + "CHOOSE_DATE": "选择一个日期以查看计划", + "PLAN_NOT_FOUND": "未找到计划", + "FOR_DATE": "针对日期 {date}", + "FOR_TODAY": "今天的计划", + "FOR_TOMORROW": "明天的计划", + "EDIT_PLAN": "编辑计划", + "TRACKED_TIME": "跟踪时间", + "SEE_PLANS": "查看计划", + "ADD_PLAN": "添加计划" + } }, "hotkeys": { "HELP": "帮助", @@ -537,6 +557,7 @@ "WORK_TIME_PLANNED_PLACEHOLDER": "今天计划的工作时间", "TASKS_WITH_NO_ESTIMATIONS": "没有时间估算的任务", "START_WORKING_BUTTON": "开始工作", + "TIMER_RUNNING": "计时器已经在运行", "WARNING_PLAN_ESTIMATION": "请更正计划工时或重新估算任务工时" } }, diff --git a/northflank-template.json b/northflank-template.json index 1caa83873..06769f1dc 100644 --- a/northflank-template.json +++ b/northflank-template.json @@ -1,59 +1,86 @@ { - "apiVersion": "v1", - "spec": { - "kind": "Workflow", - "spec": { - "type": "sequential", - "steps": [ - { - "kind": "DeploymentService", - "spec": { - "deployment": { - "instances": 1, - "docker": { - "configType": "default" - }, - "external": { - "imagePath": "ghcr.io/ever-co/ever-teams-webapp:latest" - } - }, - "runtimeEnvironment": {}, - "runtimeFiles": {}, - "billing": { - "deploymentPlan": "nf-compute-10" - }, - "name": "ever-teams-web", - "ports": [ - { - "internalPort": 3030, - "protocol": "HTTP", - "public": true, - "name": "p01", - "domains": [], - "security": { - "policies": [], - "credentials": [] - }, - "disableNfDomain": false - } - ] - } - } - ] - } - }, - "name": "ever-teams-template", - "description": "Open Work and Project Management Platform.", - "project": { - "spec": { - "name": "Ever Teams Template", - "region": "europe-west", - "description": "Open Work and Project Management Platform.", - "color": "#3826A6", - "networking": { - "allowedIngressProjects": [] - } - } - }, - "$schema": "https://api.northflank.com/v1/schemas/template" -} + "apiVersion": "v1.2", + "spec": { + "kind": "Workflow", + "spec": { + "type": "sequential", + "steps": [ + { + "kind": "Project", + "ref": "project", + "spec": { + "name": "Ever Teams Template", + "region": "europe-west", + "description": "Open Work and Project Management Platform.", + "color": "#3826A6", + "networking": { + "allowedIngressProjects": [] + } + } + }, + { + "kind": "Workflow", + "spec": { + "type": "sequential", + "steps": [ + { + "kind": "DeploymentService", + "spec": { + "deployment": { + "instances": 1, + "docker": { + "configType": "default" + }, + "external": { + "imagePath": "ghcr.io/ever-co/ever-teams-webapp:latest" + } + }, + "runtimeEnvironment": {}, + "runtimeFiles": {}, + "billing": { + "deploymentPlan": "nf-compute-10" + }, + "name": "ever-teams-web", + "ports": [ + { + "internalPort": 3030, + "protocol": "HTTP", + "public": true, + "name": "p01", + "domains": [], + "security": { + "policies": [], + "credentials": [] + }, + "disableNfDomain": false + } + ] + } + } + ], + "context": { + "projectId": "${refs.project.id}" + } + } + } + ] + } + }, + "name": "ever-teams-template", + "description": "Open Work and Project Management Platform.", + "options": { + "autorun": false, + "concurrencyPolicy": "allow" + }, + "gitops": { + "repoUrl": "https://github.com/ever-co/ever-teams", + "vcsService": "github", + "vcsLinkId": "6561e62d6069fdc2fe81dffc", + "installationId": 44402628, + "branch": "develop", + "filePath": "/northflank-template.json", + "templateSha": "8b67d68a7666047ccd64928875f0a88fb762035b", + "templateSHA": "12e71fb141d67d0a4d61dec6962c3d1ec57f98fc" + }, + "$schema": "https://api.northflank.com/v1/schemas/template" +} \ No newline at end of file